├── composeApp
├── src
│ ├── androidMain
│ │ ├── res
│ │ │ ├── resources.properties
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ └── themes.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── xml
│ │ │ │ └── locale_config.xml
│ │ │ ├── values-v31
│ │ │ │ └── themes.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher_round.xml
│ │ │ │ └── ic_launcher.xml
│ │ │ └── drawable
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ └── ic_launcher_monochrome.xml
│ │ ├── kotlin
│ │ │ └── pw
│ │ │ │ └── janyo
│ │ │ │ └── whatanime
│ │ │ │ ├── ui
│ │ │ │ ├── screen
│ │ │ │ │ └── MainScreen.android.kt
│ │ │ │ ├── theme
│ │ │ │ │ ├── NightMode.android.kt
│ │ │ │ │ └── Theme.android.kt
│ │ │ │ └── activity
│ │ │ │ │ └── MainActivity.kt
│ │ │ │ ├── module
│ │ │ │ ├── DatabaseModule.android.kt
│ │ │ │ ├── NetworkModule.android.kt
│ │ │ │ └── PlatformModule.android.kt
│ │ │ │ ├── utils
│ │ │ │ ├── FileUtil.android.kt
│ │ │ │ └── Util.android.kt
│ │ │ │ ├── ApplicationExt.kt
│ │ │ │ ├── Application.kt
│ │ │ │ ├── Configure.android.kt
│ │ │ │ └── base
│ │ │ │ └── AppInfo.android.kt
│ │ └── AndroidManifest.xml
│ ├── commonMain
│ │ ├── composeResources
│ │ │ ├── drawable
│ │ │ │ ├── janyo_studio.png
│ │ │ │ ├── ic_app_store.xml
│ │ │ │ ├── ic_google_play.xml
│ │ │ │ ├── ic_app_icon.xml
│ │ │ │ ├── ic_whatanime.xml
│ │ │ │ ├── ic_github.xml
│ │ │ │ └── ic_load_failed.xml
│ │ │ ├── values-zh-rCN
│ │ │ │ └── strings.xml
│ │ │ ├── values-zh-rTW
│ │ │ │ └── strings.xml
│ │ │ └── values
│ │ │ │ └── strings.xml
│ │ └── kotlin
│ │ │ └── pw
│ │ │ └── janyo
│ │ │ └── whatanime
│ │ │ ├── module
│ │ │ ├── PlatformModule.kt
│ │ │ ├── RepositoryModule.kt
│ │ │ ├── Module.kt
│ │ │ ├── ViewModelModule.kt
│ │ │ ├── MediaModule.kt
│ │ │ ├── NetworkModule.kt
│ │ │ └── DatabaseModule.kt
│ │ │ ├── model
│ │ │ ├── DebugHttpInfo.kt
│ │ │ ├── SearchQuota.kt
│ │ │ ├── Animation.kt
│ │ │ └── AnimationHistory.kt
│ │ │ ├── Helper.kt
│ │ │ ├── db
│ │ │ ├── DB.kt
│ │ │ ├── service
│ │ │ │ ├── HistoryService.kt
│ │ │ │ └── HistoryServiceImpl.kt
│ │ │ └── dao
│ │ │ │ └── HistoryDao.kt
│ │ │ ├── base
│ │ │ ├── AppInfo.kt
│ │ │ └── ComposeViewModel.kt
│ │ │ ├── utils
│ │ │ ├── Util.kt
│ │ │ ├── FileUtil.kt
│ │ │ ├── EncryptUtil.kt
│ │ │ ├── StringUtil.kt
│ │ │ └── TimeUtil.kt
│ │ │ ├── ui
│ │ │ ├── theme
│ │ │ │ ├── NightMode.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Icon.kt
│ │ │ ├── components
│ │ │ │ ├── DialogState.kt
│ │ │ │ ├── VideoDialog.kt
│ │ │ │ ├── MediaPlayer.kt
│ │ │ │ ├── ShowProgressDialog.kt
│ │ │ │ ├── NoDataLayout.kt
│ │ │ │ ├── AppInfo.kt
│ │ │ │ └── SwipeToDeleteContainer.kt
│ │ │ ├── preference
│ │ │ │ ├── Group.kt
│ │ │ │ └── Settings.kt
│ │ │ ├── navigation
│ │ │ │ └── Nav.kt
│ │ │ └── screen
│ │ │ │ ├── AboutScreen.kt
│ │ │ │ └── DetailScreen.kt
│ │ │ ├── api
│ │ │ └── SearchApi.kt
│ │ │ ├── Configure.kt
│ │ │ ├── viewmodel
│ │ │ ├── HistoryViewModel.kt
│ │ │ ├── DetailViewModel.kt
│ │ │ ├── SettingsViewModel.kt
│ │ │ └── MainViewModel.kt
│ │ │ ├── App.kt
│ │ │ └── repository
│ │ │ └── AnimationRepository.kt
│ └── iosMain
│ │ └── kotlin
│ │ └── pw
│ │ └── janyo
│ │ └── whatanime
│ │ ├── MainViewController.kt
│ │ ├── ui
│ │ ├── screen
│ │ │ └── MainScreen.ios.kt
│ │ └── theme
│ │ │ ├── NightMode.ios.kt
│ │ │ └── Theme.ios.kt
│ │ ├── module
│ │ ├── NetworkModule.ios.kt
│ │ ├── PlatformModule.ios.kt
│ │ └── DatabaseModule.ios.kt
│ │ ├── utils
│ │ ├── FileUtil.ios.kt
│ │ └── Util.ios.kt
│ │ ├── Configure.ios.kt
│ │ └── base
│ │ └── AppInfo.ios.kt
├── proguard-rules.pro
├── dictionary.txt
└── build.gradle.kts
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── iosApp
├── iosApp
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── app-icon-1024.jpg
│ │ │ └── Contents.json
│ │ └── AccentColor.colorset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── iOSApp.swift
│ ├── Info.plist
│ └── ContentView.swift
├── iosApp.xcodeproj
│ └── project.xcworkspace
│ │ └── contents.xcworkspacedata
└── Configuration
│ └── Config.xcconfig.template
├── CHANGELOG.md
├── gradle.properties
├── README.md
├── .gitignore
├── signing.gradle
├── settings.gradle.kts
├── .github
└── workflows
│ ├── release.yaml
│ ├── build_android.yaml
│ └── build_ios.yml
├── gradlew.bat
└── gradlew
/composeApp/src/androidMain/res/resources.properties:
--------------------------------------------------------------------------------
1 | unqualifiedResLocale=en-US
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanYoStudio/WhatAnime/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | WhatAnime
3 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanYoStudio/WhatAnime/HEAD/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanYoStudio/WhatAnime/HEAD/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanYoStudio/WhatAnime/HEAD/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanYoStudio/WhatAnime/HEAD/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanYoStudio/WhatAnime/HEAD/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### 修复
2 |
3 | - 修复多语言选择界面只有英语的问题
4 | - 修复视频预览无法播放
5 |
6 | ### 优化
7 |
8 | - 优化文字选择体验,现在使用与平台相匹配的文字选择器
9 | - 优化检索结果交互体验
10 | - 将 Material3 升级到 Material Express
11 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanYoStudio/WhatAnime/HEAD/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanYoStudio/WhatAnime/HEAD/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanYoStudio/WhatAnime/HEAD/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanYoStudio/WhatAnime/HEAD/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/janyo_studio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanYoStudio/WhatAnime/HEAD/composeApp/src/commonMain/composeResources/drawable/janyo_studio.png
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JanYoStudio/WhatAnime/HEAD/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.jpg
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/module/PlatformModule.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.module
2 |
3 | import org.koin.core.module.Module
4 |
5 | expect fun platformModule(): Module
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/pw/janyo/whatanime/MainViewController.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime
2 |
3 | import androidx.compose.ui.window.ComposeUIViewController
4 |
5 | fun MainViewController() = ComposeUIViewController { App() }
--------------------------------------------------------------------------------
/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/model/DebugHttpInfo.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.model
2 |
3 | data class DebugHttpInfo(
4 | val title: String,
5 | val datetime: String,
6 | val response: String,
7 | )
8 |
--------------------------------------------------------------------------------
/iosApp/Configuration/Config.xcconfig.template:
--------------------------------------------------------------------------------
1 | TEAM_ID=
2 |
3 | PRODUCT_NAME=WhatAnime
4 | PRODUCT_BUNDLE_IDENTIFIER=pw.janyo.whatanime.WhatAnime$(TEAM_ID)
5 |
6 | CURRENT_PROJECT_VERSION={gitVersionCode}
7 | MARKETING_VERSION={appVersionName}
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/pw/janyo/whatanime/ui/screen/MainScreen.ios.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.screen
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | @Composable
6 | actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
7 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | #Kotlin
2 | kotlin.code.style=official
3 | kotlin.daemon.jvmargs=-Xmx3072M
4 |
5 | #Gradle
6 | org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
7 |
8 | #Android
9 | android.nonTransitiveRClass=true
10 | android.useAndroidX=true
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/Helper.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime
2 |
3 | import org.koin.core.context.startKoin
4 | import pw.janyo.whatanime.module.moduleList
5 |
6 | fun initKoin(){
7 | startKoin {
8 | modules(moduleList())
9 | }
10 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/module/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.module
2 |
3 | import org.koin.dsl.module
4 | import pw.janyo.whatanime.repository.AnimationRepository
5 |
6 | val repositoryModule = module {
7 | single { AnimationRepository() }
8 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/iOSApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import ComposeApp
3 |
4 | @main
5 | struct iOSApp: App {
6 | init() {
7 | HelperKt.doInitKoin()
8 | }
9 | var body: some Scene {
10 | WindowGroup {
11 | ContentView()
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/xml/locale_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values-v31/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/pw/janyo/whatanime/ui/theme/NightMode.ios.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.theme
2 |
3 | actual fun showNightModeSelectList(): List {
4 | val list = NightMode.entries.sortedBy { it.value }.toMutableList()
5 | list.remove(NightMode.MATERIAL_YOU)
6 | return list
7 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/pw/janyo/whatanime/ui/screen/MainScreen.android.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.screen
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | @Composable
6 | actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) {
7 | androidx.activity.compose.BackHandler(enabled, onBack)
8 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/module/Module.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.module
2 |
3 | import org.koin.core.module.Module
4 |
5 | fun moduleList(): List =
6 | listOf(
7 | platformModule(),
8 | databaseModule,
9 | networkModule,
10 | viewModelModule,
11 | repositoryModule,
12 | mediaModule,
13 | )
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/model/SearchQuota.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class SearchQuota(
7 | val priority: Int = 0,
8 | val concurrency: Int = 0,
9 | val quota: Int = 0,
10 | val quotaUsed: Int = 0,
11 | ) {
12 | companion object {
13 | val EMPTY = SearchQuota()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/pw/janyo/whatanime/ui/theme/NightMode.android.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.theme
2 |
3 | import android.os.Build
4 |
5 | actual fun showNightModeSelectList(): List {
6 | val list = NightMode.entries.sortedBy { it.value }.toMutableList()
7 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
8 | list.remove(NightMode.MATERIAL_YOU)
9 | }
10 | return list
11 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CADisableMinimumFrameDurationOnPhone
6 |
7 | CFBundleLocalizations
8 |
9 | en-US
10 | zh-CN
11 | zh-TW
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/pw/janyo/whatanime/module/NetworkModule.ios.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.module
2 |
3 | import io.ktor.client.engine.HttpClientEngineConfig
4 | import io.ktor.client.engine.HttpClientEngineFactory
5 | import io.ktor.client.engine.darwin.Darwin
6 |
7 | actual fun httpClientEngine(): HttpClientEngineFactory = Darwin
8 | actual fun httpClientEngineConfig(config: HttpClientEngineConfig) {
9 | }
10 |
11 | actual fun userAgent(): String = "TODO"
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/pw/janyo/whatanime/module/PlatformModule.ios.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.module
2 |
3 | import androidx.room.RoomDatabase
4 | import androidx.sqlite.driver.NativeSQLiteDriver
5 | import org.koin.core.module.Module
6 | import org.koin.dsl.module
7 | import pw.janyo.whatanime.db.AppDatabase
8 |
9 | actual fun platformModule(): Module = module {
10 | single> {
11 | getDatabaseBuilder()
12 | .setDriver(NativeSQLiteDriver())
13 | }
14 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # “JanYo Studio” 授权解决方案(草案)
2 |
3 | ***为避免在其它非“JanYo Studio”通过书面授权认可的平台引发的授权 风险/争议,特拟此案。***
4 |
5 | “JanYo Studio”(以下简称“工作室”)发布的产品缺省使用“GPL(GNU General Public License)”授权协议。工作室将在产品内部标识产品源程序及其附属品,使得任何人可以自由获取、阅读、传播,以及基于源代码重新构建它的二进制版本并发行该副本。
6 |
7 | 工作室希望使用者在自由使用我们的产品时,尊重工作室的劳动成果,承认工作室的知识产权。
8 |
9 | 工作室保证产品的自由使用、传播、阅读不受限,使用者应保障工作室的合法权益:
10 | - 针对开源资料,禁止删改原作者注明的版权、授权方式 等信息。
11 | - 源代码允许自由使用、添加至自己的项目,但必须注明来源,以及源的授权协议。
12 | - 在从事盈利、商用活动中,针对产品的传播复制需注明来源,以及源的授权协议。
13 |
14 | **针对产品授权、答疑可电邮至工作室官方电子邮箱 : mystery0dyl520@gmail.com**
15 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/pw/janyo/whatanime/module/DatabaseModule.android.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.module
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import androidx.room.RoomDatabase
6 |
7 | import pw.janyo.whatanime.db.AppDatabase
8 |
9 | fun getDatabaseBuilder(ctx: Context): RoomDatabase.Builder {
10 | val appContext = ctx.applicationContext
11 | return Room.databaseBuilder(
12 | context = appContext,
13 | klass = AppDatabase::class.java,
14 | name = DATABASE_NAME
15 | )
16 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/ContentView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 | import ComposeApp
4 |
5 | struct ComposeView: UIViewControllerRepresentable {
6 | func makeUIViewController(context: Context) -> UIViewController {
7 | MainViewControllerKt.MainViewController()
8 | }
9 |
10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
11 | }
12 |
13 | struct ContentView: View {
14 | var body: some View {
15 | ComposeView()
16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/db/DB.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.db
2 |
3 | import androidx.room.ConstructedBy
4 | import androidx.room.Database
5 | import androidx.room.RoomDatabase
6 | import pw.janyo.whatanime.db.dao.HistoryDao
7 | import pw.janyo.whatanime.model.AnimationHistory
8 | import pw.janyo.whatanime.module.AppDatabaseConstructor
9 |
10 | @Database(entities = [(AnimationHistory::class)], version = 5)
11 | @ConstructedBy(AppDatabaseConstructor::class)
12 | abstract class AppDatabase : RoomDatabase() {
13 | abstract fun getHistoryDao(): HistoryDao
14 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .kotlin
3 | .gradle
4 | **/build/
5 | xcuserdata
6 | !src/**/build/
7 | composeApp/mapping.txt
8 | composeApp/seeds.txt
9 | composeApp/unused.txt
10 | composeApp/schemas/
11 | composeApp/src/commonMain/composeResources/files/aboutlibraries.json
12 | local.properties
13 | .idea
14 | .DS_Store
15 | captures
16 | .externalNativeBuild
17 | .cxx
18 | *.xcodeproj/*
19 | !*.xcodeproj/project.pbxproj
20 | !*.xcodeproj/xcshareddata/
21 | !*.xcodeproj/project.xcworkspace/
22 | !*.xcworkspace/contents.xcworkspacedata
23 | **/xcshareddata/WorkspaceSettings.xcsettings
24 | /iosApp/Pods/
25 | /iosApp/Configuration/Config.xcconfig
26 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/db/service/HistoryService.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.db.service
2 |
3 | import pw.janyo.whatanime.model.AnimationHistory
4 |
5 | interface HistoryService {
6 | suspend fun saveHistory(animationHistory: AnimationHistory): Long
7 |
8 | suspend fun getById(historyId: Int): AnimationHistory?
9 |
10 | suspend fun delete(historyId: Int): Int
11 |
12 | suspend fun queryAllHistory(): List
13 |
14 | suspend fun update(animationHistory: AnimationHistory): Int
15 |
16 | suspend fun queryHistoryByOriginPath(originPath: String): AnimationHistory?
17 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/base/AppInfo.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.base
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.graphics.painter.Painter
5 | import org.jetbrains.compose.resources.StringResource
6 |
7 | //设备id
8 | expect fun publicDeviceId(): String
9 |
10 | //应用名称
11 | expect fun appName(): String
12 |
13 | //应用包名
14 | expect fun packageName(): String
15 |
16 | //版本名称
17 | expect fun appVersionName(): String
18 |
19 | //版本号
20 | expect fun appVersionCode(): String
21 | expect fun appVersionCodeNumber(): Long
22 |
23 | expect fun getStoreUrl(): StringResource
24 |
25 | expect fun getStoreTitle(): StringResource
26 |
27 | @Composable
28 | expect fun getStoreIcon(): Painter
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/utils/Util.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.utils
2 |
3 | import coil3.PlatformContext
4 | import multiplatform.network.cmptoast.showToast
5 | import org.jetbrains.compose.resources.getString
6 | import whatanime.composeapp.generated.resources.Res
7 | import whatanime.composeapp.generated.resources.hint_copy_done
8 |
9 | expect fun isOnline(): Boolean
10 |
11 | expect suspend fun copyToClipboard(context: PlatformContext, text: String)
12 |
13 | suspend fun copyToClipboardThenToast(context: PlatformContext, text: String) {
14 | copyToClipboard(context,text)
15 | showToast(getString(Res.string.hint_copy_done))
16 | }
17 |
18 | expect fun showSharePanel(context: PlatformContext, shareText: String)
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/utils/FileUtil.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.utils
2 |
3 | import io.github.vinceglb.filekit.PlatformFile
4 |
5 | private val map = hashMapOf(
6 | "jpeg" to "image/jpeg",
7 | "jpg" to "image/jpeg",
8 | "png" to "image/png",
9 | "gif" to "image/gif",
10 | "bmp" to "image/bmp",
11 | "svg" to "image/svg+xml",
12 | "webp" to "image/webp",
13 | "tiff" to "image/tiff",
14 | "tif" to "image/tiff",
15 | "ico" to "image/vnd.microsoft.icon",
16 | "avif" to "image/avif",
17 | )
18 |
19 | fun getMimeType(extension: String): String? = map[extension]
20 |
21 | expect suspend fun getCacheFile(file: PlatformFile): PlatformFile?
22 |
23 | expect fun getCacheFilePathBySavedCacheFilePath(path: String): String
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/pw/janyo/whatanime/ui/activity/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.activity
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.enableEdgeToEdge
7 | import chaintech.videoplayer.util.PlaybackPreference
8 | import io.github.vinceglb.filekit.FileKit
9 | import io.github.vinceglb.filekit.dialogs.init
10 | import pw.janyo.whatanime.App
11 |
12 | class MainActivity : ComponentActivity() {
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | enableEdgeToEdge()
15 | super.onCreate(savedInstanceState)
16 | FileKit.init(this)
17 | PlaybackPreference.initialize(this)
18 | setContent {
19 | App()
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/module/ViewModelModule.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.module
2 |
3 | import org.koin.core.module.dsl.viewModel
4 | import org.koin.dsl.module
5 | import pw.janyo.whatanime.base.ComposeViewModel
6 | import pw.janyo.whatanime.viewmodel.DetailViewModel
7 | import pw.janyo.whatanime.viewmodel.HistoryViewModel
8 | import pw.janyo.whatanime.viewmodel.MainViewModel
9 | import pw.janyo.whatanime.viewmodel.SettingsViewModel
10 |
11 | val viewModelModule = module {
12 | viewModel {
13 | object : ComposeViewModel() {
14 | init {
15 | launchToInit()
16 | }
17 | }
18 | }
19 | viewModel { MainViewModel() }
20 | viewModel { HistoryViewModel() }
21 | viewModel { DetailViewModel() }
22 | viewModel { SettingsViewModel() }
23 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename": "app-icon-1024.jpg",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | },
9 | {
10 | "appearances" : [
11 | {
12 | "appearance" : "luminosity",
13 | "value" : "dark"
14 | }
15 | ],
16 | "idiom" : "universal",
17 | "platform" : "ios",
18 | "size" : "1024x1024"
19 | },
20 | {
21 | "appearances" : [
22 | {
23 | "appearance" : "luminosity",
24 | "value" : "tinted"
25 | }
26 | ],
27 | "idiom" : "universal",
28 | "platform" : "ios",
29 | "size" : "1024x1024"
30 | }
31 | ],
32 | "info" : {
33 | "author" : "xcode",
34 | "version" : 1
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/signing.gradle:
--------------------------------------------------------------------------------
1 | def kFile = rootProject.file('local.properties')
2 | def props = new Properties()
3 | if (kFile.canRead()) {
4 | props.load(new FileInputStream(kFile))
5 |
6 | if (props != null) {
7 | android.signingConfigs.sign.storeFile file(props['SIGN_KEY_STORE_FILE'])
8 | android.signingConfigs.sign.storePassword props['SIGN_KEY_STORE_PASSWORD']
9 | android.signingConfigs.sign.keyAlias props['SIGN_KEY_ALIAS']
10 | android.signingConfigs.sign.keyPassword props['SIGN_KEY_PASSWORD']
11 | }
12 | } else {
13 | android.signingConfigs.sign.storeFile file(System.getenv('SIGN_KEY_STORE_FILE'))
14 | android.signingConfigs.sign.storePassword System.getenv('SIGN_KEY_STORE_PASSWORD')
15 | android.signingConfigs.sign.keyAlias System.getenv('SIGN_KEY_ALIAS')
16 | android.signingConfigs.sign.keyPassword System.getenv('SIGN_KEY_PASSWORD')
17 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/pw/janyo/whatanime/utils/FileUtil.android.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.utils
2 |
3 | import io.github.vinceglb.filekit.PlatformFile
4 | import io.github.vinceglb.filekit.absolutePath
5 | import pw.janyo.whatanime.context
6 | import java.io.File
7 |
8 | private const val CACHE_IMAGE_FILE_NAME = "cacheImage"
9 |
10 | actual suspend fun getCacheFile(file: PlatformFile): PlatformFile? {
11 | val saveParent = context.getExternalFilesDir(CACHE_IMAGE_FILE_NAME) ?: return null
12 | if (!saveParent.exists())
13 | saveParent.mkdirs()
14 | if (saveParent.isDirectory || saveParent.delete() && saveParent.mkdirs()) {
15 | val md5Name = file.absolutePath().md5()
16 | return PlatformFile(File(saveParent, md5Name))
17 | }
18 | return null
19 | }
20 |
21 | actual fun getCacheFilePathBySavedCacheFilePath(path: String): String = path
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/pw/janyo/whatanime/module/NetworkModule.android.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.module
2 |
3 | import android.webkit.WebSettings
4 | import io.ktor.client.engine.HttpClientEngineConfig
5 | import io.ktor.client.engine.HttpClientEngineFactory
6 | import io.ktor.client.engine.okhttp.OkHttp
7 | import io.ktor.client.engine.okhttp.OkHttpConfig
8 | import pw.janyo.whatanime.context
9 | import java.util.concurrent.TimeUnit
10 |
11 | actual fun httpClientEngine(): HttpClientEngineFactory = OkHttp
12 |
13 | actual fun httpClientEngineConfig(config: HttpClientEngineConfig) {
14 | val okHttpConfig = config as OkHttpConfig
15 | okHttpConfig.config {
16 | connectTimeout(40, TimeUnit.SECONDS)
17 | readTimeout(40, TimeUnit.SECONDS)
18 | }
19 | }
20 |
21 | actual fun userAgent(): String = WebSettings.getDefaultUserAgent(context)
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "WhatAnime"
2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
3 |
4 | pluginManagement {
5 | repositories {
6 | google {
7 | mavenContent {
8 | includeGroupAndSubgroups("androidx")
9 | includeGroupAndSubgroups("com.android")
10 | includeGroupAndSubgroups("com.google")
11 | }
12 | }
13 | mavenCentral()
14 | gradlePluginPortal()
15 | }
16 | }
17 |
18 | dependencyResolutionManagement {
19 | repositories {
20 | google {
21 | mavenContent {
22 | includeGroupAndSubgroups("androidx")
23 | includeGroupAndSubgroups("com.android")
24 | includeGroupAndSubgroups("com.google")
25 | }
26 | }
27 | mavenCentral()
28 | }
29 | }
30 |
31 | include(":composeApp")
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/pw/janyo/whatanime/ApplicationExt.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.provider.Settings
6 |
7 | @SuppressLint("StaticFieldLeak")
8 | internal lateinit var context: Context
9 |
10 | //设备id
11 | val publicDeviceId: String
12 | @SuppressLint("HardwareIds")
13 | get() = Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
14 |
15 | //应用名称
16 | val appName: String
17 | get() = context.getString(R.string.app_name)
18 |
19 | //应用包名
20 | const val packageName: String = BuildConfig.APPLICATION_ID
21 |
22 | //版本名称
23 | const val appVersionName: String = BuildConfig.VERSION_NAME
24 |
25 | //版本号
26 | const val appVersionCode: String = BuildConfig.VERSION_CODE.toString()
27 | const val appVersionCodeNumber: Long = BuildConfig.VERSION_CODE.toLong()
28 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/pw/janyo/whatanime/module/PlatformModule.android.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.module
2 |
3 | import android.content.ClipboardManager
4 | import android.content.Context
5 | import android.net.ConnectivityManager
6 | import androidx.room.RoomDatabase
7 | import androidx.sqlite.driver.AndroidSQLiteDriver
8 | import org.koin.android.ext.koin.androidContext
9 | import org.koin.core.module.Module
10 | import org.koin.dsl.module
11 | import pw.janyo.whatanime.db.AppDatabase
12 |
13 | actual fun platformModule(): Module = module {
14 | single { androidContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
15 | single { androidContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
16 |
17 | single> {
18 | getDatabaseBuilder(get())
19 | .setDriver(AndroidSQLiteDriver())
20 | }
21 | }
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/pw/janyo/whatanime/ui/theme/Theme.ios.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material3.ColorScheme
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.collectAsState
7 | import androidx.compose.runtime.getValue
8 |
9 | @Composable
10 | actual fun getColorScheme(): ColorScheme {
11 | val mode by Theme.nightMode.collectAsState()
12 | val isSystemInDarkTheme = isSystemInDarkTheme()
13 | return when (mode) {
14 | //强制开启夜间模式
15 | NightMode.ON -> DarkColorScheme
16 | //强制关闭夜间模式
17 | NightMode.OFF -> LightColorScheme
18 |
19 | else -> {
20 | if (isSystemInDarkTheme) {
21 | DarkColorScheme
22 | } else {
23 | LightColorScheme
24 | }
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/ui/theme/NightMode.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.theme
2 |
3 | import org.jetbrains.compose.resources.StringResource
4 | import whatanime.composeapp.generated.resources.Res
5 | import whatanime.composeapp.generated.resources.array_night_mode_always_off
6 | import whatanime.composeapp.generated.resources.array_night_mode_always_on
7 | import whatanime.composeapp.generated.resources.array_night_mode_auto
8 | import whatanime.composeapp.generated.resources.array_night_mode_material_you
9 |
10 | enum class NightMode(
11 | val value: Int,
12 | val title: StringResource,
13 | ) {
14 | AUTO(0, Res.string.array_night_mode_auto),
15 | ON(1, Res.string.array_night_mode_always_on),
16 | OFF(2, Res.string.array_night_mode_always_off),
17 | MATERIAL_YOU(3, Res.string.array_night_mode_material_you),
18 | }
19 |
20 | expect fun showNightModeSelectList(): List
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/base/ComposeViewModel.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.base
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import kotlinx.coroutines.launch
6 | import org.jetbrains.compose.resources.StringResource
7 | import org.jetbrains.compose.resources.getString
8 | import org.koin.core.component.KoinComponent
9 | import whatanime.composeapp.generated.resources.Res
10 | import whatanime.composeapp.generated.resources.allStringResources
11 |
12 | abstract class ComposeViewModel : ViewModel(), KoinComponent {
13 | fun launchToInit() {
14 | viewModelScope.launch {
15 | Res.allStringResources.forEach {
16 | stringResourceMap[it.value] = getString(it.value)
17 | }
18 | }
19 | }
20 |
21 | protected fun StringResource.string(): String = stringResourceMap[this]!!
22 |
23 | companion object {
24 | private val stringResourceMap = hashMapOf()
25 | }
26 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/pw/janyo/whatanime/Application.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime
2 |
3 | import android.app.Application
4 | import com.tencent.mmkv.MMKV
5 | import multiplatform.network.cmptoast.AppContext
6 | import org.koin.android.ext.koin.androidContext
7 | import org.koin.android.ext.koin.androidLogger
8 | import org.koin.core.context.startKoin
9 | import org.koin.core.logger.Level
10 | import pw.janyo.whatanime.module.moduleList
11 | import pw.janyo.whatanime.BuildConfig
12 |
13 | class Application : Application() {
14 | override fun onCreate() {
15 | super.onCreate()
16 | context = this
17 | startKoin {
18 | androidLogger(Level.ERROR)
19 | androidContext(this@Application)
20 | modules(moduleList())
21 | }
22 | AppContext.apply { set(applicationContext) }
23 | MMKV.initialize(this)
24 | Configure.lastVersion = BuildConfig.VERSION_CODE
25 | //每次启动都禁用调试模式
26 | Configure.debugMode = BuildConfig.DEBUG
27 | }
28 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/ic_app_store.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/pw/janyo/whatanime/module/DatabaseModule.ios.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.module
2 |
3 | import androidx.room.Room
4 | import androidx.room.RoomDatabase
5 | import kotlinx.cinterop.ExperimentalForeignApi
6 | import platform.Foundation.NSDocumentDirectory
7 | import platform.Foundation.NSFileManager
8 | import platform.Foundation.NSUserDomainMask
9 | import pw.janyo.whatanime.db.AppDatabase
10 |
11 | fun getDatabaseBuilder(): RoomDatabase.Builder {
12 | val dbFilePath = documentDirectory() + "/" + DATABASE_NAME + ".db"
13 | return Room.databaseBuilder(
14 | name = dbFilePath,
15 | )
16 | }
17 |
18 | @OptIn(ExperimentalForeignApi::class)
19 | private fun documentDirectory(): String {
20 | val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
21 | directory = NSDocumentDirectory,
22 | inDomain = NSUserDomainMask,
23 | appropriateForURL = null,
24 | create = false,
25 | error = null,
26 | )
27 | return requireNotNull(documentDirectory?.path)
28 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/pw/janyo/whatanime/Configure.android.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime
2 |
3 | import com.tencent.mmkv.MMKV
4 |
5 | val kv = MMKV.mmkvWithID("configure")
6 |
7 | actual inline fun getConfiguration(key: String, defaultValue: T): T =
8 | when (defaultValue) {
9 | is Boolean -> kv.decodeBool(key, defaultValue)
10 | is Int -> kv.decodeInt(key, defaultValue)
11 | is Long -> kv.decodeLong(key, defaultValue)
12 | is Float -> kv.decodeFloat(key, defaultValue)
13 | is String -> kv.decodeString(key, defaultValue)
14 | else -> throw IllegalArgumentException("Unsupported type")
15 | } as T
16 |
17 | actual inline fun setConfiguration(key: String, value: T) {
18 | when(value){
19 | is Boolean -> kv.encode(key, value)
20 | is Int -> kv.encode(key, value)
21 | is Long -> kv.encode(key, value)
22 | is Float -> kv.encode(key, value)
23 | is String -> kv.encode(key, value)
24 | else -> throw IllegalArgumentException("Unsupported type")
25 | }
26 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/db/dao/HistoryDao.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.db.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.Query
6 | import androidx.room.Update
7 | import pw.janyo.whatanime.model.AnimationHistory
8 |
9 | @Dao
10 | interface HistoryDao {
11 | @Insert
12 | suspend fun saveHistory(animationHistory: AnimationHistory): Long
13 |
14 | @Query("SELECT * FROM tb_animation_history where id = :historyId LIMIT 1")
15 | suspend fun getById(historyId: Int): AnimationHistory?
16 |
17 | @Query("DELETE FROM tb_animation_history where id = :historyId")
18 | suspend fun delete(historyId: Int): Int
19 |
20 | @Query("SELECT * FROM tb_animation_history")
21 | suspend fun queryAllHistory(): List
22 |
23 | @Update
24 | suspend fun update(animationHistory: AnimationHistory): Int
25 |
26 | @Query("SELECT * FROM tb_animation_history WHERE origin_path = :originPath LIMIT 1")
27 | suspend fun queryHistoryByOriginPath(originPath: String): AnimationHistory?
28 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.theme
2 |
3 | import androidx.compose.material3.ColorScheme
4 | import androidx.compose.material3.MaterialExpressiveTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.lightColorScheme
8 | import androidx.compose.runtime.Composable
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import pw.janyo.whatanime.Configure
11 |
12 | val DarkColorScheme = darkColorScheme()
13 |
14 | val LightColorScheme = lightColorScheme()
15 |
16 | @Composable
17 | expect fun getColorScheme(): ColorScheme
18 |
19 | @Composable
20 | fun WhatAnimeTheme(
21 | content: @Composable() () -> Unit
22 | ) {
23 | val colorScheme = getColorScheme()
24 |
25 | MaterialExpressiveTheme(
26 | typography = MaterialTheme.typography,
27 | colorScheme = colorScheme,
28 | content = content,
29 | )
30 | }
31 |
32 | object Theme {
33 | val nightMode = MutableStateFlow(Configure.nightMode)
34 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/model/Animation.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.model
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 | import kotlinx.serialization.json.JsonElement
6 |
7 | @Serializable
8 | data class SearchAnimeResult(
9 | val error: String = "",
10 | val frameCount: Long = 0,
11 | val result: List,
12 | )
13 |
14 | @Serializable
15 | data class SearchAnimeResultItem(
16 | @SerialName("anilist")
17 | val aniList: SearchAniListResult,
18 | @SerialName("filename")
19 | val fileName: String,
20 | val episode: JsonElement? = null,
21 | val from: Double? = 0.0,
22 | val to: Double? = 0.0,
23 | val similarity: Double = 0.0,
24 | val video: String,
25 | val image: String,
26 | )
27 |
28 | @Serializable
29 | data class SearchAniListResult(
30 | val id: Long? = 0,
31 | val idMal: Long? = 0,
32 | val title: AniListTitleResult,
33 | @SerialName("isAdult")
34 | val adult: Boolean = false,
35 | )
36 |
37 | @Serializable
38 | data class AniListTitleResult(
39 | val native: String? = "",
40 | )
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/pw/janyo/whatanime/utils/FileUtil.ios.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.utils
2 |
3 | import io.github.vinceglb.filekit.FileKit
4 | import io.github.vinceglb.filekit.PlatformFile
5 | import io.github.vinceglb.filekit.absolutePath
6 | import io.github.vinceglb.filekit.createDirectories
7 | import io.github.vinceglb.filekit.exists
8 | import io.github.vinceglb.filekit.filesDir
9 | import kotlinx.cinterop.ExperimentalForeignApi
10 |
11 | private const val CACHE_IMAGE_FILE_NAME = "cacheImage"
12 |
13 | @OptIn(ExperimentalForeignApi::class)
14 | actual suspend fun getCacheFile(file: PlatformFile): PlatformFile? {
15 | val dir = PlatformFile(FileKit.filesDir, CACHE_IMAGE_FILE_NAME)
16 | if (!dir.exists()) {
17 | dir.createDirectories()
18 | }
19 | val originalFilePath = file.absolutePath()
20 | val md5Name = originalFilePath.md5()
21 | return PlatformFile(dir, md5Name)
22 | }
23 |
24 | actual fun getCacheFilePathBySavedCacheFilePath(path: String): String {
25 | val dir = PlatformFile(FileKit.filesDir, CACHE_IMAGE_FILE_NAME)
26 | val fileName = path.substringAfterLast("/")
27 | return PlatformFile(dir, fileName).absolutePath()
28 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/db/service/HistoryServiceImpl.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.db.service
2 |
3 | import org.koin.core.component.KoinComponent
4 | import org.koin.core.component.inject
5 | import pw.janyo.whatanime.db.dao.HistoryDao
6 | import pw.janyo.whatanime.model.AnimationHistory
7 |
8 | class HistoryServiceImpl : HistoryService, KoinComponent {
9 | private val historyDao: HistoryDao by inject()
10 |
11 | override suspend fun saveHistory(animationHistory: AnimationHistory): Long =
12 | historyDao.saveHistory(animationHistory)
13 |
14 | override suspend fun getById(historyId: Int): AnimationHistory? = historyDao.getById(historyId)
15 |
16 | override suspend fun delete(historyId: Int): Int =
17 | historyDao.delete(historyId)
18 |
19 | override suspend fun queryAllHistory(): List = historyDao.queryAllHistory()
20 |
21 | override suspend fun update(animationHistory: AnimationHistory): Int =
22 | historyDao.update(animationHistory)
23 |
24 | override suspend fun queryHistoryByOriginPath(originPath: String): AnimationHistory? =
25 | historyDao.queryHistoryByOriginPath(originPath)
26 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/ui/components/DialogState.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.saveable.rememberSaveable
7 | import androidx.compose.runtime.setValue
8 |
9 | class ShowDialogState(
10 | initialValue: Boolean,
11 | ) {
12 | internal var show by mutableStateOf(initialValue)
13 |
14 | val isShowing: Boolean
15 | get() = show
16 |
17 | fun show() {
18 | show = true
19 | }
20 |
21 | fun hide() {
22 | show = false
23 | }
24 |
25 | companion object {
26 | fun Saver() = androidx.compose.runtime.saveable.Saver(
27 | save = { it.show },
28 | restore = { ShowDialogState(it) }
29 | )
30 | }
31 | }
32 |
33 | @Composable
34 | fun rememberShowDialogState(
35 | initialValue: Boolean = false,
36 | ): ShowDialogState {
37 | return rememberSaveable(
38 | saver = ShowDialogState.Saver()
39 | ) {
40 | ShowDialogState(initialValue)
41 | }
42 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/module/MediaModule.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.module
2 |
3 | import chaintech.videoplayer.host.MediaPlayerEvent
4 | import chaintech.videoplayer.host.MediaPlayerHost
5 | import chaintech.videoplayer.model.ScreenResize
6 | import org.koin.dsl.module
7 | import pw.janyo.whatanime.ui.components.PlayerState
8 |
9 | val mediaModule = module {
10 | single {
11 | MediaPlayerHost(
12 | isPaused = true,
13 | isLooping = false,
14 | isFullScreen = false,
15 | initialVideoFitMode = ScreenResize.FIT,
16 | )
17 | }
18 | single {
19 | val mediaPlayerHost = get()
20 | val playerState = PlayerState()
21 | mediaPlayerHost.onEvent = { event ->
22 | when (event) {
23 | is MediaPlayerEvent.PauseChange -> {
24 | playerState.isPaused.value = event.isPaused
25 | }
26 |
27 | MediaPlayerEvent.MediaEnd -> {
28 | playerState.playEnd()
29 | }
30 |
31 | else -> {}
32 | }
33 | }
34 | playerState
35 | }
36 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/ui/preference/Group.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.preference
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.ColumnScope
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import me.zhanghai.compose.preference.PreferenceCategory
10 | import me.zhanghai.compose.preference.ProvidePreferenceTheme
11 | import me.zhanghai.compose.preference.preferenceTheme
12 |
13 | @Composable
14 | fun SettingsGroup(
15 | title: @Composable () -> Unit,
16 | modifier: Modifier = Modifier,
17 | content: @Composable ColumnScope.() -> Unit,
18 | ) {
19 | ProvidePreferenceTheme(
20 | theme = myPreferenceTheme(),
21 | ) {
22 | PreferenceCategory(
23 | title = title,
24 | )
25 | Column(
26 | modifier = modifier.fillMaxWidth(),
27 | content = content,
28 | )
29 | }
30 | }
31 |
32 | @Composable
33 | private fun myPreferenceTheme() = preferenceTheme(
34 | summaryColor = MaterialTheme.colorScheme.outline,
35 | )
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/ic_google_play.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/utils/EncryptUtil.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.utils
2 |
3 | import io.github.vinceglb.filekit.PlatformFile
4 | import io.github.vinceglb.filekit.readBytes
5 | import org.kotlincrypto.hash.md.MD5
6 | import org.kotlincrypto.hash.sha1.SHA1
7 | import org.kotlincrypto.hash.sha2.SHA256
8 | import org.kotlincrypto.hash.sha2.SHA384
9 | import org.kotlincrypto.hash.sha2.SHA512
10 |
11 | @OptIn(ExperimentalStdlibApi::class)
12 | fun String.md5(): String = MD5().digest(encodeToByteArray()).toHexString()
13 |
14 | @OptIn(ExperimentalStdlibApi::class)
15 | fun String.sha1(): String = SHA1().digest(encodeToByteArray()).toHexString()
16 |
17 | @OptIn(ExperimentalStdlibApi::class)
18 | fun String.sha256(): String = SHA256().digest(encodeToByteArray()).toHexString()
19 |
20 | @OptIn(ExperimentalStdlibApi::class)
21 | fun String.sha512(): String = SHA512().digest(encodeToByteArray()).toHexString()
22 |
23 | @OptIn(ExperimentalStdlibApi::class)
24 | fun String.sha384(): String = SHA384().digest(encodeToByteArray()).toHexString()
25 |
26 | @OptIn(ExperimentalStdlibApi::class)
27 | suspend fun PlatformFile.md5(): String {
28 | val digest = MD5()
29 | digest.update(readBytes())
30 | val bytes = digest.digest()
31 | return bytes.toHexString()
32 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/utils/StringUtil.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.utils
2 |
3 | import kotlinx.serialization.json.JsonArray
4 | import kotlinx.serialization.json.JsonElement
5 | import kotlinx.serialization.json.JsonNull
6 | import kotlinx.serialization.json.JsonPrimitive
7 | import kotlin.math.pow
8 | import kotlin.math.roundToLong
9 |
10 | fun formatDecimal(value: Double, digits: Int = 1): String {
11 | val multiplier = 10.0.pow(digits + 1)
12 | val rounded = (value * multiplier).roundToLong() / multiplier
13 | return rounded.toString()
14 | }
15 |
16 | fun formatEpisode(episodeElement: JsonElement?): String? {
17 | return when (episodeElement) {
18 | null, is JsonNull -> null
19 | is JsonPrimitive -> {
20 | if (episodeElement.isString) {
21 | episodeElement.content
22 | } else {
23 | episodeElement.content
24 | }
25 | }
26 | is JsonArray -> {
27 | episodeElement.joinToString(", ") {
28 | if (it is JsonPrimitive) {
29 | it.content
30 | } else {
31 | ""
32 | }
33 | }.ifEmpty { null }
34 | }
35 | else -> null
36 | }
37 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/pw/janyo/whatanime/utils/Util.android.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.utils
2 |
3 | import android.content.ClipData
4 | import android.content.ClipboardManager
5 | import android.content.Intent
6 | import android.net.ConnectivityManager
7 | import coil3.PlatformContext
8 | import org.koin.java.KoinJavaComponent
9 | import pw.janyo.whatanime.appName
10 |
11 | @Suppress("DEPRECATION")
12 | actual fun isOnline(): Boolean {
13 | val connectivityManager =
14 | KoinJavaComponent.get(ConnectivityManager::class.java)
15 | val networkInfo = connectivityManager.activeNetworkInfo
16 | return networkInfo?.isConnected == true
17 | }
18 |
19 | actual suspend fun copyToClipboard(context: PlatformContext, text: String) {
20 | val clipboardManager =
21 | KoinJavaComponent.get(ClipboardManager::class.java)
22 | val clipData = ClipData.newPlainText(appName, text)
23 | clipboardManager.setPrimaryClip(clipData)
24 | }
25 |
26 | actual fun showSharePanel(context: PlatformContext, shareText: String) {
27 | val shareIntent = Intent(Intent.ACTION_SEND).apply {
28 | putExtra(Intent.EXTRA_TEXT, shareText)
29 | type = "text/plain"
30 | }
31 | context.startActivity(Intent.createChooser(shareIntent, ""))
32 | }
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/pw/janyo/whatanime/Configure.ios.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime
2 |
3 | import com.russhwolf.settings.NSUserDefaultsSettings
4 | import com.russhwolf.settings.Settings
5 | import com.russhwolf.settings.set
6 | import platform.Foundation.NSUserDefaults
7 |
8 | val delegate: NSUserDefaults = NSUserDefaults.standardUserDefaults()
9 | val settings: Settings = NSUserDefaultsSettings(delegate)
10 |
11 | actual inline fun getConfiguration(key: String, defaultValue: T): T =
12 | when (defaultValue) {
13 | is Boolean -> settings.getBoolean(key, defaultValue)
14 | is Int -> settings.getInt(key, defaultValue)
15 | is Long -> settings.getLong(key, defaultValue)
16 | is Float -> settings.getFloat(key, defaultValue)
17 | is String -> settings.getString(key, defaultValue)
18 | else -> throw IllegalArgumentException("Unsupported type")
19 | } as T
20 |
21 | actual inline fun setConfiguration(key: String, value: T) {
22 | when(value){
23 | is Boolean -> settings[key] = value
24 | is Int -> settings[key] = value
25 | is Long -> settings[key] = value
26 | is Float -> settings[key] = value
27 | is String -> settings[key] = value
28 | else -> throw IllegalArgumentException("Unsupported type")
29 | }
30 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/ui/components/VideoDialog.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.components
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.height
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.width
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.collectAsState
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.unit.dp
12 | import androidx.compose.ui.window.Dialog
13 | import chaintech.videoplayer.host.MediaPlayerHost
14 | import org.koin.compose.koinInject
15 |
16 | @Composable
17 | fun BuildVideoDialog() {
18 | val playerState = koinInject()
19 | val mediaPlayerHost = koinInject()
20 | val isLoadUrl by playerState.isLoadUrl.collectAsState()
21 | if (!isLoadUrl) {
22 | return
23 | }
24 | Dialog(onDismissRequest = {
25 | mediaPlayerHost.pause()
26 | playerState.release()
27 | }) {
28 | Box(modifier = Modifier.padding(4.dp)) {
29 | PlatformMediaPlayerView(
30 | modifier = Modifier
31 | .width(480.dp)
32 | .height(270.dp),
33 | mediaPlayerHost,
34 | )
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
14 |
15 |
16 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/ui/navigation/Nav.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.navigation
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 | import androidx.navigation.NavController
5 | import androidx.navigation.NavGraphBuilder
6 | import androidx.navigation.compose.composable
7 | import androidx.navigation.toRoute
8 | import kotlinx.serialization.Serializable
9 | import pw.janyo.whatanime.ui.screen.AboutScreen
10 | import pw.janyo.whatanime.ui.screen.DetailScreen
11 | import pw.janyo.whatanime.ui.screen.HistoryScreen
12 | import pw.janyo.whatanime.ui.screen.MainScreen
13 | import pw.janyo.whatanime.ui.screen.SettingsScreen
14 |
15 | val LocalNavController = compositionLocalOf { null }
16 |
17 | @Serializable
18 | object RouteMain
19 |
20 | @Serializable
21 | object RouteAbout
22 |
23 | @Serializable
24 | object RouteHistory
25 |
26 | @Serializable
27 | data class RouteDetail(val historyId: Int, val cachePath: String)
28 |
29 | @Serializable
30 | object RouteSettings
31 |
32 | val Navs: NavGraphBuilder.() -> Unit = {
33 | composable { MainScreen() }
34 | composable { AboutScreen() }
35 | composable { HistoryScreen() }
36 | composable { backStackEntry ->
37 | val detail: RouteDetail = backStackEntry.toRoute()
38 | DetailScreen(detail.historyId, detail.cachePath)
39 | }
40 | composable { SettingsScreen() }
41 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/api/SearchApi.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.api
2 |
3 | import de.jensklingenberg.ktorfit.http.Body
4 | import de.jensklingenberg.ktorfit.http.GET
5 | import de.jensklingenberg.ktorfit.http.Header
6 | import de.jensklingenberg.ktorfit.http.Multipart
7 | import de.jensklingenberg.ktorfit.http.POST
8 | import de.jensklingenberg.ktorfit.http.Tag
9 | import io.ktor.client.request.forms.MultiPartFormDataContent
10 | import pw.janyo.whatanime.Configure
11 | import pw.janyo.whatanime.model.SearchAnimeResult
12 | import pw.janyo.whatanime.model.SearchQuota
13 |
14 | interface SearchApi {
15 | companion object {
16 | const val apiKeyHeader = "x-trace-key"
17 | }
18 |
19 | @Multipart
20 | @POST("search?cutBorders&anilistInfo")
21 | suspend fun search(
22 | @Body map: MultiPartFormDataContent,
23 | @Header(apiKeyHeader) key: String = Configure.apiKey,
24 | @Tag("methodName") methodName: String = "search",
25 | ): SearchAnimeResult
26 |
27 | @Multipart
28 | @POST("search?anilistInfo")
29 | suspend fun searchNoCut(
30 | @Body map: MultiPartFormDataContent,
31 | @Header(apiKeyHeader) key: String = Configure.apiKey,
32 | @Tag("methodName") methodName: String = "searchNoCut",
33 | ): SearchAnimeResult
34 |
35 | @GET("me")
36 | suspend fun getMe(
37 | @Header(apiKeyHeader) key: String = Configure.apiKey,
38 | @Tag("methodName") methodName: String = "getMe",
39 | ): SearchQuota
40 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/ui/components/MediaPlayer.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 | import chaintech.videoplayer.host.MediaPlayerHost
6 | import chaintech.videoplayer.model.VideoPlayerConfig
7 | import chaintech.videoplayer.ui.video.VideoPlayerComposable
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 |
10 | class PlayerState {
11 | val isPaused = MutableStateFlow(false)
12 | val isLoadUrl = MutableStateFlow(false)
13 | val isEnd = MutableStateFlow(false)
14 |
15 | fun loadUrl() {
16 | isLoadUrl.value = true
17 | isEnd.value = false
18 | }
19 |
20 | fun playEnd() {
21 | isEnd.value = true
22 | }
23 |
24 | fun release() {
25 | isPaused.value = false
26 | isLoadUrl.value = false
27 | isEnd.value = false
28 | }
29 | }
30 |
31 | @Composable
32 | fun PlatformMediaPlayerView(modifier: Modifier = Modifier, mediaPlayerHost: MediaPlayerHost) {
33 | VideoPlayerComposable(
34 | modifier = modifier,
35 | playerHost = mediaPlayerHost,
36 | playerConfig = VideoPlayerConfig(
37 | isFullScreenEnabled = false,
38 | isScreenLockEnabled = false,
39 | isSpeedControlEnabled = false,
40 | isZoomEnabled = false,
41 | isScreenResizeEnabled = false,
42 | isGestureVolumeControlEnabled = false,
43 | isFastForwardBackwardEnabled = false,
44 | )
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/ui/theme/Icon.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.theme
2 |
3 | import androidx.compose.material3.Icon
4 | import androidx.compose.material3.LocalContentColor
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.graphics.painter.Painter
9 | import androidx.compose.ui.graphics.vector.ImageVector
10 | import org.jetbrains.compose.resources.painterResource
11 | import whatanime.composeapp.generated.resources.Res
12 | import whatanime.composeapp.generated.resources.ic_github
13 | import whatanime.composeapp.generated.resources.ic_google_play
14 |
15 | object WaIcons {
16 | object Settings {
17 | val github: Painter
18 | @Composable
19 | get() = painterResource(Res.drawable.ic_github)
20 | val googlePlay: Painter
21 | @Composable
22 | get() = painterResource(Res.drawable.ic_google_play)
23 | }
24 | }
25 |
26 | @Composable
27 | fun Icons(
28 | painter: Painter,
29 | modifier: Modifier = Modifier,
30 | tint: Color = LocalContentColor.current,
31 | ) {
32 | Icon(
33 | painter = painter,
34 | contentDescription = null,
35 | modifier = modifier,
36 | tint = tint,
37 | )
38 | }
39 |
40 | @Composable
41 | fun Icons(
42 | imageVector: ImageVector,
43 | modifier: Modifier = Modifier,
44 | tint: Color = LocalContentColor.current,
45 | ) {
46 | Icon(
47 | imageVector = imageVector,
48 | contentDescription = imageVector.name,
49 | modifier = modifier,
50 | tint = tint,
51 | )
52 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/ic_app_icon.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/model/AnimationHistory.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.model
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Entity(tableName = "tb_animation_history")
8 | class AnimationHistory {
9 | @PrimaryKey(autoGenerate = true)
10 | var id = 0
11 |
12 | @ColumnInfo(name = "origin_path")
13 | var originPath: String = ""
14 |
15 | @ColumnInfo(name = "cache_path")
16 | var cachePath: String = ""
17 |
18 | @ColumnInfo(name = "animation_result")
19 | var result: String = ""
20 |
21 | @ColumnInfo(name = "animation_time")
22 | var time: Long = 0L
23 |
24 | @ColumnInfo(name = "animation_title")
25 | var title: String = ""
26 |
27 | @ColumnInfo(name = "animation_anilist_id")
28 | var anilistId: Long = 0L
29 |
30 | @ColumnInfo(name = "animation_episode")
31 | var episode: String = ""
32 |
33 | @ColumnInfo(name = "animation_similarity")
34 | var similarity: Double = 0.0
35 |
36 | fun readonly(): ReadOnlyAnimationHistory {
37 | return ReadOnlyAnimationHistory(
38 | id,
39 | originPath,
40 | cachePath,
41 | result,
42 | time,
43 | title,
44 | anilistId,
45 | episode,
46 | similarity,
47 | )
48 | }
49 | }
50 |
51 | data class ReadOnlyAnimationHistory(
52 | val id: Int,
53 | val originPath: String,
54 | val cachePath: String,
55 | val result: String,
56 | val time: Long,
57 | val title: String,
58 | val anilistId: Long,
59 | val episode: String,
60 | val similarity: Double,
61 | val isOldData: Boolean = episode == "old" || similarity == 0.0
62 | )
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/pw/janyo/whatanime/ui/theme/Theme.android.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.ColorScheme
6 | import androidx.compose.material3.dynamicDarkColorScheme
7 | import androidx.compose.material3.dynamicLightColorScheme
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.collectAsState
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.ui.platform.LocalContext
12 |
13 | @Composable
14 | actual fun getColorScheme(): ColorScheme {
15 | val mode by Theme.nightMode.collectAsState()
16 | val isSystemInDarkTheme = isSystemInDarkTheme()
17 | when (mode) {
18 | NightMode.MATERIAL_YOU -> {
19 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
20 | //满足Material You条件
21 | val context = LocalContext.current
22 | if (isSystemInDarkTheme)
23 | dynamicDarkColorScheme(context)
24 | else
25 | dynamicLightColorScheme(context)
26 | } else {
27 | //不满足Material You条件,降级为自动
28 | if (isSystemInDarkTheme) {
29 | DarkColorScheme
30 | } else {
31 | LightColorScheme
32 | }
33 | }
34 | }
35 |
36 | //强制开启夜间模式
37 | NightMode.ON -> return DarkColorScheme
38 | //强制关闭夜间模式
39 | NightMode.OFF -> return LightColorScheme
40 |
41 | NightMode.AUTO -> {
42 | return if (isSystemInDarkTheme) {
43 | DarkColorScheme
44 | } else {
45 | LightColorScheme
46 | }
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/pw/janyo/whatanime/utils/Util.ios.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.utils
2 |
3 | import coil3.PlatformContext
4 | import multiplatform.network.cmptoast.ToastDuration
5 | import multiplatform.network.cmptoast.showToast
6 | import org.jetbrains.compose.resources.getString
7 | import platform.UIKit.UIActivityViewController
8 | import platform.UIKit.UIApplication
9 | import platform.UIKit.UIPasteboard
10 | import platform.UIKit.popoverPresentationController
11 | import whatanime.composeapp.generated.resources.Res
12 | import whatanime.composeapp.generated.resources.hint_copy_done
13 |
14 | actual fun isOnline(): Boolean = true // TODO: Implement actual network check for iOS
15 |
16 | actual suspend fun copyToClipboard(context: PlatformContext, text: String) {
17 | val pasteboard = UIPasteboard.generalPasteboard
18 | pasteboard.string = text
19 | showToast(getString(Res.string.hint_copy_done))
20 | }
21 |
22 | actual fun showSharePanel(context: PlatformContext, shareText: String) {
23 | val rootViewController = UIApplication.sharedApplication.keyWindow?.rootViewController
24 | if (rootViewController != null) {
25 | val activityItems = listOf(shareText)
26 | val activityViewController = UIActivityViewController(
27 | activityItems = activityItems,
28 | applicationActivities = null,
29 | )
30 | activityViewController.popoverPresentationController?.sourceView = rootViewController.view
31 | rootViewController.presentViewController(
32 | activityViewController,
33 | animated = true,
34 | completion = null
35 | )
36 | } else {
37 | showToast(
38 | "could not find root view controller to present share sheet",
39 | duration = ToastDuration.Long
40 | )
41 | }
42 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/ui/components/ShowProgressDialog.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.height
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.shape.RoundedCornerShape
8 | import androidx.compose.material3.Card
9 | import androidx.compose.material3.LoadingIndicator
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.unit.TextUnit
16 | import androidx.compose.ui.unit.dp
17 | import androidx.compose.ui.window.Dialog
18 | import androidx.compose.ui.window.DialogProperties
19 |
20 | @Composable
21 | fun ShowProgressDialog(
22 | state: ShowDialogState,
23 | text: String,
24 | fontSize: TextUnit = TextUnit.Unspecified,
25 | ) {
26 | if (!state.isShowing) {
27 | return
28 | }
29 | Dialog(
30 | onDismissRequest = { state.hide() },
31 | properties = DialogProperties(
32 | dismissOnBackPress = false,
33 | dismissOnClickOutside = false,
34 | )
35 | ) {
36 | Card(shape = RoundedCornerShape(16.dp)) {
37 | Column(
38 | horizontalAlignment = Alignment.CenterHorizontally,
39 | modifier = Modifier.padding(32.dp)
40 | ) {
41 | LoadingIndicator()
42 | Spacer(modifier = Modifier.height(16.dp))
43 | Text(
44 | text = text, fontSize = fontSize,
45 | color = MaterialTheme.colorScheme.onSurface,
46 | )
47 | }
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/ic_whatanime.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
16 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/Configure.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime
2 |
3 | import pw.janyo.whatanime.model.DebugHttpInfo
4 | import pw.janyo.whatanime.ui.theme.NightMode
5 |
6 | expect inline fun getConfiguration(key: String, defaultValue: T): T
7 | expect inline fun setConfiguration(key: String, value: T)
8 |
9 | object Configure {
10 | var lastVersion: Int
11 | set(value) = setConfiguration("config_last_version", value)
12 | get() = getConfiguration("config_last_version", 0)
13 | var hideSex: Boolean
14 | set(value) = setConfiguration("config_hide_sex", value)
15 | get() = getConfiguration("config_hide_sex", true)
16 | var apiKey: String
17 | set(value) = setConfiguration("config_api_key", value)
18 | get() = getConfiguration("config_api_key", "")
19 | var nightMode: NightMode
20 | set(value) = setConfiguration("nightMode", value.value)
21 | get() {
22 | val value = getConfiguration("nightMode", NightMode.AUTO.value)
23 | return NightMode.entries.first { it.value == value }
24 | }
25 | var preferWebp: Boolean
26 | set(value) = setConfiguration("preferWebp", value)
27 | get() = getConfiguration("preferWebp", false)
28 | var cutBorders: Boolean
29 | set(value) = setConfiguration("cutBorders", value)
30 | get() = getConfiguration("cutBorders", false)
31 | var debugMode: Boolean
32 | set(value) = setConfiguration("debugMode", value)
33 | get() = getConfiguration("debugMode", false)
34 | }
35 |
36 | object Constant {
37 | const val BASE_URL = "https://api.trace.moe/"
38 |
39 | const val WHAT_ANIME_URL = "https://trace.moe/about"
40 | const val JANYO_STUDIO_URL = "https://studio.janyos.top"
41 |
42 | const val DONATE_URL = "https://github.com/sponsors/soruly"
43 | }
44 |
45 | // Global list to store HTTP responses
46 | val httpResponses = mutableListOf()
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/ui/components/NoDataLayout.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.components
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.text.style.TextAlign
16 | import androidx.compose.ui.unit.dp
17 | import org.jetbrains.compose.resources.painterResource
18 | import org.jetbrains.compose.resources.stringResource
19 | import whatanime.composeapp.generated.resources.Res
20 | import whatanime.composeapp.generated.resources.hint_no_result
21 | import whatanime.composeapp.generated.resources.img_no_data
22 |
23 | @Composable
24 | fun NoDataLayout(modifier: Modifier) {
25 | Box(
26 | modifier = modifier
27 | .fillMaxSize()
28 | .background(MaterialTheme.colorScheme.surface),
29 | contentAlignment = Alignment.Center
30 | ) {
31 | Column(
32 | horizontalAlignment = Alignment.CenterHorizontally,
33 | ) {
34 | Image(
35 | painter = painterResource(Res.drawable.img_no_data),
36 | contentDescription = null,
37 | modifier = Modifier.height(256.dp)
38 | )
39 | Text(
40 | text = stringResource(Res.string.hint_no_result),
41 | color = MaterialTheme.colorScheme.onSurface,
42 | textAlign = TextAlign.Center,
43 | modifier = Modifier.padding(48.dp)
44 | )
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: "Build Release"
2 | on:
3 | workflow_dispatch:
4 | env:
5 | OUTPUT_DIR: "composeApp/build/outputs/bundle/release"
6 | SIGN_KEY_ALIAS: ${{ secrets.SIGN_KEY_ALIAS }}
7 | SIGN_KEY_STORE_PASSWORD: ${{ secrets.SIGN_KEY_STORE_PASSWORD }}
8 | SIGN_KEY_PASSWORD: ${{ secrets.SIGN_KEY_PASSWORD }}
9 | SIGN_KEY_STORE_FILE: ${{ github.workspace }}/keystore.jks
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | with:
16 | fetch-depth: 0
17 | - name: 设置JDK
18 | uses: actions/setup-java@v4
19 | with:
20 | java-version: 21
21 | distribution: 'temurin'
22 | - name: 设置Android SDK
23 | uses: android-actions/setup-android@v3
24 | - name: 解密签名
25 | run: |
26 | echo ${{ secrets.SIGN_KEY_BASE64 }} | base64 --decode > ${{ env.SIGN_KEY_STORE_FILE }}
27 | - name: Setup Gradle
28 | uses: gradle/actions/setup-gradle@v5
29 | - name: 构建 APK
30 | run: |
31 | ./gradlew composeApp:exportLibraryDefinitions
32 | ./gradlew bundleRelease
33 | - name: 安装工具
34 | run: |
35 | sudo apt-get install wget -y
36 | - name: 解析版本号
37 | run: |
38 | wget -q -O /tmp/bundletool.jar https://github.com/google/bundletool/releases/download/1.18.2/bundletool-all-1.18.2.jar
39 | fileName=$(ls ${{ env.OUTPUT_DIR }}/ | head -n 1)
40 | versionName=$(java -jar /tmp/bundletool.jar dump manifest --bundle ${{ env.OUTPUT_DIR }}/$fileName --xpath /manifest/@android:versionName)
41 | echo "versionName=$versionName" >> $GITHUB_ENV
42 | echo "fileName=$fileName" >> $GITHUB_ENV
43 | - name: 发布版本
44 | uses: softprops/action-gh-release@v2
45 | with:
46 | tag_name: ${{ env.versionName }}
47 | body_path: CHANGELOG.md
48 | files: |
49 | ${{ env.OUTPUT_DIR }}/${{ env.fileName }}
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/ui/components/AppInfo.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.components
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.layout.size
9 | import androidx.compose.foundation.shape.CircleShape
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.draw.clip
16 | import androidx.compose.ui.layout.ContentScale
17 | import androidx.compose.ui.unit.dp
18 | import org.jetbrains.compose.resources.painterResource
19 | import pw.janyo.whatanime.base.appName
20 | import pw.janyo.whatanime.base.appVersionCode
21 | import pw.janyo.whatanime.base.appVersionName
22 | import whatanime.composeapp.generated.resources.Res
23 | import whatanime.composeapp.generated.resources.ic_app_icon
24 |
25 | @Composable
26 | fun AppInfo() {
27 | Column(
28 | modifier = Modifier
29 | .fillMaxWidth()
30 | .padding(vertical = 32.dp),
31 | verticalArrangement = Arrangement.spacedBy(4.dp),
32 | horizontalAlignment = Alignment.CenterHorizontally,
33 | ) {
34 | Image(
35 | painter = painterResource(Res.drawable.ic_app_icon),
36 | contentDescription = null,
37 | contentScale = ContentScale.Crop,
38 | modifier = Modifier
39 | .size(120.dp)
40 | .clip(CircleShape)
41 | )
42 | Text(text = appName(), color = MaterialTheme.colorScheme.onSurface)
43 | Text(
44 | text = "${appVersionName()}(${appVersionCode()})",
45 | color = MaterialTheme.colorScheme.onSurface
46 | )
47 | }
48 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/ic_github.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/composeApp/src/androidMain/kotlin/pw/janyo/whatanime/base/AppInfo.android.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.base
2 |
3 | import android.annotation.SuppressLint
4 | import android.provider.Settings
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.graphics.painter.Painter
7 | import org.jetbrains.compose.resources.StringResource
8 | import org.jetbrains.compose.resources.painterResource
9 | import pw.janyo.whatanime.BuildConfig
10 | import pw.janyo.whatanime.R
11 | import pw.janyo.whatanime.context
12 | import whatanime.composeapp.generated.resources.Res
13 | import whatanime.composeapp.generated.resources.ic_google_play
14 | import whatanime.composeapp.generated.resources.settings_link_about_google_play
15 | import whatanime.composeapp.generated.resources.settings_title_about_google_play
16 |
17 | //设备id
18 | val publicDeviceId: String
19 | @SuppressLint("HardwareIds")
20 | get() = Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
21 |
22 | //应用名称
23 | val appName: String
24 | get() = context.getString(R.string.app_name)
25 |
26 | //应用包名
27 | const val packageName: String = BuildConfig.APPLICATION_ID
28 |
29 | //版本名称
30 | const val appVersionName: String = BuildConfig.VERSION_NAME
31 |
32 | //版本号
33 | const val appVersionCode: String = BuildConfig.VERSION_CODE.toString()
34 | const val appVersionCodeNumber: Long = BuildConfig.VERSION_CODE.toLong()
35 |
36 | actual fun publicDeviceId(): String = publicDeviceId
37 |
38 | actual fun appName(): String = appName
39 |
40 | actual fun packageName(): String = packageName
41 |
42 | actual fun appVersionName(): String = appVersionName
43 |
44 | actual fun appVersionCode(): String = appVersionCode
45 |
46 | actual fun appVersionCodeNumber(): Long = appVersionCodeNumber
47 |
48 | actual fun getStoreUrl(): StringResource = Res.string.settings_link_about_google_play
49 |
50 | actual fun getStoreTitle(): StringResource = Res.string.settings_title_about_google_play
51 |
52 | @Composable
53 | actual fun getStoreIcon(): Painter = painterResource(Res.drawable.ic_google_play)
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
8 |
11 |
14 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/ui/screen/AboutScreen.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.screen
2 |
3 | import androidx.compose.foundation.layout.fillMaxSize
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
6 | import androidx.compose.material3.IconButton
7 | import androidx.compose.material3.Scaffold
8 | import androidx.compose.material3.Text
9 | import androidx.compose.material3.TopAppBar
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.ui.Modifier
13 | import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
14 | import com.mikepenz.aboutlibraries.ui.compose.produceLibraries
15 | import org.jetbrains.compose.resources.stringResource
16 | import pw.janyo.whatanime.ui.components.AppInfo
17 | import pw.janyo.whatanime.ui.navigation.LocalNavController
18 | import pw.janyo.whatanime.ui.theme.Icons
19 | import whatanime.composeapp.generated.resources.Res
20 | import whatanime.composeapp.generated.resources.app_name
21 |
22 | @Composable
23 | fun AboutScreen() {
24 | val navController = LocalNavController.current!!
25 | Scaffold(
26 | topBar = {
27 | TopAppBar(
28 | title = { Text(text = stringResource(Res.string.app_name)) },
29 | navigationIcon = {
30 | IconButton(onClick = {
31 | navController.popBackStack()
32 | }) {
33 | Icons(Icons.AutoMirrored.Filled.ArrowBack)
34 | }
35 | },
36 | )
37 | },
38 | ) { innerPadding ->
39 | val libraries by produceLibraries {
40 | Res.readBytes("files/aboutlibraries.json").decodeToString()
41 | }
42 | LibrariesContainer(
43 | libraries,
44 | Modifier.fillMaxSize(),
45 | header = {
46 | item {
47 | AppInfo()
48 | }
49 | },
50 | contentPadding = innerPadding,
51 | )
52 | }
53 | }
--------------------------------------------------------------------------------
/composeApp/src/androidMain/res/drawable/ic_launcher_monochrome.xml:
--------------------------------------------------------------------------------
1 |
6 |
8 |
13 |
18 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/viewmodel/HistoryViewModel.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.viewmodel
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import kotlinx.coroutines.flow.MutableStateFlow
5 | import kotlinx.coroutines.flow.StateFlow
6 | import kotlinx.coroutines.launch
7 | import org.jetbrains.compose.resources.getString
8 | import org.koin.core.component.inject
9 | import pw.janyo.whatanime.base.ComposeViewModel
10 | import pw.janyo.whatanime.model.ReadOnlyAnimationHistory
11 | import pw.janyo.whatanime.repository.AnimationRepository
12 | import whatanime.composeapp.generated.resources.Res
13 | import whatanime.composeapp.generated.resources.hint_no_result
14 |
15 | class HistoryViewModel : ComposeViewModel() {
16 | private val animationRepository: AnimationRepository by inject()
17 |
18 | private val _historyListState = MutableStateFlow(HistoryListState())
19 | val historyListState: StateFlow = _historyListState
20 |
21 | fun refresh() {
22 | viewModelScope.launch {
23 | _historyListState.value = _historyListState.value.copy(
24 | loading = true,
25 | errorMessage = "",
26 | )
27 | val list = animationRepository.queryAllHistory()
28 | if (list.isEmpty()) {
29 | _historyListState.value = _historyListState.value.copy(
30 | loading = false,
31 | list = emptyList(),
32 | errorMessage = getString(Res.string.hint_no_result),
33 | )
34 | } else {
35 | _historyListState.value = _historyListState.value.copy(
36 | loading = false,
37 | list = list.map { it.readonly() },
38 | )
39 | }
40 | }
41 | }
42 |
43 | fun deleteHistory(historyId: Int) {
44 | viewModelScope.launch {
45 | _historyListState.value = _historyListState.value.copy(
46 | loading = true,
47 | errorMessage = "",
48 | )
49 | animationRepository.deleteHistory(historyId)
50 | }.invokeOnCompletion {
51 | refresh()
52 | }
53 | }
54 | }
55 |
56 | data class HistoryListState(
57 | val loading: Boolean = false,
58 | val list: List = emptyList(),
59 | val errorMessage: String = "",
60 | )
--------------------------------------------------------------------------------
/composeApp/src/iosMain/kotlin/pw/janyo/whatanime/base/AppInfo.ios.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.base
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.graphics.painter.Painter
5 | import org.jetbrains.compose.resources.StringResource
6 | import org.jetbrains.compose.resources.painterResource
7 | import platform.Foundation.NSBundle
8 | import platform.Foundation.NSUUID
9 | import platform.UIKit.UIDevice
10 | import pw.janyo.whatanime.Configure
11 | import whatanime.composeapp.generated.resources.Res
12 | import whatanime.composeapp.generated.resources.ic_app_store
13 | import whatanime.composeapp.generated.resources.settings_link_about_app_store
14 | import whatanime.composeapp.generated.resources.settings_title_about_app_store
15 | import pw.janyo.whatanime.getConfiguration
16 | import pw.janyo.whatanime.setConfiguration
17 |
18 | private fun getOrCreateDeviceUniqueId(): String {
19 | var uniqueId = getConfiguration("device_unique_id", "")
20 | if (uniqueId == "") {
21 | // 首次安装或数据清除后生成新的ID
22 | // identifierForVendor 是一个很好的选择,因为它在同一厂商的应用间保持一致
23 | uniqueId = UIDevice.currentDevice.identifierForVendor?.UUIDString ?: NSUUID().UUIDString
24 | setConfiguration("device_unique_id", uniqueId)
25 | }
26 | //每次启动都关闭调试模式
27 | Configure.debugMode = false
28 | return uniqueId
29 | }
30 |
31 | //设备id
32 | val publicDeviceId: String = getOrCreateDeviceUniqueId()
33 |
34 | //应用名称
35 | val appName: String =
36 | NSBundle.mainBundle.infoDictionary?.get("CFBundleDisplayName") as? String
37 | ?: NSBundle.mainBundle.infoDictionary?.get("CFBundleName") as? String
38 | ?: "Unknown"
39 |
40 | //应用包名
41 | val packageName: String = NSBundle.mainBundle.bundleIdentifier ?: "Unknown"
42 |
43 | //版本名称
44 | val appVersionName: String by lazy {
45 | NSBundle.mainBundle.infoDictionary?.get("CFBundleShortVersionString") as? String
46 | ?: "Unknown"
47 | }
48 |
49 | //版本号
50 | val appVersionCode: String =
51 | NSBundle.mainBundle.infoDictionary?.get("CFBundleVersion") as? String ?: "Unknown"
52 |
53 | val appVersionCodeNumber: Long
54 | get() = runCatching { appVersionCode.toLong() }.getOrDefault(1L)
55 |
56 | actual fun publicDeviceId(): String = publicDeviceId
57 |
58 | actual fun appName(): String = appName
59 |
60 | actual fun packageName(): String = packageName
61 |
62 | actual fun appVersionName(): String = appVersionName
63 |
64 | actual fun appVersionCode(): String = appVersionCode
65 |
66 | actual fun appVersionCodeNumber(): Long = appVersionCodeNumber
67 |
68 | actual fun getStoreUrl(): StringResource = Res.string.settings_link_about_app_store
69 |
70 | actual fun getStoreTitle(): StringResource = Res.string.settings_title_about_app_store
71 |
72 | @Composable
73 | actual fun getStoreIcon(): Painter = painterResource(Res.drawable.ic_app_store)
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/ui/components/SwipeToDeleteContainer.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.components
2 |
3 | import androidx.compose.animation.animateColorAsState
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.Delete
10 | import androidx.compose.material3.Icon
11 | import androidx.compose.material3.SwipeToDismissBox
12 | import androidx.compose.material3.SwipeToDismissBoxValue
13 | import androidx.compose.material3.rememberSwipeToDismissBoxState
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.LaunchedEffect
16 | import androidx.compose.runtime.getValue
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.unit.dp
21 |
22 | @Composable
23 | fun SwipeToDeleteContainer(
24 | item: T,
25 | onDelete: (T) -> Unit,
26 | content: @Composable (T) -> Unit
27 | ) {
28 | val state = rememberSwipeToDismissBoxState()
29 |
30 | SwipeToDismissBox(
31 | state = state,
32 | backgroundContent = {
33 | val color by animateColorAsState(
34 | when (state.targetValue) {
35 | SwipeToDismissBoxValue.Settled -> Color.Transparent
36 | SwipeToDismissBoxValue.StartToEnd -> Color.Green
37 | SwipeToDismissBoxValue.EndToStart -> Color.Red
38 | }
39 | )
40 | Box(
41 | Modifier
42 | .fillMaxSize()
43 | .background(color)
44 | ) {
45 | when (state.targetValue) {
46 | SwipeToDismissBoxValue.EndToStart -> {
47 | Icon(
48 | modifier = Modifier
49 | .align(Alignment.CenterEnd)
50 | .padding(end = 16.dp),
51 | imageVector = Icons.Default.Delete,
52 | contentDescription = "delete",
53 | tint = Color.White,
54 | )
55 | }
56 |
57 | else -> {
58 | // Nothing to do
59 | }
60 | }
61 | }
62 | },
63 | enableDismissFromStartToEnd = false,
64 | ) { content(item) }
65 |
66 | when (state.currentValue) {
67 | SwipeToDismissBoxValue.EndToStart -> {
68 | LaunchedEffect(state.currentValue) {
69 | onDelete(item)
70 | state.snapTo(SwipeToDismissBoxValue.Settled)
71 | }
72 |
73 | }
74 |
75 | else -> {
76 | // Nothing to do
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/composeApp/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -dontobfuscate
2 | -optimizationpasses 5 # 指定代码的压缩级别
3 | -dontusemixedcaseclassnames # 是否使用大小写混合
4 | -dontskipnonpubliclibraryclasses # 是否混淆第三方jar
5 | -dontpreverify # 混淆时是否做预校验
6 | -verbose # 混淆时是否记录日志
7 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* # 混淆时所采用的算法
8 |
9 | #指定外部模糊字典
10 | -obfuscationdictionary dictionary.txt
11 | #指定class模糊字典
12 | -classobfuscationdictionary dictionary.txt
13 | #指定package模糊字典
14 | -packageobfuscationdictionary dictionary.txt
15 |
16 | # 保护注解
17 | -keepattributes *Annotation*
18 |
19 | # 忽略警告
20 | -ignorewarnings
21 |
22 | -keep public class * extends android.app.Activity # 保持哪些类不被混淆
23 | -keep public class * extends android.app.Application # 保持哪些类不被混淆
24 | -keep public class * extends android.app.Service # 保持哪些类不被混淆
25 | -keep public class * extends android.content.BroadcastReceiver # 保持哪些类不被混淆
26 | -keep public class * extends android.content.ContentProvider # 保持哪些类不被混淆
27 | -keep public class * extends android.app.backup.BackupAgentHelper # 保持哪些类不被混淆
28 | -keep public class * extends android.preference.Preference # 保持哪些类不被混淆
29 |
30 | #记录生成的日志数据,gradle build时在本项目根目录输出
31 |
32 | # 未混淆的类和成员
33 | -printseeds seeds.txt
34 | # 列出从 apk 中删除的代码
35 | -printusage unused.txt
36 | # 混淆前后的映射
37 | -printmapping mapping.txt
38 |
39 | #保持 native 方法不被混淆
40 | -keepclasseswithmembernames class * {
41 | native ;
42 | }
43 |
44 | #保持自定义控件类不被混淆
45 | -keepclasseswithmembers class * {
46 | public (android.content.Context, android.util.AttributeSet);
47 | }
48 |
49 | #保持自定义控件类不被混淆
50 | -keepclassmembers class * extends android.app.Activity {
51 | public void *(android.view.View);
52 | }
53 |
54 | #保持 Parcelable 不被混淆
55 | -keep class * implements android.os.Parcelable {
56 | public static final android.os.Parcelable$Creator *;
57 | }
58 |
59 | #保持 Serializable 不被混淆
60 | -keepnames class * implements java.io.Serializable
61 |
62 | #保持 Serializable 不被混淆并且enum 类也不被混淆
63 | -keepclassmembers class * implements java.io.Serializable {
64 | static final long serialVersionUID;
65 | private static final java.io.ObjectStreamField[] serialPersistentFields;
66 | !static !transient ;
67 | !private ;
68 | !private ;
69 | private void writeObject(java.io.ObjectOutputStream);
70 | private void readObject(java.io.ObjectInputStream);
71 | java.lang.Object writeReplace();
72 | java.lang.Object readResolve();
73 | }
74 |
75 | ##混淆保护自己项目的部分代码以及引用的第三方jar包library(想混淆去掉"#")
76 | -keep class pw.janyo.whatanime.model.** { *; }
77 |
78 | #不混淆资源类
79 | -keepclassmembers class **.R$* {
80 | public static ;
81 | }
82 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/viewmodel/DetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.viewmodel
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import chaintech.videoplayer.host.MediaPlayerHost
5 | import io.github.vinceglb.filekit.PlatformFile
6 | import io.github.vinceglb.filekit.absolutePath
7 | import kotlinx.coroutines.cancel
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 | import kotlinx.coroutines.flow.StateFlow
10 | import kotlinx.coroutines.launch
11 | import org.jetbrains.compose.resources.getString
12 | import org.koin.core.component.inject
13 | import pw.janyo.whatanime.Configure
14 | import pw.janyo.whatanime.base.ComposeViewModel
15 | import pw.janyo.whatanime.model.SearchAnimeResultItem
16 | import pw.janyo.whatanime.repository.AnimationRepository
17 | import pw.janyo.whatanime.ui.components.PlayerState
18 | import whatanime.composeapp.generated.resources.Res
19 | import whatanime.composeapp.generated.resources.hint_no_result
20 | import kotlin.time.Clock
21 |
22 | class DetailViewModel : ComposeViewModel() {
23 | private val animationRepository by inject()
24 | private val mediaPlayerHost by inject()
25 | private val playerState by inject()
26 |
27 | private val _listState = MutableStateFlow(MainListState())
28 | val listState: StateFlow = _listState
29 |
30 | fun loadHistoryDetail(historyId: Int, cacheFile: PlatformFile) {
31 | viewModelScope.launch {
32 | _listState.value = _listState.value.copy(loading = true)
33 | val pair = animationRepository.getByHistoryId(historyId)
34 | val result = pair.first
35 | if (result == null) {
36 | _listState.value = _listState.value.copy(
37 | loading = false,
38 | searchImageFile = cacheFile,
39 | tokenExpired = false,
40 | errorMessage = getString(Res.string.hint_no_result)
41 | )
42 | return@launch
43 | }
44 | val list = if (Configure.hideSex) {
45 | result.result.filter { !it.aniList.adult }
46 | } else {
47 | result.result
48 | }
49 | _listState.value = _listState.value.copy(
50 | loading = false,
51 | searchImageFile = cacheFile,
52 | tokenExpired = pair.second + 1000 * 60 * 10 < Clock.System.now()
53 | .toEpochMilliseconds(),
54 | list = list,
55 | errorMessage = "",
56 | )
57 | }
58 | }
59 |
60 | fun playVideo(result: SearchAnimeResultItem) {
61 | viewModelScope.launch {
62 | val requestUrl = "${result.video}&size=l"
63 | mediaPlayerHost.loadUrl(requestUrl)
64 | playerState.loadUrl()
65 | mediaPlayerHost.play()
66 | }
67 | }
68 |
69 | override fun onCleared() {
70 | viewModelScope.cancel()
71 | playerState.release()
72 | super.onCleared()
73 | }
74 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/App.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime
2 |
3 | import androidx.compose.animation.AnimatedContentTransitionScope
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.animation.fadeIn
6 | import androidx.compose.animation.fadeOut
7 | import androidx.compose.foundation.ComposeFoundationFlags
8 | import androidx.compose.foundation.ExperimentalFoundationApi
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.CompositionLocalProvider
11 | import androidx.navigation.compose.NavHost
12 | import androidx.navigation.compose.rememberNavController
13 | import coil3.ImageLoader
14 | import coil3.compose.setSingletonImageLoaderFactory
15 | import coil3.request.CachePolicy
16 | import io.github.vinceglb.filekit.coil.addPlatformFileSupport
17 | import pw.janyo.whatanime.ui.navigation.LocalNavController
18 | import pw.janyo.whatanime.ui.navigation.Navs
19 | import pw.janyo.whatanime.ui.navigation.RouteMain
20 | import pw.janyo.whatanime.ui.theme.WhatAnimeTheme
21 |
22 | @Composable
23 | fun App() {
24 | setSingletonImageLoaderFactory { context ->
25 | ImageLoader.Builder(context)
26 | .components {
27 | addPlatformFileSupport()
28 | }
29 | .memoryCachePolicy(CachePolicy.DISABLED)
30 | .diskCachePolicy(CachePolicy.DISABLED)
31 | .build()
32 | }
33 | val navController = rememberNavController()
34 | WhatAnimeTheme {
35 | CompositionLocalProvider(LocalNavController provides navController) {
36 | NavHost(
37 | navController = navController,
38 | startDestination = RouteMain,
39 | enterTransition = {
40 | fadeIn(animationSpec = tween(300)) + slideIntoContainer(
41 | AnimatedContentTransitionScope.SlideDirection.Start,
42 | animationSpec = tween(300),
43 | initialOffset = { it / 5 },
44 | )
45 | },
46 | exitTransition = {
47 | fadeOut(animationSpec = tween(300)) + slideOutOfContainer(
48 | AnimatedContentTransitionScope.SlideDirection.Start,
49 | animationSpec = tween(300),
50 | targetOffset = { it / 5 },
51 | )
52 | },
53 | popEnterTransition = {
54 | slideIntoContainer(
55 | AnimatedContentTransitionScope.SlideDirection.End,
56 | animationSpec = tween(300),
57 | initialOffset = { it / 5 },
58 | ) + fadeIn(animationSpec = tween(300))
59 | },
60 | popExitTransition = {
61 | slideOutOfContainer(
62 | AnimatedContentTransitionScope.SlideDirection.End,
63 | animationSpec = tween(300),
64 | targetOffset = { it / 5 },
65 | ) + fadeOut(animationSpec = tween(300))
66 | },
67 | builder = Navs,
68 | )
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/ui/preference/Settings.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.preference
2 |
3 | import androidx.compose.material3.Text
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.text.AnnotatedString
7 | import me.zhanghai.compose.preference.ListPreference
8 | import me.zhanghai.compose.preference.ListPreferenceType
9 | import me.zhanghai.compose.preference.Preference
10 | import me.zhanghai.compose.preference.SwitchPreference
11 | import me.zhanghai.compose.preference.TextFieldPreference
12 |
13 | @Composable
14 | fun CheckboxSetting(
15 | modifier: Modifier = Modifier,
16 | title: String,
17 | subtitle: String? = null,
18 | icon: @Composable () -> Unit = {},
19 | checked: Boolean,
20 | onCheckedChange: (Boolean) -> Unit = {},
21 | ) {
22 | SwitchPreference(
23 | value = checked,
24 | onValueChange = onCheckedChange,
25 | title = {
26 | Text(title)
27 | },
28 | summary = subtitle?.let {
29 | { Text(it) }
30 | },
31 | modifier = modifier,
32 | icon = icon,
33 | )
34 | }
35 |
36 | @Composable
37 | fun ListSetting(
38 | modifier: Modifier = Modifier,
39 | title: String,
40 | subtitle: String? = null,
41 | icon: @Composable () -> Unit = {},
42 | defaultValue: T,
43 | onValueChange: (T) -> Unit = {},
44 | values: List,
45 | valueToText: (T) -> AnnotatedString = { AnnotatedString(it.toString()) },
46 | ) {
47 | ListPreference(
48 | value = defaultValue,
49 | onValueChange = onValueChange,
50 | title = {
51 | Text(title)
52 | },
53 | summary = subtitle?.let {
54 | { Text(it) }
55 | },
56 | modifier = modifier,
57 | icon = icon,
58 | values = values,
59 | valueToText = valueToText,
60 | type = ListPreferenceType.DROPDOWN_MENU,
61 | )
62 | }
63 |
64 | @Composable
65 | fun TextSettings(
66 | modifier: Modifier = Modifier,
67 | title: String,
68 | subtitle: String? = null,
69 | icon: @Composable () -> Unit = {},
70 | defaultValue: String,
71 | onValueChange: (String) -> Unit = {},
72 | ) {
73 | TextFieldPreference(
74 | value = defaultValue,
75 | onValueChange = onValueChange,
76 | title = {
77 | Text(title)
78 | },
79 | summary = subtitle?.let {
80 | { Text(it) }
81 | },
82 | modifier = modifier,
83 | icon = icon,
84 | valueToText = { it },
85 | textToValue = { it },
86 | )
87 | }
88 |
89 | @Composable
90 | fun SettingsMenuLink(
91 | modifier: Modifier = Modifier,
92 | icon: @Composable () -> Unit = {},
93 | title: String,
94 | subtitle: String? = null,
95 | onClick: () -> Unit = {},
96 | ) {
97 | Preference(
98 | title = { Text(text = title) },
99 | summary = subtitle?.let { { Text(text = it) } },
100 | icon = icon,
101 | onClick = onClick,
102 | modifier = modifier,
103 | )
104 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/utils/TimeUtil.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.utils
2 |
3 | import kotlinx.datetime.LocalDateTime
4 | import kotlinx.datetime.number
5 |
6 | enum class TimeUnit(val level: Int, val unit: String, val interval: Int) {
7 | MILLISECOND(0, "毫秒", 1000),
8 | SECOND(1, "秒", 60),
9 | MINUTE(2, "分", 60),
10 | HOUR(3, "小时", 24),
11 | DAY(4, "天", 1)
12 | }
13 |
14 | private fun getTimeUnitByLevel(level: Int): TimeUnit? = when (level) {
15 | 0 -> TimeUnit.MILLISECOND
16 | 1 -> TimeUnit.SECOND
17 | 2 -> TimeUnit.MINUTE
18 | 3 -> TimeUnit.HOUR
19 | 4 -> TimeUnit.DAY
20 | else -> null
21 | }
22 |
23 | fun Long.formatTime(
24 | minTimeUnit: TimeUnit = TimeUnit.MILLISECOND,
25 | maxTimeUnit: TimeUnit = TimeUnit.DAY
26 | ): String {
27 | if (minTimeUnit.level > maxTimeUnit.level) {
28 | //等级不正确,抛出异常
29 | throw NumberFormatException("level error")
30 | }
31 | val ss = 1000
32 | val mi = ss * 60
33 | val hh = mi * 60
34 | val dd = hh * 24
35 |
36 | if (this <= 0) return "0${minTimeUnit.unit}"
37 | if (maxTimeUnit == TimeUnit.MILLISECOND) return "$this${TimeUnit.MILLISECOND.unit}"
38 |
39 | val day = this / dd
40 | val hour = (this - day * dd) / hh
41 | val minute = (this - day * dd - hour * hh) / mi
42 | val second = (this - day * dd - hour * hh - minute * mi) / ss
43 | val milliSecond = this % ss
44 | val array = arrayOf(day, hour, minute, second, milliSecond)
45 | val sb = StringBuilder()
46 | for (index in array.indices) {
47 | val unit = getTimeUnitByLevel(array.size - index - 1)!!
48 | val nextUnit = getTimeUnitByLevel(array.size - index - 2)
49 | if (array[index] > 0) {
50 | if (maxTimeUnit.level < unit.level) {
51 | if (nextUnit != null)
52 | array[index + 1] += array[index] * nextUnit.interval
53 | } else {
54 | sb.append(array[index]).append(unit.unit)
55 | }
56 | }
57 | if (minTimeUnit == unit) {
58 | if (sb.isEmpty()) sb.append(0).append(minTimeUnit.unit)
59 | return sb.toString()
60 | }
61 | }
62 | return sb.toString()
63 | }
64 |
65 | fun LocalDateTime.formatDateTime(): String = formatWithFormatter("yyyy-MM-dd HH:mm:ss")
66 |
67 | private fun LocalDateTime.formatWithFormatter(format: String): String {
68 | return format.replace("yyyy", year.toString())
69 | .replace("MM", month.number.pad2())
70 | .replace("M", month.number.toString())
71 | .replace("dd", day.pad2())
72 | .replace("d", day.toString())
73 | .replace("HH", hour.pad2())
74 | .replace("H", hour.toString())
75 | .replace("hh", if (hour > 12) (hour - 12).pad2() else hour.pad2())
76 | .replace("h", if (hour > 12) (hour - 12).toString() else hour.toString())
77 | .replace("mm", minute.pad2())
78 | .replace("m", minute.toString())
79 | .replace("ss", second.pad2())
80 | .replace("s", second.toString())
81 | .replace("SSS", nanosecond.pad3())
82 | .replace("S", nanosecond.toString())
83 | }
84 |
85 | private fun Int.pad2(): String = toString().padStart(2, '0')
86 | private fun Int.pad3(): String = toString().padStart(2, '0')
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/module/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.module
2 |
3 | import de.jensklingenberg.ktorfit.Ktorfit
4 | import io.ktor.client.HttpClient
5 | import io.ktor.client.engine.HttpClientEngineConfig
6 | import io.ktor.client.engine.HttpClientEngineFactory
7 | import io.ktor.client.plugins.UserAgent
8 | import io.ktor.client.plugins.api.createClientPlugin
9 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
10 | import io.ktor.client.statement.bodyAsText
11 | import io.ktor.serialization.kotlinx.json.json
12 | import kotlinx.datetime.TimeZone
13 | import kotlinx.datetime.toLocalDateTime
14 | import kotlinx.serialization.json.Json
15 | import org.koin.dsl.module
16 | import pw.janyo.whatanime.Configure
17 | import pw.janyo.whatanime.Constant
18 | import pw.janyo.whatanime.api.SearchApi
19 | import pw.janyo.whatanime.api.createSearchApi
20 | import pw.janyo.whatanime.httpResponses
21 | import pw.janyo.whatanime.model.DebugHttpInfo
22 | import pw.janyo.whatanime.utils.formatDateTime
23 | import kotlin.time.Clock
24 |
25 | val networkModule = module {
26 | single {
27 | HttpClient(httpClientEngine()) {
28 | engine { httpClientEngineConfig(this) }
29 | install(ContentNegotiation) {
30 | json(Json {
31 | isLenient = true
32 | ignoreUnknownKeys = true
33 | })
34 | }
35 | install(ApiKeyHeaderPlugin)
36 | install(DebugResponsePlugin)
37 | install(UserAgent) {
38 | agent = userAgent()
39 | }
40 | }
41 | }
42 | single {
43 | Ktorfit.Builder()
44 | .httpClient(get())
45 | .baseUrl(Constant.BASE_URL)
46 | .build()
47 | }
48 | single {
49 | get().createSearchApi()
50 | }
51 | }
52 |
53 | private val ApiKeyHeaderPlugin = createClientPlugin("ApiKeyHeaderPlugin") {
54 | onRequest { request, _ ->
55 | if (request.headers[SearchApi.apiKeyHeader].isNullOrBlank()) {
56 | request.headers.remove(SearchApi.apiKeyHeader)
57 | }
58 | }
59 | }
60 |
61 | private val DebugResponsePlugin = createClientPlugin("DebugResponsePlugin") {
62 | onResponse { resp ->
63 | if (!Configure.debugMode) return@onResponse
64 | val methodName =
65 | resp.call.attributes.allKeys.firstOrNull { it -> it.name == "methodName" }?.let {
66 | resp.call.attributes[it].toString()
67 | } ?: "tag"
68 | val responseBody = resp.bodyAsText().replace(
69 | Regex("\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b"),
70 | "0.0.0.0"
71 | )
72 | val currentDateTime =
73 | Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).formatDateTime()
74 | val responseCode = resp.status.value
75 | httpResponses.add(
76 | DebugHttpInfo(
77 | title = "$methodName --> $responseCode",
78 | datetime = currentDateTime,
79 | response = responseBody
80 | )
81 | )
82 | if (httpResponses.size > 5) {
83 | httpResponses.removeAt(0)
84 | }
85 | }
86 | }
87 |
88 | expect fun httpClientEngine(): HttpClientEngineFactory
89 |
90 | expect fun httpClientEngineConfig(config: HttpClientEngineConfig)
91 |
92 | expect fun userAgent(): String
--------------------------------------------------------------------------------
/.github/workflows/build_android.yaml:
--------------------------------------------------------------------------------
1 | name: "Build Android Commit"
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches:
6 | - dev
7 | env:
8 | OUTPUT_DIR: "composeApp/build/outputs/apk/release"
9 | SIGN_KEY_ALIAS: ${{ secrets.SIGN_KEY_ALIAS || 'a' }}
10 | SIGN_KEY_STORE_PASSWORD: ${{ secrets.SIGN_KEY_STORE_PASSWORD || 'passwd' }}
11 | SIGN_KEY_PASSWORD: ${{ secrets.SIGN_KEY_PASSWORD || 'passwd' }}
12 | SIGN_KEY_STORE_FILE: ${{ github.workspace }}/keystore.jks
13 | NIGHTLY: true
14 | jobs:
15 | build:
16 | runs-on: ubuntu-latest
17 | if: "!contains(github.event.head_commit.message, 'ci skip')"
18 | steps:
19 | - name: Checkout code
20 | uses: actions/checkout@v4
21 | with:
22 | fetch-depth: 0
23 | - name: Set up JDK 21
24 | uses: actions/setup-java@v4
25 | with:
26 | distribution: 'temurin'
27 | java-version: '21'
28 | - name: Set up Android SDK
29 | uses: android-actions/setup-android@v3
30 | - name: Set up Gradle
31 | uses: gradle/actions/setup-gradle@v5
32 | - name: Check for secrets and handle keystore
33 | run: |
34 | if [ -n "${{ secrets.SIGN_KEY_ALIAS }}" ] && \
35 | [ -n "${{ secrets.SIGN_KEY_STORE_PASSWORD }}" ] && \
36 | [ -n "${{ secrets.SIGN_KEY_PASSWORD }}" ] && \
37 | [ -n "${{ secrets.SIGN_KEY_BASE64 }}" ]; then
38 | echo "Signing secrets found. Decoding keystore..."
39 | echo "${{ secrets.SIGN_KEY_BASE64 }}" | base64 --decode > ${{ env.SIGN_KEY_STORE_FILE }}
40 | echo "Keystore decoded successfully."
41 | else
42 | echo "Signing secrets not found. Generating temporary keystore..."
43 | keytool -genkey -v \
44 | -keystore ${{ env.SIGN_KEY_STORE_FILE }} \
45 | -alias ${{ env.SIGN_KEY_ALIAS }} \
46 | -keyalg RSA \
47 | -keysize 2048 \
48 | -validity 10000 \
49 | -storepass ${{ env.SIGN_KEY_STORE_PASSWORD }} \
50 | -keypass ${{ env.SIGN_KEY_PASSWORD }} \
51 | -dname "CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown"
52 | echo "Temporary keystore generated successfully."
53 | fi
54 | shell: bash
55 | - name: Make gradlew executable
56 | run: chmod +x gradlew
57 | - name: Build APK
58 | run: |
59 | ./gradlew composeApp:exportLibraryDefinitions
60 | ./gradlew assembleRelease
61 |
62 | - name: Install jq
63 | run: sudo apt-get install -y jq
64 |
65 | - name: Parse output-metadata.json
66 | id: parse_metadata
67 | run: |
68 | OUTPUT_METADATA_FILE="${{ env.OUTPUT_DIR }}/output-metadata.json"
69 | VERSION_NAME=$(jq -r '.elements[0].versionName' $OUTPUT_METADATA_FILE)
70 | VERSION_CODE=$(jq -r '.elements[0].versionCode' $OUTPUT_METADATA_FILE)
71 | echo "::set-output name=versionName::$VERSION_NAME"
72 | echo "::set-output name=versionCode::$VERSION_CODE"
73 | echo "::set-output name=apkPath::$(jq -r '.elements[0].outputFile' $OUTPUT_METADATA_FILE)"
74 |
75 | - name: Rename APK
76 | id: rename_apk
77 | run: |
78 | APK_PATH="${{ env.OUTPUT_DIR }}/${{ steps.parse_metadata.outputs.apkPath }}"
79 | echo "::set-output name=apkPath::$APK_PATH"
80 |
81 | - name: Get latest commit message
82 | id: get_commit_message
83 | run: echo "::set-output name=message::$(git show -s --format=%s)"
84 |
85 | - name: Create GitHub Release
86 | uses: softprops/action-gh-release@v2
87 | with:
88 | tag_name: ${{ steps.parse_metadata.outputs.versionName }}
89 | prerelease: true
90 | body: ${{ steps.get_commit_message.outputs.message }}
91 | files: ${{ steps.rename_apk.outputs.apkPath }}
92 | env:
93 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
94 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/viewmodel/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.viewmodel
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import kotlinx.coroutines.CoroutineExceptionHandler
5 | import kotlinx.coroutines.flow.MutableStateFlow
6 | import kotlinx.coroutines.flow.StateFlow
7 | import kotlinx.coroutines.launch
8 | import org.koin.core.component.inject
9 | import pw.janyo.whatanime.Configure
10 | import pw.janyo.whatanime.base.ComposeViewModel
11 | import pw.janyo.whatanime.httpResponses
12 | import pw.janyo.whatanime.model.DebugHttpInfo
13 | import pw.janyo.whatanime.model.SearchQuota
14 | import pw.janyo.whatanime.repository.AnimationRepository
15 | import pw.janyo.whatanime.ui.theme.NightMode
16 | import pw.janyo.whatanime.ui.theme.Theme
17 | import whatanime.composeapp.generated.resources.Res
18 | import whatanime.composeapp.generated.resources.hint_unknown_error
19 |
20 | class SettingsViewModel : ComposeViewModel() {
21 | private val animationRepository: AnimationRepository by inject()
22 |
23 | private val _errorMessage = MutableStateFlow("")
24 | val errorMessage: StateFlow = _errorMessage
25 |
26 | private val _hideSex = MutableStateFlow(Configure.hideSex)
27 | val hideSex: StateFlow = _hideSex
28 |
29 | private val _preferWebp = MutableStateFlow(Configure.preferWebp)
30 | val preferWebp: StateFlow = _preferWebp
31 |
32 | private val _debugMode = MutableStateFlow(Configure.debugMode)
33 | val debugMode: StateFlow = _debugMode
34 |
35 | private val _nightMode = MutableStateFlow(Configure.nightMode)
36 | val nightMode: StateFlow = _nightMode
37 |
38 | private val _customApiKey = MutableStateFlow("")
39 | val customApiKey: StateFlow = _customApiKey
40 |
41 | private val _searchQuota = MutableStateFlow(SearchQuota.EMPTY)
42 | val searchQuota: StateFlow = _searchQuota
43 |
44 | private val _httpResponses = MutableStateFlow>(emptyList())
45 | val httpResponsesFlow: StateFlow> = _httpResponses
46 |
47 | fun init() {
48 | viewModelScope.launch {
49 | Theme.nightMode.value = Configure.nightMode
50 | _customApiKey.value = Configure.apiKey
51 | }
52 | if (Configure.debugMode) {
53 | _httpResponses.value = httpResponses.toList()
54 | }
55 | showQuota()
56 | }
57 |
58 | fun setHideSex(hideSex: Boolean) {
59 | viewModelScope.launch {
60 | Configure.hideSex = hideSex
61 | _hideSex.value = hideSex
62 | }
63 | }
64 |
65 | fun setDebugMode(debugMode: Boolean) {
66 | viewModelScope.launch {
67 | Configure.debugMode = debugMode
68 | _debugMode.value = debugMode
69 | if (debugMode) {
70 | _httpResponses.value = httpResponses.toList()
71 | } else {
72 | _httpResponses.value = emptyList()
73 | }
74 | }
75 | }
76 |
77 | fun setPreferWebp(preferWebp: Boolean) {
78 | viewModelScope.launch {
79 | Configure.preferWebp = preferWebp
80 | _preferWebp.value = preferWebp
81 | }
82 | }
83 |
84 | fun setNightMode(nightMode: NightMode) {
85 | viewModelScope.launch {
86 | Configure.nightMode = nightMode
87 | _nightMode.value = nightMode
88 | Theme.nightMode.value = nightMode
89 | }
90 | }
91 |
92 | fun setCustomApiKey(customApiKey: String) {
93 | viewModelScope.launch {
94 | Configure.apiKey = customApiKey
95 | _customApiKey.value = customApiKey
96 | showQuota()
97 | }
98 | }
99 |
100 | private fun showQuota() {
101 | viewModelScope.launch(CoroutineExceptionHandler { _, throwable ->
102 | _errorMessage.value = throwable.message ?: Res.string.hint_unknown_error.string()
103 | }) {
104 | _searchQuota.value = animationRepository.showQuota()
105 | }
106 | }
107 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/module/DatabaseModule.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.module
2 |
3 | import androidx.room.RoomDatabase
4 | import androidx.room.RoomDatabaseConstructor
5 | import androidx.room.migration.Migration
6 | import androidx.sqlite.SQLiteConnection
7 | import androidx.sqlite.execSQL
8 | import org.koin.dsl.module
9 | import pw.janyo.whatanime.db.AppDatabase
10 | import pw.janyo.whatanime.db.service.HistoryService
11 | import pw.janyo.whatanime.db.service.HistoryServiceImpl
12 |
13 | internal const val DATABASE_NAME = "db_what_anime"
14 |
15 | val databaseModule = module {
16 | single {
17 | getRoomDatabase(get())
18 | }
19 | single {
20 | get().getHistoryDao()
21 | }
22 | single {
23 | HistoryServiceImpl()
24 | }
25 | }
26 |
27 | // Room compiler generates the `actual` implementations
28 | @Suppress("NO_ACTUAL_FOR_EXPECT")
29 | expect object AppDatabaseConstructor : RoomDatabaseConstructor {
30 | override fun initialize(): AppDatabase
31 | }
32 |
33 | private fun getRoomDatabase(
34 | builder: RoomDatabase.Builder
35 | ): AppDatabase = builder
36 | .addMigrations(MIGRATION_1_2)
37 | .addMigrations(MIGRATION_2_3)
38 | .addMigrations(MIGRATION_3_4)
39 | .addMigrations(MIGRATION_4_5)
40 | .build()
41 |
42 | private val MIGRATION_1_2: Migration = object : Migration(1, 2) {
43 | override fun migrate(connection: SQLiteConnection) {
44 | connection.execSQL("alter table tb_animation_history rename to _tb_animation_history")
45 | connection.execSQL("CREATE TABLE IF NOT EXISTS `tb_animation_history` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `origin_path` TEXT NOT NULL, `cache_path` TEXT NOT NULL, `animation_result` TEXT NOT NULL, `animation_time` INTEGER NOT NULL, `animation_title` TEXT NOT NULL, `animation_filter` TEXT , `base64` TEXT NOT NULL)")
46 | connection.execSQL("insert into tb_animation_history select *,' ' from _tb_animation_history")
47 | connection.execSQL("drop table _tb_animation_history")
48 | }
49 | }
50 | private val MIGRATION_2_3: Migration = object : Migration(2, 3) {
51 | override fun migrate(connection: SQLiteConnection) {
52 | connection.execSQL("alter table tb_animation_history rename to _tb_animation_history")
53 | connection.execSQL("CREATE TABLE IF NOT EXISTS `tb_animation_history` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `origin_path` TEXT, `cache_path` TEXT, `animation_result` TEXT, `animation_time` INTEGER NOT NULL, `animation_title` TEXT, `animation_filter` TEXT)")
54 | connection.execSQL("insert into tb_animation_history select id, origin_path, cache_path, animation_result, animation_time, animation_title, animation_filter from _tb_animation_history")
55 | connection.execSQL("drop table _tb_animation_history")
56 | }
57 | }
58 | private val MIGRATION_3_4: Migration = object : Migration(3, 4) {
59 | override fun migrate(connection: SQLiteConnection) {
60 | connection.execSQL("alter table tb_animation_history rename to _tb_animation_history")
61 | connection.execSQL("CREATE TABLE IF NOT EXISTS `tb_animation_history` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `origin_path` TEXT NOT NULL, `cache_path` TEXT NOT NULL, `animation_result` TEXT NOT NULL, `animation_time` INTEGER NOT NULL, `animation_title` TEXT NOT NULL, `animation_filter` TEXT)")
62 | connection.execSQL("insert into tb_animation_history select id, origin_path, cache_path, animation_result, animation_time, animation_title, animation_filter from _tb_animation_history")
63 | connection.execSQL("drop table _tb_animation_history")
64 | }
65 | }
66 | private val MIGRATION_4_5: Migration = object : Migration(4, 5) {
67 | override fun migrate(connection: SQLiteConnection) {
68 | connection.execSQL("alter table tb_animation_history rename to _tb_animation_history")
69 | connection.execSQL("CREATE TABLE IF NOT EXISTS `tb_animation_history` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `origin_path` TEXT NOT NULL, `cache_path` TEXT NOT NULL, `animation_result` TEXT NOT NULL, `animation_time` INTEGER NOT NULL, `animation_title` TEXT NOT NULL, `animation_anilist_id` INTEGER NOT NULL, `animation_episode` TEXT NOT NULL, `animation_similarity` REAL NOT NULL)")
70 | connection.execSQL("insert into tb_animation_history select id, origin_path, cache_path, animation_result, animation_time, animation_title, 0, 'old', 0 from _tb_animation_history")
71 | connection.execSQL("drop table _tb_animation_history")
72 | }
73 | }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/drawable/ic_load_failed.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
21 |
24 |
27 |
30 |
33 |
36 |
37 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/ui/screen/DetailScreen.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.ui.screen
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.foundation.lazy.LazyColumn
6 | import androidx.compose.foundation.lazy.items
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
9 | import androidx.compose.material3.CenterAlignedTopAppBar
10 | import androidx.compose.material3.IconButton
11 | import androidx.compose.material3.Scaffold
12 | import androidx.compose.material3.SnackbarHost
13 | import androidx.compose.material3.SnackbarHostState
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.LaunchedEffect
17 | import androidx.compose.runtime.collectAsState
18 | import androidx.compose.runtime.getValue
19 | import androidx.compose.runtime.mutableStateOf
20 | import androidx.compose.runtime.remember
21 | import androidx.compose.runtime.rememberCoroutineScope
22 | import androidx.compose.runtime.saveable.rememberSaveable
23 | import androidx.compose.runtime.setValue
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.platform.LocalUriHandler
26 | import androidx.compose.ui.unit.dp
27 | import coil3.compose.LocalPlatformContext
28 | import io.github.vinceglb.filekit.PlatformFile
29 | import kotlinx.coroutines.launch
30 | import multiplatform.network.cmptoast.showToast
31 | import org.jetbrains.compose.resources.getString
32 | import org.jetbrains.compose.resources.stringResource
33 | import org.koin.compose.viewmodel.koinViewModel
34 | import pw.janyo.whatanime.model.SearchAnimeResultItem
35 | import pw.janyo.whatanime.ui.components.BuildBottomSheet
36 | import pw.janyo.whatanime.ui.components.BuildVideoDialog
37 | import pw.janyo.whatanime.ui.components.SearchResultItem
38 | import pw.janyo.whatanime.ui.navigation.LocalNavController
39 | import pw.janyo.whatanime.ui.theme.Icons
40 | import pw.janyo.whatanime.viewmodel.DetailViewModel
41 | import whatanime.composeapp.generated.resources.Res
42 | import whatanime.composeapp.generated.resources.title_activity_history
43 | import whatanime.composeapp.generated.resources.video_play_hint_410
44 |
45 | @Composable
46 | fun DetailScreen(historyId: Int, cachePath: String) {
47 | val navController = LocalNavController.current!!
48 | val uriHandler = LocalUriHandler.current
49 | val context = LocalPlatformContext.current
50 | val vm = koinViewModel()
51 |
52 | val listState by vm.listState.collectAsState()
53 |
54 | val openBottomSheet = rememberSaveable { mutableStateOf(false) }
55 | var selectedItemForBottomSheet by remember { mutableStateOf(null) }
56 |
57 | val snackbarHostState = remember { SnackbarHostState() }
58 | val scope = rememberCoroutineScope()
59 |
60 | Scaffold(
61 | snackbarHost = { SnackbarHost(snackbarHostState) },
62 | topBar = {
63 | CenterAlignedTopAppBar(
64 | title = { Text(text = stringResource(Res.string.title_activity_history)) },
65 | navigationIcon = {
66 | IconButton(onClick = {
67 | navController.popBackStack()
68 | }) {
69 | Icons(Icons.AutoMirrored.Filled.ArrowBack)
70 | }
71 | },
72 | )
73 | },
74 | ) { innerPadding ->
75 | LazyColumn(
76 | modifier = Modifier.padding(innerPadding),
77 | verticalArrangement = Arrangement.spacedBy(8.dp),
78 | ) {
79 | items(listState.list) { item: SearchAnimeResultItem ->
80 | SearchResultItem(
81 | item,
82 | onClick = {
83 | selectedItemForBottomSheet = item
84 | openBottomSheet.value = true
85 | },
86 | )
87 | }
88 | }
89 | }
90 |
91 | BuildBottomSheet(
92 | uriHandler = uriHandler,
93 | context = context,
94 | openBottomSheet = openBottomSheet,
95 | item = selectedItemForBottomSheet,
96 | onPlayVideo = {
97 | if (listState.tokenExpired) {
98 | scope.launch {
99 | showToast(getString(Res.string.video_play_hint_410))
100 | }
101 | return@BuildBottomSheet
102 | }
103 | vm.playVideo(it)
104 | }
105 | )
106 |
107 | BuildVideoDialog()
108 |
109 | LaunchedEffect(listState) {
110 | if (listState.errorMessage.isNotBlank()) {
111 | snackbarHostState.showSnackbar(listState.errorMessage)
112 | }
113 | }
114 | LaunchedEffect(Unit) {
115 | val cacheFile = PlatformFile(cachePath)
116 | vm.loadHistoryDetail(historyId, cacheFile)
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/values-zh-rCN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | WhatAnime
4 | 酱油工作室
5 | 关于WhatAnime
6 | 设置
7 | 开始搜索
8 | 自动去除黑边
9 | 前往捐赠
10 | 确认
11 | 复制
12 | 取消
13 | 已使用次数:%1$d
14 | 本月总次数:%1$d
15 | 保存时间:
16 | 标题:
17 | 准确度:
18 | 标题:%1$s
19 | 集数:%1$s
20 | 时间:
21 | 准确度:
22 | 无效的参数
23 | 无效的令牌
24 | 文件不存在
25 | 令牌已过期,无法播放
26 | 服务端异常
27 | 未知异常
28 | 向左滑动可以删除历史记录
29 | 确认删除(%1$s)吗?
30 | 请注意,此操作是不可恢复的
31 | 网络连接不可用,请检查网络设置
32 | 无结果
33 | 搜索失败
34 | 未知错误
35 | 设备ID已经复制到剪切板中
36 | 正在搜索中,请稍后……
37 | 点击搜索按钮开始
38 | 已复制到剪切板
39 | 应用设置
40 | 隐藏敏感内容
41 | 使用 WEBP 格式
42 | 优先响应WEBP格式的图片,这会降低图片的传输数据量(注意,如果设备不支持WEBP,请关闭此选项)
43 | 启动调试模式记录HTTP响应数据
44 | 自定义Api Key
45 | 如果您捐赠过WhatAnime,您可以在这里填入获得的ApiKey。请注意:如果您输入的ApiKey是无效的,可能会导致无法正常使用搜索功能。
46 | 已使用的额度
47 | 本月已使用为:%1$d
48 | 总额度
49 | 本月总额度为:%1$d
50 | 关于
51 | Github地址
52 | 开源协议声明
53 | 授权管理
54 | JanYo Studio授权解决方案
55 | 当前应用版本
56 | 设备ID
57 | 关于WhatAnime
58 | WhatAnime API开发者
59 | 最近的HTTP请求
60 | 夜间模式
61 | 自动
62 | 始终开启
63 | 始终关闭
64 | 历史记录
65 | 设置
66 | 历史记录
67 | 临时目录创建失败,请检查权限!
68 | 选择的图片文件不存在!
69 | 文件不存在,请检查!
70 | 请先安装一个浏览器应用
71 | 您想查阅更多关于《%1$s》的信息吗?
72 | 文件无效!
73 | 文件过大,请先手动做一次压缩!
74 | 因为数据结构变更,您已经无法再查阅这条历史记录的详细信息,请删除该记录
75 | 使用WhatAnime进行搜索
76 | 调试模式
77 | 复制标题
78 | 分享标题
79 | 查看Anilist信息
80 | 播放预览视频
81 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/values-zh-rTW/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | WhatAnime
4 | 醬油工作室
5 | 關於WhatAnime
6 | 設置
7 | 開始搜索
8 | 自動去除黑邊
9 | 前往捐贈
10 | 確認
11 | 複製
12 | 取消
13 | 已使用次數:%1$d
14 | 本月總次數:%1$d
15 | 保存時間:
16 | 標題:
17 | 準確度:
18 | 標題:%1$s
19 | 集數:%1$s
20 | 時間:
21 | 準確度:
22 | 無效的參數
23 | 無效的令牌
24 | 文件不存在
25 | 令牌已過期,無法播放
26 | 服務端異常
27 | 未知異常
28 | 向左滑可以刪除歷史紀錄
29 | 確認刪除(%1$s)嗎?
30 | 請注意,此操作是不可恢復的
31 | 網絡連接不可用,請檢查網絡設置
32 | 無結果
33 | 搜索失敗
34 | 未知錯誤
35 | 設備ID已經復制到剪切板中
36 | 正在搜索中,請稍後……
37 | 點擊搜索按鈕開始
38 | 已複製到剪貼簿
39 | 應用設置
40 | 隱藏敏感內容
41 | 使用 WEBP 格式
42 | 優先回應 WEBP 格式的圖片,這會降低圖片的傳輸資料量(注意,如果裝置不支援 WEBP,請關閉此選項)
43 | 啟動調試模式記錄HTTP響應數據
44 | 自定義Api Key
45 | 如果您曾捐贈過WhatAnime,您可以在這裡輸入您獲得的ApiKey。請注意:如果您輸入的ApiKey無效,可能會導致無法正常使用搜尋功能。
46 | 已使用的額度
47 | 本月已使用為:%1$d
48 | 總額度
49 | 本月總額度為:%1$d
50 | 關於
51 | Github地址
52 | 開源協議聲明
53 | 授權管理
54 | JanYo Studio授權解決方案
55 | 當前應用版本
56 | 設備ID
57 | 關於WhatAnime
58 | WhatAnime API開發者
59 | 最近的HTTP請求
60 | 夜間模式
61 | 自動
62 | 始終開啟
63 | 始終關閉
64 | 歷史記錄
65 | 設置
66 | 歷史記錄
67 | 臨時目錄創建失敗,請檢查權限!
68 | 選擇的圖片文件不存在!
69 | 文件不存在,請檢查!
70 | 請先安裝一個瀏覽器應用
71 | 您想查閱更多關於《%1$s》的信息嗎?
72 | 文件無效!
73 | 文件過大,請先手動做一次壓縮!
74 | 因為數據結構變更,您已經無法再查閱這條歷史記錄的詳細信息,請刪除該記錄
75 | 使用WhatAnime進行搜索
76 | 調試模式
77 | 複製標題
78 | 分享標題
79 | 查看 Anilist 資訊
80 | 播放預覽影片
81 |
--------------------------------------------------------------------------------
/composeApp/dictionary.txt:
--------------------------------------------------------------------------------
1 | ʻ
2 | ʼ
3 | ʽ
4 | ʾ
5 | ʿ
6 | ˆ
7 | ˈ
8 | ˉ
9 | ˊ
10 | ˋ
11 | ˎ
12 | ˏ
13 | ˑ
14 | י
15 | ـ
16 | ٴ
17 | ᐧ
18 | ᴵ
19 | ᵎ
20 | ᵔ
21 | ᵢ
22 | ⁱ
23 | ﹳ
24 | ﹶ
25 | ゙
26 | ゙゙
27 | ᐧᐧ
28 | ᴵᴵ
29 | ʻʻ
30 | ʽʽ
31 | ʼʼ
32 | ʿʿ
33 | ʾʾ
34 | ــ
35 | ˆˆ
36 | ˉˉ
37 | ˈˈ
38 | ˋˋ
39 | ˊˊ
40 | ˏˏ
41 | ˎˎ
42 | ˑˑ
43 | ᵔᵔ
44 | יי
45 | ᵎᵎ
46 | ᵢᵢ
47 | ⁱⁱ
48 | ﹳﹳ
49 | ٴٴ
50 | ﹶﹶ
51 | ʻʼ
52 | ʻʽ
53 | ʻʾ
54 | ʻʿ
55 | ʻˆ
56 | ʻˈ
57 | ʻˉ
58 | ʻˊ
59 | ʻˋ
60 | ʻˎ
61 | ʻˏ
62 | ʻˑ
63 | ʻי
64 | ʻـ
65 | ʻٴ
66 | ʻᐧ
67 | ʻᴵ
68 | ʻᵎ
69 | ʻᵔ
70 | ʻᵢ
71 | ʻⁱ
72 | ʻﹳ
73 | ʻﹶ
74 | ʻ゙
75 | ʼʻ
76 | ʼʽ
77 | ʼʾ
78 | ʼʿ
79 | ʼˆ
80 | ʼˈ
81 | ʼˉ
82 | ʼˊ
83 | ʼˋ
84 | ʼˎ
85 | ʼˏ
86 | ʼˑ
87 | ʼי
88 | ʼـ
89 | ʼٴ
90 | ʼᐧ
91 | ʼᴵ
92 | ʼᵎ
93 | ʼᵔ
94 | ʼᵢ
95 | ʼⁱ
96 | ʼﹳ
97 | ʼﹶ
98 | ʼ゙
99 | ʽʻ
100 | ʽʼ
101 | ʽʾ
102 | ʽʿ
103 | ʽˆ
104 | ʽˈ
105 | ʽˉ
106 | ʽˊ
107 | ʽˋ
108 | ʽˎ
109 | ʽˏ
110 | ʽˑ
111 | ʽי
112 | ʽـ
113 | ʽٴ
114 | ʽᐧ
115 | ʽᴵ
116 | ʽᵎ
117 | ʽᵔ
118 | ʽᵢ
119 | ʽⁱ
120 | ʽﹳ
121 | ʽﹶ
122 | ʽ゙
123 | ʾʻ
124 | ʾʼ
125 | ʾʽ
126 | ʾʿ
127 | ʾˆ
128 | ʾˈ
129 | ʾˉ
130 | ʾˊ
131 | ʾˋ
132 | ʾˎ
133 | ʾˏ
134 | ʾˑ
135 | ʾי
136 | ʾـ
137 | ʾٴ
138 | ʾᐧ
139 | ʾᴵ
140 | ʾᵎ
141 | ʾᵔ
142 | ʾᵢ
143 | ʾⁱ
144 | ʾﹳ
145 | ʾﹶ
146 | ʾ゙
147 | ʿʻ
148 | ʿʼ
149 | ʿʽ
150 | ʿʾ
151 | ʿˆ
152 | ʿˈ
153 | ʿˉ
154 | ʿˊ
155 | ʿˋ
156 | ʿˎ
157 | ʿˏ
158 | ʿˑ
159 | ʿי
160 | ʿـ
161 | ʿٴ
162 | ʿᐧ
163 | ʿᴵ
164 | ʿᵎ
165 | ʿᵔ
166 | ʿᵢ
167 | ʿⁱ
168 | ʿﹳ
169 | ʿﹶ
170 | ʿ゙
171 | ˆʻ
172 | ˆʼ
173 | ˆʽ
174 | ˆʾ
175 | ˆʿ
176 | ˆˈ
177 | ˆˉ
178 | ˆˊ
179 | ˆˋ
180 | ˆˎ
181 | ˆˏ
182 | ˆˑ
183 | ˆי
184 | ˆـ
185 | ˆٴ
186 | ˆᐧ
187 | ˆᴵ
188 | ˆᵎ
189 | ˆᵔ
190 | ˆᵢ
191 | ˆⁱ
192 | ˆﹳ
193 | ˆﹶ
194 | ˆ゙
195 | ˈʻ
196 | ˈʼ
197 | ˈʽ
198 | ˈʾ
199 | ˈʿ
200 | ˈˆ
201 | ˈˉ
202 | ˈˊ
203 | ˈˋ
204 | ˈˎ
205 | ˈˏ
206 | ˈˑ
207 | ˈי
208 | ˈـ
209 | ˈٴ
210 | ˈᐧ
211 | ˈᴵ
212 | ˈᵎ
213 | ˈᵔ
214 | ˈᵢ
215 | ˈⁱ
216 | ˈﹳ
217 | ˈﹶ
218 | ˈ゙
219 | ˉʻ
220 | ˉʼ
221 | ˉʽ
222 | ˉʾ
223 | ˉʿ
224 | ˉˆ
225 | ˉˈ
226 | ˉˊ
227 | ˉˋ
228 | ˉˎ
229 | ˉˏ
230 | ˉˑ
231 | ˉי
232 | ˉـ
233 | ˉٴ
234 | ˉᐧ
235 | ˉᴵ
236 | ˉᵎ
237 | ˉᵔ
238 | ˉᵢ
239 | ˉⁱ
240 | ˉﹳ
241 | ˉﹶ
242 | ˉ゙
243 | ˊʻ
244 | ˊʼ
245 | ˊʽ
246 | ˊʾ
247 | ˊʿ
248 | ˊˆ
249 | ˊˈ
250 | ˊˉ
251 | ˊˋ
252 | ˊˎ
253 | ˊˏ
254 | ˊˑ
255 | ˊי
256 | ˊـ
257 | ˊٴ
258 | ˊᐧ
259 | ˊᴵ
260 | ˊᵎ
261 | ˊᵔ
262 | ˊᵢ
263 | ˊⁱ
264 | ˊﹳ
265 | ˊﹶ
266 | ˊ゙
267 | ˋʻ
268 | ˋʼ
269 | ˋʽ
270 | ˋʾ
271 | ˋʿ
272 | ˋˆ
273 | ˋˈ
274 | ˋˉ
275 | ˋˊ
276 | ˋˎ
277 | ˋˏ
278 | ˋˑ
279 | ˋי
280 | ˋـ
281 | ˋٴ
282 | ˋᐧ
283 | ˋᴵ
284 | ˋᵎ
285 | ˋᵔ
286 | ˋᵢ
287 | ˋⁱ
288 | ˋﹳ
289 | ˋﹶ
290 | ˋ゙
291 | ˎʻ
292 | ˎʼ
293 | ˎʽ
294 | ˎʾ
295 | ˎʿ
296 | ˎˆ
297 | ˎˈ
298 | ˎˉ
299 | ˎˊ
300 | ˎˋ
301 | ˎˏ
302 | ˎˑ
303 | ˎי
304 | ˎـ
305 | ˎٴ
306 | ˎᐧ
307 | ˎᴵ
308 | ˎᵎ
309 | ˎᵔ
310 | ˎᵢ
311 | ˎⁱ
312 | ˎﹳ
313 | ˎﹶ
314 | ˎ゙
315 | ˏʻ
316 | ˏʼ
317 | ˏʽ
318 | ˏʾ
319 | ˏʿ
320 | ˏˆ
321 | ˏˈ
322 | ˏˉ
323 | ˏˊ
324 | ˏˋ
325 | ˏˎ
326 | ˏˑ
327 | ˏי
328 | ˏـ
329 | ˏٴ
330 | ˏᐧ
331 | ˏᴵ
332 | ˏᵎ
333 | ˏᵔ
334 | ˏᵢ
335 | ˏⁱ
336 | ˏﹳ
337 | ˏﹶ
338 | ˏ゙
339 | ˑʻ
340 | ˑʼ
341 | ˑʽ
342 | ˑʾ
343 | ˑʿ
344 | ˑˆ
345 | ˑˈ
346 | ˑˉ
347 | ˑˊ
348 | ˑˋ
349 | ˑˎ
350 | ˑˏ
351 | ˑי
352 | ˑـ
353 | ˑٴ
354 | ˑᐧ
355 | ˑᴵ
356 | ˑᵎ
357 | ˑᵔ
358 | ˑᵢ
359 | ˑⁱ
360 | ˑﹳ
361 | ˑﹶ
362 | ˑ゙
363 | יʻ
364 | יʼ
365 | יʽ
366 | יʾ
367 | יʿ
368 | יˆ
369 | יˈ
370 | יˉ
371 | יˊ
372 | יˋ
373 | יˎ
374 | יˏ
375 | יˑ
376 | יـ
377 | יٴ
378 | יᐧ
379 | יᴵ
380 | יᵎ
381 | יᵔ
382 | יᵢ
383 | יⁱ
384 | יﹳ
385 | יﹶ
386 | י゙
387 | ـʻ
388 | ـʼ
389 | ـʽ
390 | ـʾ
391 | ـʿ
392 | ـˆ
393 | ـˈ
394 | ـˉ
395 | ـˊ
396 | ـˋ
397 | ـˎ
398 | ـˏ
399 | ـˑ
400 | ـי
401 | ـٴ
402 | ـᐧ
403 | ـᴵ
404 | ـᵎ
405 | ـᵔ
406 | ـᵢ
407 | ـⁱ
408 | ـﹳ
409 | ـﹶ
410 | ـ゙
411 | ٴʻ
412 | ٴʼ
413 | ٴʽ
414 | ٴʾ
415 | ٴʿ
416 | ٴˆ
417 | ٴˈ
418 | ٴˉ
419 | ٴˊ
420 | ٴˋ
421 | ٴˎ
422 | ٴˏ
423 | ٴˑ
424 | ٴי
425 | ٴـ
426 | ٴᐧ
427 | ٴᴵ
428 | ٴᵎ
429 | ٴᵔ
430 | ٴᵢ
431 | ٴⁱ
432 | ٴﹳ
433 | ٴﹶ
434 | ٴ゙
435 | ᐧʻ
436 | ᐧʼ
437 | ᐧʽ
438 | ᐧʾ
439 | ᐧʿ
440 | ᐧˆ
441 | ᐧˈ
442 | ᐧˉ
443 | ᐧˊ
444 | ᐧˋ
445 | ᐧˎ
446 | ᐧˏ
447 | ᐧˑ
448 | ᐧי
449 | ᐧـ
450 | ᐧٴ
451 | ᐧᴵ
452 | ᐧᵎ
453 | ᐧᵔ
454 | ᐧᵢ
455 | ᐧⁱ
456 | ᐧﹳ
457 | ᐧﹶ
458 | ᐧ゙
459 | ᴵʻ
460 | ᴵʼ
461 | ᴵʽ
462 | ᴵʾ
463 | ᴵʿ
464 | ᴵˆ
465 | ᴵˈ
466 | ᴵˉ
467 | ᴵˊ
468 | ᴵˋ
469 | ᴵˎ
470 | ᴵˏ
471 | ᴵˑ
472 | ᴵי
473 | ᴵـ
474 | ᴵٴ
475 | ᴵᐧ
476 | ᴵᵎ
477 | ᴵᵔ
478 | ᴵᵢ
479 | ᴵⁱ
480 | ᴵﹳ
481 | ᴵﹶ
482 | ᴵ゙
483 | ᵎʻ
484 | ᵎʼ
485 | ᵎʽ
486 | ᵎʾ
487 | ᵎʿ
488 | ᵎˆ
489 | ᵎˈ
490 | ᵎˉ
491 | ᵎˊ
492 | ᵎˋ
493 | ᵎˎ
494 | ᵎˏ
495 | ᵎˑ
496 | ᵎי
497 | ᵎـ
498 | ᵎٴ
499 | ᵎᐧ
500 | ᵎᴵ
501 | ᵎᵔ
502 | ᵎᵢ
503 | ᵎⁱ
504 | ᵎﹳ
505 | ᵎﹶ
506 | ᵎ゙
507 | ᵔʻ
508 | ᵔʼ
509 | ᵔʽ
510 | ᵔʾ
511 | ᵔʿ
512 | ᵔˆ
513 | ᵔˈ
514 | ᵔˉ
515 | ᵔˊ
516 | ᵔˋ
517 | ᵔˎ
518 | ᵔˏ
519 | ᵔˑ
520 | ᵔי
521 | ᵔـ
522 | ᵔٴ
523 | ᵔᐧ
524 | ᵔᴵ
525 | ᵔᵎ
526 | ᵔᵢ
527 | ᵔⁱ
528 | ᵔﹳ
529 | ᵔﹶ
530 | ᵔ゙
531 | ᵢʻ
532 | ᵢʼ
533 | ᵢʽ
534 | ᵢʾ
535 | ᵢʿ
536 | ᵢˆ
537 | ᵢˈ
538 | ᵢˉ
539 | ᵢˊ
540 | ᵢˋ
541 | ᵢˎ
542 | ᵢˏ
543 | ᵢˑ
544 | ᵢי
545 | ᵢـ
546 | ᵢٴ
547 | ᵢᐧ
548 | ᵢᴵ
549 | ᵢᵎ
550 | ᵢᵔ
551 | ᵢⁱ
552 | ᵢﹳ
553 | ᵢﹶ
554 | ᵢ゙
555 | ⁱʻ
556 | ⁱʼ
557 | ⁱʽ
558 | ⁱʾ
559 | ⁱʿ
560 | ⁱˆ
561 | ⁱˈ
562 | ⁱˉ
563 | ⁱˊ
564 | ⁱˋ
565 | ⁱˎ
566 | ⁱˏ
567 | ⁱˑ
568 | ⁱי
569 | ⁱـ
570 | ⁱٴ
571 | ⁱᐧ
572 | ⁱᴵ
573 | ⁱᵎ
574 | ⁱᵔ
575 | ⁱᵢ
576 | ⁱﹳ
577 | ⁱﹶ
578 | ⁱ゙
579 | ﹳʻ
580 | ﹳʼ
581 | ﹳʽ
582 | ﹳʾ
583 | ﹳʿ
584 | ﹳˆ
585 | ﹳˈ
586 | ﹳˉ
587 | ﹳˊ
588 | ﹳˋ
589 | ﹳˎ
590 | ﹳˏ
591 | ﹳˑ
592 | ﹳי
593 | ﹳـ
594 | ﹳٴ
595 | ﹳᐧ
596 | ﹳᴵ
597 | ﹳᵎ
598 | ﹳᵔ
599 | ﹳᵢ
600 | ﹳⁱ
601 | ﹳﹶ
602 | ﹳ゙
603 | ﹶʻ
604 | ﹶʼ
605 | ﹶʽ
606 | ﹶʾ
607 | ﹶʿ
608 | ﹶˆ
609 | ﹶˈ
610 | ﹶˉ
611 | ﹶˊ
612 | ﹶˋ
613 | ﹶˎ
614 | ﹶˏ
615 | ﹶˑ
616 | ﹶי
617 | ﹶـ
618 | ﹶٴ
619 | ﹶᐧ
620 | ﹶᴵ
621 | ﹶᵎ
622 | ﹶᵔ
623 | ﹶᵢ
624 | ﹶⁱ
625 | ﹶﹳ
626 | ﹶ゙
627 | ゙ʻ
628 | ゙ʼ
629 | ゙ʽ
630 | ゙ʾ
631 | ゙ʿ
632 | ゙ˆ
633 | ゙ˈ
634 | ゙ˉ
635 | ゙ˊ
636 | ゙ˋ
637 | ゙ˎ
638 | ゙ˏ
639 | ゙ˑ
640 | ゙י
641 | ゙ـ
642 | ゙ٴ
643 | ゙ᐧ
644 | ゙ᴵ
645 | ゙ᵎ
646 | ゙ᵔ
647 | ゙ᵢ
648 | ゙ⁱ
649 | ゙ﹳ
650 | ゙ﹶ
--------------------------------------------------------------------------------
/.github/workflows/build_ios.yml:
--------------------------------------------------------------------------------
1 | name: "Build iOS Commit"
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches:
6 | - dev
7 | env:
8 | SIGN_KEY_ALIAS: ${{ secrets.SIGN_KEY_ALIAS }}
9 | SIGN_KEY_STORE_PASSWORD: ${{ secrets.SIGN_KEY_STORE_PASSWORD }}
10 | SIGN_KEY_PASSWORD: ${{ secrets.SIGN_KEY_PASSWORD }}
11 | SIGN_KEY_STORE_FILE: "/tmp/key.jks"
12 | XCODE_PROJECT: iosApp
13 | MOBILE_DIRECTORY: iosApp
14 | IPA_NAME: WhatAnime
15 | jobs:
16 | build-ios:
17 | runs-on: macos-latest
18 | if: "!contains(github.event.head_commit.message, 'ci skip')"
19 | steps:
20 | - uses: actions/checkout@v4
21 | with:
22 | fetch-depth: 0
23 | - name: Set up JDK 21
24 | uses: actions/setup-java@v4
25 | with:
26 | distribution: 'temurin'
27 | java-version: '21'
28 | - name: Set up Android SDK
29 | uses: android-actions/setup-android@v3
30 | - name: Set up Gradle
31 | uses: gradle/actions/setup-gradle@v5
32 | - name: Check for secrets and handle keystore
33 | run: |
34 | if [ -n "${{ secrets.SIGN_KEY_ALIAS }}" ] && \
35 | [ -n "${{ secrets.SIGN_KEY_STORE_PASSWORD }}" ] && \
36 | [ -n "${{ secrets.SIGN_KEY_PASSWORD }}" ] && \
37 | [ -n "${{ secrets.SIGN_KEY_BASE64 }}" ]; then
38 | echo "Signing secrets found. Decoding keystore..."
39 | echo "${{ secrets.SIGN_KEY_BASE64 }}" | base64 --decode > ${{ env.SIGN_KEY_STORE_FILE }}
40 | echo "Keystore decoded successfully."
41 | else
42 | echo "Signing secrets not found. Generating temporary keystore..."
43 | keytool -genkey -v \
44 | -keystore ${{ env.SIGN_KEY_STORE_FILE }} \
45 | -alias ${{ env.SIGN_KEY_ALIAS }} \
46 | -keyalg RSA \
47 | -keysize 2048 \
48 | -validity 10000 \
49 | -storepass ${{ env.SIGN_KEY_STORE_PASSWORD }} \
50 | -keypass ${{ env.SIGN_KEY_PASSWORD }} \
51 | -dname "CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown"
52 | echo "Temporary keystore generated successfully."
53 | fi
54 | shell: bash
55 | - name: Make gradlew executable
56 | run: chmod +x gradlew
57 | - name: 更新iOS应用版本号
58 | run: |
59 | ./gradlew composeApp:exportLibraryDefinitions
60 | ./gradlew composeApp:updateAppleBuildVersion
61 | - name: 安装Apple证书
62 | id: profile
63 | env:
64 | IOS_CERTIFICATE: ${{ secrets.IOS_CERTIFICATE }}
65 | IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
66 | IOS_PROVISION_PROFILE: ${{ secrets.IOS_PROVISION_PROFILE }}
67 | IOS_KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
68 | run: |
69 | # create variables
70 | CERTIFICATE_PATH=${{ runner.temp }}/build_certificate.p12
71 | PP_PATH=${{ runner.temp }}/XhuTimetableProfile.mobileprovision
72 | KEYCHAIN_PATH=${{ runner.temp }}/app-signing.keychain-db
73 |
74 | # import certificate and provisioning profile from secrets
75 | echo -n "$IOS_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH
76 | echo -n "$IOS_PROVISION_PROFILE" | base64 --decode -o $PP_PATH
77 | uuid=`grep UUID -A1 -a $PP_PATH | grep -io "[-A-F0-9]\\{36\\}"`
78 | echo "uuid=$uuid" >> $GITHUB_OUTPUT
79 |
80 | # create temporary keychain
81 | security create-keychain -p "$IOS_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
82 | security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
83 | security unlock-keychain -p "$IOS_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
84 |
85 | # import certificate to keychain
86 | security import $CERTIFICATE_PATH -P "$IOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
87 | security list-keychain -d user -s $KEYCHAIN_PATH
88 |
89 | # apply provisioning profile
90 | mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
91 | cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/$uuid.mobileprovision
92 | - name: 缓存Xcode编译数据
93 | uses: actions/cache@v4
94 | with:
95 | path: ~/Library/Developer/Xcode/DerivedData
96 | key: xcode-build
97 | restore-keys: xcode-build
98 | - name: 构建iOS APP
99 | run: |
100 | cd ${{ env.MOBILE_DIRECTORY }}
101 | xcodebuild -scheme ${{ env.XCODE_PROJECT }} archive -archivePath "Actions" -configuration Release -arch arm64 CODE_SIGN_STYLE=Manual PROVISIONING_PROFILE_SPECIFIER="${{ steps.profile.outputs.uuid }}" CODE_SIGN_IDENTITY="Apple Distribution"
102 | - name: 导出ipa
103 | env:
104 | EXPORT_PLIST: ${{ secrets.IOS_EXPORT_PRODUCTION }}
105 | run: |
106 | EXPORT_PLIST_PATH=${{ runner.temp }}/ExportOptions.plist
107 | echo -n "$EXPORT_PLIST" | base64 --decode --output $EXPORT_PLIST_PATH
108 | xcodebuild -exportArchive -archivePath ${{ env.MOBILE_DIRECTORY }}/Actions.xcarchive -exportOptionsPlist $EXPORT_PLIST_PATH -exportPath ${{ runner.temp }}/export
109 | - name: 解密API密钥
110 | env:
111 | API_KEY_BASE64: ${{ secrets.IOS_APPSTORE_API_PRIVATE_KEY }}
112 | run: |
113 | mkdir -p ~/private_keys
114 | echo -n "$API_KEY_BASE64" | base64 --decode --output ~/private_keys/AuthKey_${{ secrets.IOS_APPSTORE_API_KEY_ID }}.p8
115 | - name: 上传到Testflight
116 | run: |
117 | xcrun altool --validate-app -f ${{ runner.temp }}/export/${{ env.IPA_NAME }}.ipa -t ios --apiKey ${{ secrets.IOS_APPSTORE_API_KEY_ID }} --apiIssuer ${{ secrets.IOS_APPSTORE_ISSUER_ID }}
118 | xcrun altool --upload-app -f ${{ runner.temp }}/export/${{ env.IPA_NAME }}.ipa -t ios --apiKey ${{ secrets.IOS_APPSTORE_API_KEY_ID }} --apiIssuer ${{ secrets.IOS_APPSTORE_ISSUER_ID }}
119 | - name: 上传文件
120 | uses: actions/upload-artifact@v4
121 | with:
122 | name: ipa-build
123 | path: ${{ runner.temp }}/export/${{ env.IPA_NAME }}.ipa
124 |
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/repository/AnimationRepository.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.repository
2 |
3 | import co.touchlab.kermit.Logger
4 | import io.github.vinceglb.filekit.PlatformFile
5 | import io.github.vinceglb.filekit.delete
6 | import io.github.vinceglb.filekit.exists
7 | import io.github.vinceglb.filekit.name
8 | import io.github.vinceglb.filekit.readBytes
9 | import io.ktor.client.request.forms.MultiPartFormDataContent
10 | import io.ktor.client.request.forms.formData
11 | import io.ktor.http.Headers
12 | import io.ktor.http.HttpHeaders
13 | import kotlinx.coroutines.Dispatchers
14 | import kotlinx.coroutines.withContext
15 | import kotlinx.serialization.json.Json
16 | import org.jetbrains.compose.resources.getString
17 | import org.koin.core.component.KoinComponent
18 | import org.koin.core.component.inject
19 | import pw.janyo.whatanime.Configure
20 | import pw.janyo.whatanime.api.SearchApi
21 | import pw.janyo.whatanime.db.service.HistoryService
22 | import pw.janyo.whatanime.model.AnimationHistory
23 | import pw.janyo.whatanime.model.SearchAnimeResult
24 | import pw.janyo.whatanime.model.SearchQuota
25 | import pw.janyo.whatanime.utils.formatEpisode
26 | import pw.janyo.whatanime.utils.getCacheFilePathBySavedCacheFilePath
27 | import pw.janyo.whatanime.utils.isOnline
28 | import pw.janyo.whatanime.utils.md5
29 | import whatanime.composeapp.generated.resources.Res
30 | import whatanime.composeapp.generated.resources.hint_no_network
31 | import whatanime.composeapp.generated.resources.hint_no_result
32 | import whatanime.composeapp.generated.resources.hint_search_error
33 | import kotlin.time.Clock
34 |
35 | class AnimationRepository : KoinComponent {
36 | private val searchApi by inject()
37 | private val historyService by inject()
38 |
39 | private suspend fun checkNetwork() {
40 | if (!isOnline()) {
41 | throw RuntimeException(getString(Res.string.hint_no_network))
42 | }
43 | }
44 |
45 | private suspend fun queryAnimationByImageOnline(
46 | file: PlatformFile,
47 | originPath: String,
48 | cachePath: String,
49 | mimeType: String,
50 | ): SearchAnimeResult {
51 | val history = queryByFileMd5(file)
52 | if (history != null) {
53 | return history
54 | }
55 | checkNetwork()
56 | val byteArray = file.readBytes()
57 | val multipart = MultiPartFormDataContent(formData {
58 | append("image", byteArray, Headers.build {
59 | append(HttpHeaders.ContentType, mimeType)
60 | append(HttpHeaders.ContentDisposition, "filename=${file.name}")
61 | })
62 | })
63 | val data = if (Configure.cutBorders) {
64 | searchApi.search(multipart)
65 | } else {
66 | searchApi.searchNoCut(multipart)
67 | }
68 | if (data.error.isNotBlank()) {
69 | Logger.e("http request failed, ${data.error}")
70 | throw RuntimeException(getString(Res.string.hint_search_error))
71 | }
72 | saveHistory(originPath, cachePath, data)
73 | return data
74 | }
75 |
76 | suspend fun showQuota(): SearchQuota {
77 | checkNetwork()
78 | return searchApi.getMe()
79 | }
80 |
81 | suspend fun queryAnimationByImageLocal(
82 | file: PlatformFile,
83 | originPath: String,
84 | cachePath: String,
85 | mimeType: String,
86 | ): SearchAnimeResult {
87 | val animationHistory = historyService.queryHistoryByOriginPath(originPath)
88 | ?: return queryAnimationByImageOnline(file, originPath, cachePath, mimeType)
89 | return Json.decodeFromString(animationHistory.result)
90 | }
91 |
92 | private suspend fun queryByFileMd5(file: PlatformFile): SearchAnimeResult? {
93 | val md5 = file.md5()
94 | //用现有的originPath字段来存储md5
95 | val animationHistory = historyService.queryHistoryByOriginPath(md5) ?: return null
96 | return Json.decodeFromString(animationHistory.result)
97 | }
98 |
99 | suspend fun queryHistoryByOriginPath(originPath: String): AnimationHistory? =
100 | historyService.queryHistoryByOriginPath(originPath)
101 |
102 | suspend fun getByHistoryId(historyId: Int): Pair {
103 | val history = historyService.getById(historyId) ?: return null to 0
104 | val result: SearchAnimeResult? = Json.decodeFromString(history.result)
105 | return result to history.time
106 | }
107 |
108 | private suspend fun saveHistory(
109 | originPath: String,
110 | cachePath: String,
111 | searchAnimeResult: SearchAnimeResult
112 | ) {
113 | val animationHistory = withContext(Dispatchers.Default) {
114 | AnimationHistory().apply {
115 | this.originPath = originPath
116 | this.cachePath = cachePath
117 | this.result = Json.encodeToString(searchAnimeResult)
118 | this.time = Clock.System.now().toEpochMilliseconds()
119 | if (searchAnimeResult.result.isNotEmpty()) {
120 | val result = searchAnimeResult.result[0]
121 | this.title = result.aniList.title.native ?: ""
122 | this.anilistId = result.aniList.id ?: 0
123 | this.episode = formatEpisode(result.episode) ?: ""
124 | this.similarity = result.similarity
125 | } else {
126 | this.title = getString(Res.string.hint_no_result)
127 | }
128 | }
129 | }
130 | historyService.saveHistory(animationHistory)
131 | }
132 |
133 | suspend fun queryAllHistory(): List {
134 | val histories = historyService.queryAllHistory()
135 | //重新组装缓存图片路径,因为iOS沙盒id会变
136 | histories.forEach { history ->
137 | history.cachePath = getCacheFilePathBySavedCacheFilePath(history.cachePath)
138 | }
139 | return histories
140 | }
141 |
142 | suspend fun deleteHistory(historyId: Int) {
143 | val animationHistory = historyService.getById(historyId)
144 | historyService.delete(historyId)
145 | animationHistory?.let {
146 | val cacheFIle = PlatformFile(it.cachePath)
147 | if (cacheFIle.exists()) {
148 | cacheFIle.delete()
149 | }
150 | }
151 | }
152 | }
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.13.1"
3 | kotlin = "2.2.21"
4 | # https://github.com/google/ksp/releases
5 | ksp = "2.2.21-2.0.4"
6 |
7 | app-version = "1.8.8"
8 |
9 | composeMultiplatform = "1.9.3"
10 | aboutlibraries = "13.1.0"
11 | kotlinx-serialization = "1.9.0"
12 |
13 | android-compileSdk = "36"
14 | android-minSdk = "24"
15 | android-targetSdk = "36"
16 | androidx-activity = "1.12.0"
17 | androidx-appcompat = "1.7.1"
18 | androidx-splashscreen = "1.2.0"
19 | androidx-core = "1.17.0"
20 | androidx-lifecycle = "2.9.6"
21 | androidx-navigation = "2.9.1"
22 | androidx-browser = "1.9.0"
23 | material = "1.13.0"
24 | material3 = "1.10.0-alpha05"
25 | material-icons = "1.7.3"
26 | ktorfit = "2.6.4"
27 | ktor = "3.3.2"
28 | koin-bom = "4.1.1"
29 | coil-bom = "3.3.0"
30 | kermit = "2.0.8"
31 | cmptoast = "1.0.71"
32 | filekit = "0.12.0"
33 | mmkv = "2.2.4"
34 | media-player = "1.0.50"
35 | # 不能升级,升级后ios无法编译
36 | room = "2.7.2"
37 | kotlin-crypto-hash = "0.8.0"
38 | compose-preference = "2.1.0"
39 | ios-settings = "1.3.0"
40 |
41 | [libraries]
42 | androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
43 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
44 | androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splashscreen" }
45 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
46 | androidx-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
47 | androidx-navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
48 | androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
49 | androidx-browser = { module = "androidx.browser:browser", version.ref = "androidx-browser" }
50 |
51 | material = { module = "com.google.android.material:material", version.ref = "material" }
52 |
53 | material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" }
54 | material-icon = { module = "org.jetbrains.compose.material:material-icons-core", version.ref = "material-icons" }
55 | material-icon-extended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "material-icons" }
56 |
57 | kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
58 |
59 | ktorfit = { module = "de.jensklingenberg.ktorfit:ktorfit-lib-light", version.ref = "ktorfit" }
60 |
61 | ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
62 | ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
63 | ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
64 | ktor-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
65 | ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
66 |
67 | koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
68 | koin-compose = { module = "io.insert-koin:koin-compose" }
69 | koin-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" }
70 | koin-navigation = { module = "io.insert-koin:koin-compose-viewmodel-navigation" }
71 | koin-android = { module = "io.insert-koin:koin-android" }
72 |
73 | coil-bom = { module = "io.coil-kt.coil3:coil-bom", version.ref = "coil-bom" }
74 | coil-compose = { module = "io.coil-kt.coil3:coil-compose" }
75 | coil-ktor3 = { module = "io.coil-kt.coil3:coil-network-ktor3" }
76 | coil-cache-control = { module = "io.coil-kt.coil3:coil-network-cache-control" }
77 |
78 | kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
79 |
80 | cmptoast = { module = "network.chaintech:cmptoast", version.ref = "cmptoast" }
81 |
82 | filekit-core = { module = "io.github.vinceglb:filekit-core", version.ref = "filekit" }
83 | filekit-coil = { module = "io.github.vinceglb:filekit-coil", version.ref = "filekit" }
84 | filekit-dialogs-compose = { module = "io.github.vinceglb:filekit-dialogs-compose", version.ref = "filekit" }
85 |
86 | mmkv-android = { module = "com.tencent:mmkv", version.ref = "mmkv" }
87 |
88 | aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlibraries" }
89 | aboutlibraries-compose-core = { module = "com.mikepenz:aboutlibraries-compose-core", version.ref = "aboutlibraries" }
90 | aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlibraries" }
91 |
92 | media-player = { module = "network.chaintech:compose-multiplatform-media-player", version.ref = "media-player" }
93 |
94 | androidx-room = { module = "androidx.room:room-runtime", version.ref = "room" }
95 | androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
96 | androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
97 |
98 | kotlin-crypto-hash-bom = { group = "org.kotlincrypto.hash", name = "bom", version.ref = "kotlin-crypto-hash" }
99 | kotlin-crypto-hash-md = { group = "org.kotlincrypto.hash", name = "md" }
100 | kotlin-crypto-hash-sha1 = { group = "org.kotlincrypto.hash", name = "sha1" }
101 | kotlin-crypto-hash-sha2 = { group = "org.kotlincrypto.hash", name = "sha2" }
102 |
103 | compose-preference = { group = "me.zhanghai.compose.preference", name = "preference", version.ref = "compose-preference" }
104 |
105 | ios-settings = { group = "com.russhwolf", name = "multiplatform-settings", version.ref = "ios-settings" }
106 |
107 | [plugins]
108 | androidApplication = { id = "com.android.application", version.ref = "agp" }
109 | androidLibrary = { id = "com.android.library", version.ref = "agp" }
110 | composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
111 | composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
112 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
113 | kotlinSerialize = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
114 | kotlinKsp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
115 | aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" }
116 | ktorfit = { id = "de.jensklingenberg.ktorfit", version.ref = "ktorfit" }
117 | room = { id = "androidx.room", version.ref = "room" }
--------------------------------------------------------------------------------
/composeApp/src/commonMain/kotlin/pw/janyo/whatanime/viewmodel/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package pw.janyo.whatanime.viewmodel
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import chaintech.videoplayer.host.MediaPlayerHost
5 | import co.touchlab.kermit.Logger
6 | import io.github.vinceglb.filekit.PlatformFile
7 | import io.github.vinceglb.filekit.absolutePath
8 | import io.github.vinceglb.filekit.copyTo
9 | import io.github.vinceglb.filekit.delete
10 | import io.github.vinceglb.filekit.exists
11 | import io.github.vinceglb.filekit.extension
12 | import io.github.vinceglb.filekit.size
13 | import kotlinx.coroutines.CoroutineExceptionHandler
14 | import kotlinx.coroutines.cancel
15 | import kotlinx.coroutines.flow.MutableStateFlow
16 | import kotlinx.coroutines.flow.StateFlow
17 | import kotlinx.coroutines.launch
18 | import org.jetbrains.compose.resources.getString
19 | import org.koin.core.component.inject
20 | import pw.janyo.whatanime.Configure
21 | import pw.janyo.whatanime.base.ComposeViewModel
22 | import pw.janyo.whatanime.model.SearchAnimeResultItem
23 | import pw.janyo.whatanime.model.SearchQuota
24 | import pw.janyo.whatanime.repository.AnimationRepository
25 | import pw.janyo.whatanime.ui.components.PlayerState
26 | import pw.janyo.whatanime.utils.getCacheFile
27 | import pw.janyo.whatanime.utils.getMimeType
28 | import whatanime.composeapp.generated.resources.Res
29 | import whatanime.composeapp.generated.resources.hint_cache_make_dir_error
30 | import whatanime.composeapp.generated.resources.hint_file_too_large
31 | import whatanime.composeapp.generated.resources.hint_no_result
32 | import whatanime.composeapp.generated.resources.hint_select_file_path_null
33 | import whatanime.composeapp.generated.resources.hint_unknown_error
34 |
35 | class MainViewModel : ComposeViewModel() {
36 | private val animationRepository by inject()
37 | private val mediaPlayerHost by inject()
38 | private val playerState by inject()
39 |
40 | private val _searchQuota = MutableStateFlow(SearchQuota.EMPTY)
41 | val searchQuota: StateFlow = _searchQuota
42 |
43 | private val _listState = MutableStateFlow(MainListState())
44 | val listState: StateFlow = _listState
45 |
46 | private val _cutBorders = MutableStateFlow(Configure.cutBorders)
47 | val cutBorders: StateFlow = _cutBorders
48 |
49 | fun showQuota() {
50 | viewModelScope.launch(CoroutineExceptionHandler { _, throwable ->
51 | Logger.w("showQuota: failed", throwable)
52 | _searchQuota.value = SearchQuota.EMPTY
53 | }) {
54 | _searchQuota.value = animationRepository.showQuota()
55 | }
56 | }
57 |
58 | fun searchImageFile(imageFile: PlatformFile) {
59 | viewModelScope.launch(CoroutineExceptionHandler { context, throwable ->
60 | Logger.w("searchImageFile: failed", throwable)
61 | _listState.value = _listState.value.copy(
62 | loading = false,
63 | errorMessage = throwable.message ?: Res.string.hint_unknown_error.string()
64 | )
65 | }) {
66 | _listState.value = _listState.value.copy(
67 | loading = true,
68 | searchImageFile = imageFile
69 | )
70 | //开始搜索图片
71 | if (imageFile.size() > 26214400L) {
72 | //大于25M,提示文件过大
73 | _listState.value = _listState.value.copy(
74 | loading = false,
75 | errorMessage = getString(Res.string.hint_file_too_large)
76 | )
77 | return@launch
78 | }
79 | //解析缓存路径,将原图片写一份到缓存目录中,避免原图片被删除
80 | var cachePath =
81 | animationRepository.queryHistoryByOriginPath(imageFile.absolutePath())?.cachePath
82 | if (cachePath == null) {
83 | val cacheFile = getCacheFile(imageFile)
84 | if (cacheFile == null) {
85 | _listState.value = _listState.value.copy(
86 | loading = false,
87 | errorMessage = getString(Res.string.hint_cache_make_dir_error)
88 | )
89 | return@launch
90 | }
91 | if (cacheFile.exists()) {
92 | cacheFile.delete()
93 | }
94 | imageFile.copyTo(cacheFile)
95 | cachePath = cacheFile.absolutePath()
96 | }
97 | val mimeType = getMimeType(imageFile.extension)
98 | ?: throw RuntimeException(getString(Res.string.hint_select_file_path_null))
99 | val animation = animationRepository.queryAnimationByImageLocal(
100 | imageFile, imageFile.absolutePath(), cachePath, mimeType,
101 | )
102 | val result = if (Configure.hideSex) {
103 | animation.result.filter { !it.aniList.adult }
104 | } else {
105 | animation.result
106 | }
107 | if (result.isEmpty()) {
108 | _listState.value = _listState.value.copy(
109 | loading = false,
110 | searchImageFile = PlatformFile(cachePath),
111 | tokenExpired = false,
112 | errorMessage = getString(Res.string.hint_no_result)
113 | )
114 | return@launch
115 | }
116 | _listState.value = _listState.value.copy(
117 | loading = false,
118 | searchImageFile = PlatformFile(cachePath),
119 | tokenExpired = false,
120 | list = result,
121 | errorMessage = "",
122 | )
123 | }
124 | }
125 |
126 | fun playVideo(result: SearchAnimeResultItem) {
127 | viewModelScope.launch {
128 | val requestUrl: String = if (result.video.contains("?")) {
129 | "${result.video}&size=l"
130 | } else {
131 | "${result.video}?size=l"
132 | }
133 | mediaPlayerHost.loadUrl(requestUrl)
134 | playerState.loadUrl()
135 | mediaPlayerHost.play()
136 | }
137 | }
138 |
139 | fun changeCutBorders() {
140 | viewModelScope.launch {
141 | Configure.cutBorders = !_cutBorders.value
142 | _cutBorders.value = !_cutBorders.value
143 | }
144 | }
145 |
146 | override fun onCleared() {
147 | viewModelScope.cancel()
148 | playerState.release()
149 | super.onCleared()
150 | }
151 | }
152 |
153 | data class MainListState(
154 | val loading: Boolean = false,
155 | val searchImageFile: PlatformFile? = null,
156 | val tokenExpired: Boolean = false,
157 | val list: List = emptyList(),
158 | val errorMessage: String = "",
159 | )
--------------------------------------------------------------------------------
/composeApp/src/commonMain/composeResources/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | WhatAnime
4 |
5 | JanYo Studio
6 | WhatAnime
7 | History
8 | Settings
9 | Start Search
10 | Cut Borders
11 | Go to Donate
12 | Ok
13 | Copy
14 | Cancel
15 | Copy Title
16 | Share Title
17 | View AniList
18 | Play Preview Video
19 |
20 | Quota Used: %1$d
21 | Quota Total: %1$d
22 |
23 | Save time:
24 | Title:
25 | AniList ID:
26 | Similarity:
27 |
28 | Title: %1$d
29 | Episode: %1$d
30 | Time:
31 | AniList ID:
32 | MyAnimeList ID:
33 | Similarity:
34 |
35 | Invalid url param
36 | Invalid token
37 | File not found
38 | Token expired, can`t play
39 | Server error
40 | Unknown Error
41 |
42 | Swipe item to delete
43 | Delete the record of (%1$d)?
44 | Please note that this action is irreversible.
45 | The network connection is not available, please check the network
46 | No results
47 | Search failed
48 | Unknown error
49 | Device ID has been copied to clipboard
50 | Searching…
51 | Click the search button to start your search
52 | Please install a browser first
53 | Do you want to view more details about %1$s ?
54 | Because of a change in the data structure, you can no longer view information about the record, please delete the record
55 | The selected image file doesn't exist!
56 | Cache directory create failed! Please check the permission!
57 | The path of file is null, please check the file!
58 | The file is invalid!
59 | The file is too large, please perform a manual compression!
60 | Copied to clipboard
61 |
62 | Application Settings
63 | Hide Sex Content
64 | #NSFW
65 | Prefer WEBP image
66 | Prioritize the delivery of images in the WEBP format to reduce the data transmission volume of images (please disable this option if your device does not support WEBP)
67 | Debug mode
68 | Enable debug mode to log HTTP responses
69 | Custom Api Key
70 | If you've donated to whatAnime, you can fill in the ApiKey you got here. Note that if the ApiKey you enter is invalid, you will not be able to use the search feature.
71 | Quota Used
72 | Usage this month: %1$d
73 | Quota Total
74 | Total for the month: %1$d
75 | About
76 | Github
77 | https://github.com/JanYoStudio/WhatAnime
78 | https://github.com/JanYoStudio/WhatAnime
79 | Open Source License
80 | Apache License 2.0
81 | https://github.com/JanYoStudio/WhatAnime/blob/dev/LICENSE
82 | Google Play Store
83 | https://play.google.com/store/apps/details?id=pw.janyo.whatanime
84 | https://play.google.com/store/apps/details?id=pw.janyo.whatanime
85 | App Store
86 | https://apps.apple.com/us/app/whatanime/id6748246489
87 | https://apps.apple.com/us/app/whatanime/id6748246489
88 | License
89 | JanYo Studio License
90 | https://github.com/JanYoStudio/WhatAnime/blob/dev/README.md
91 | Version
92 | Device ID
93 | About WhatAnime
94 | soruly
95 | WhatAnime API Developer
96 | https://about.me/soruly
97 | WAIT: What Anime Is This? - Anime Scene Search Engine
98 | GNU General Public License v3.0
99 | https://trace.moe/
100 | Recent HTTP Responses
101 | Night Mode
102 | Auto
103 | Always On
104 | Always Off
105 | Material You
106 |
107 | WhatAnime
108 | History
109 | Settings
110 | Search by WhatAnime
111 |
--------------------------------------------------------------------------------
/composeApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
3 |
4 | plugins {
5 | alias(libs.plugins.kotlinMultiplatform)
6 | alias(libs.plugins.androidApplication)
7 | alias(libs.plugins.composeCompiler)
8 | alias(libs.plugins.composeMultiplatform)
9 | alias(libs.plugins.kotlinSerialize)
10 | alias(libs.plugins.kotlinKsp)
11 | alias(libs.plugins.aboutLibraries)
12 | alias(libs.plugins.ktorfit)
13 | alias(libs.plugins.room)
14 | }
15 |
16 | room {
17 | schemaDirectory("$projectDir/schemas")
18 | }
19 |
20 | fun String.runCommand(workingDir: File = file("./")): String {
21 | val parts = this.split("\\s".toRegex())
22 | val proc = ProcessBuilder(*parts.toTypedArray())
23 | .directory(workingDir)
24 | .redirectOutput(ProcessBuilder.Redirect.PIPE)
25 | .redirectError(ProcessBuilder.Redirect.PIPE)
26 | .start()
27 |
28 | proc.waitFor(1, TimeUnit.MINUTES)
29 | return proc.inputStream.bufferedReader().readText().trim()
30 | }
31 |
32 | val gitVersionCode: Int = "git rev-list HEAD --count".runCommand().toInt()
33 | val gitVersionName = "git rev-parse --short=8 HEAD".runCommand()
34 | val appVersionName = libs.versions.app.version.get()
35 |
36 | kotlin {
37 | androidTarget {
38 | @OptIn(ExperimentalKotlinGradlePluginApi::class)
39 | compilerOptions {
40 | jvmTarget.set(JvmTarget.JVM_21)
41 | }
42 | }
43 |
44 | compilerOptions {
45 | optIn.add("kotlin.time.ExperimentalTime")
46 | optIn.add("androidx.compose.material3.ExperimentalMaterial3Api")
47 | optIn.add("androidx.compose.material3.ExperimentalMaterial3ExpressiveApi")
48 |
49 | freeCompilerArgs.add("-Xexpect-actual-classes")
50 | }
51 |
52 | listOf(
53 | iosX64(),
54 | iosArm64(),
55 | iosSimulatorArm64()
56 | ).forEach { iosTarget ->
57 | iosTarget.binaries.framework {
58 | baseName = "ComposeApp"
59 | isStatic = true
60 | linkerOpts.add("-lsqlite3")
61 | }
62 | }
63 |
64 | sourceSets {
65 | androidMain.dependencies {
66 | implementation(libs.androidx.core.ktx)
67 | implementation(libs.androidx.activity.compose)
68 | implementation(libs.androidx.appcompat)
69 | implementation(libs.androidx.splashscreen)
70 | implementation(libs.androidx.browser)
71 | implementation(libs.material)
72 | //ktor
73 | implementation(libs.ktor.client.okhttp)
74 | //koin
75 | implementation(libs.koin.android)
76 | //room
77 | implementation(libs.androidx.room.ktx)
78 | //mmkv
79 | implementation(libs.mmkv.android)
80 | }
81 | commonMain.dependencies {
82 | implementation(compose.runtime)
83 | implementation(compose.foundation)
84 | implementation(compose.ui)
85 | implementation(compose.components.resources)
86 | implementation(libs.material3)
87 | implementation(libs.androidx.lifecycle.runtimeCompose)
88 | //common-viewmodel
89 | implementation(libs.androidx.lifecycle.viewmodel)
90 | //common-navigation
91 | implementation(libs.androidx.navigation)
92 | //material-icons
93 | implementation(libs.material.icon)
94 | implementation(libs.material.icon.extended)
95 | //kotlinx-serialization
96 | implementation(libs.kotlinx.serialization)
97 | //ktorfit
98 | implementation(libs.ktorfit)
99 | //ktor
100 | implementation(libs.ktor.client.core)
101 | implementation(libs.ktor.content.negotiation)
102 | implementation(libs.ktor.serialization.json)
103 | //koin
104 | implementation(project.dependencies.platform(libs.koin.bom))
105 | implementation(libs.koin.compose)
106 | implementation(libs.koin.viewmodel)
107 | implementation(libs.koin.navigation)
108 | //coil
109 | implementation(project.dependencies.platform(libs.coil.bom))
110 | implementation(libs.coil.compose)
111 | implementation(libs.coil.ktor3)
112 | implementation(libs.coil.cache.control)
113 | //kermit
114 | implementation(libs.kermit)
115 | //cmptoast
116 | implementation(libs.cmptoast)
117 | //filekit
118 | implementation(libs.filekit.core)
119 | implementation(libs.filekit.coil)
120 | implementation(libs.filekit.dialogs.compose)
121 | //aboutlibraries
122 | implementation(libs.aboutlibraries.core)
123 | implementation(libs.aboutlibraries.compose.core)
124 | implementation(libs.aboutlibraries.compose.m3)
125 | //room
126 | implementation(libs.androidx.room)
127 | //preference
128 | implementation(libs.compose.preference)
129 | //kotlin-crypto-hash
130 | implementation(project.dependencies.platform(libs.kotlin.crypto.hash.bom))
131 | implementation(libs.kotlin.crypto.hash.md)
132 | implementation(libs.kotlin.crypto.hash.sha1)
133 | implementation(libs.kotlin.crypto.hash.sha2)
134 | //media-player
135 | implementation(libs.media.player)
136 | }
137 | iosMain.dependencies {
138 | //ktor
139 | implementation(libs.ktor.client.darwin)
140 | implementation(libs.ios.settings)
141 | }
142 | }
143 | }
144 |
145 | android {
146 | namespace = "pw.janyo.whatanime"
147 | compileSdk = libs.versions.android.compileSdk.get().toInt()
148 |
149 | defaultConfig {
150 | applicationId = "pw.janyo.whatanime"
151 | minSdk = libs.versions.android.minSdk.get().toInt()
152 | targetSdk = libs.versions.android.targetSdk.get().toInt()
153 | versionCode = gitVersionCode
154 | versionName = appVersionName
155 |
156 | setProperty("archivesBaseName", "WhatAnime-$versionName")
157 | }
158 | packaging {
159 | resources {
160 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
161 | }
162 | }
163 | signingConfigs {
164 | create("sign")
165 | }
166 | buildTypes {
167 | debug {
168 | applicationIdSuffix = ".debug"
169 | resValue("color", "ic_launcher_background", "#FFEB3B")
170 | isMinifyEnabled = false
171 | proguardFiles(
172 | getDefaultProguardFile("proguard-android-optimize.txt"),
173 | "proguard-rules.pro"
174 | )
175 | versionNameSuffix = ".d$gitVersionCode.$gitVersionName"
176 | }
177 | release {
178 | val nightly = System.getenv("NIGHTLY")?.toBoolean() == true
179 | isMinifyEnabled = true
180 | proguardFiles(
181 | getDefaultProguardFile("proguard-android-optimize.txt"),
182 | "proguard-rules.pro"
183 | )
184 | if (nightly) {
185 | versionNameSuffix = ".n$gitVersionCode.nightly"
186 | } else {
187 | versionNameSuffix = ".r$gitVersionCode.$gitVersionName"
188 | }
189 | signingConfig = signingConfigs.getByName("sign")
190 | }
191 | }
192 | compileOptions {
193 | sourceCompatibility = JavaVersion.VERSION_21
194 | targetCompatibility = JavaVersion.VERSION_21
195 | }
196 | buildFeatures {
197 | buildConfig = true
198 | }
199 | @Suppress("UnstableApiUsage")
200 | androidResources {
201 | localeFilters.add("en")
202 | localeFilters.add("zh-rCN")
203 | localeFilters.add("zh-rTW")
204 | }
205 | }
206 |
207 | dependencies {
208 | add("kspAndroid", libs.androidx.room.compiler)
209 | add("kspIosSimulatorArm64", libs.androidx.room.compiler)
210 | add("kspIosX64", libs.androidx.room.compiler)
211 | add("kspIosArm64", libs.androidx.room.compiler)
212 | }
213 |
214 | aboutLibraries {
215 | offlineMode = true
216 | collect {
217 | fetchRemoteLicense = false
218 | fetchRemoteFunding = false
219 | }
220 | export {
221 | outputFile = file("src/commonMain/composeResources/files/aboutlibraries.json")
222 | }
223 | }
224 |
225 | tasks.register("updateAppleBuildVersion") {
226 | doLast {
227 | val configTemplate = rootProject.file("iosApp/Configuration/Config.xcconfig.template")
228 | val config = rootProject.file("iosApp/Configuration/Config.xcconfig")
229 | val content = configTemplate.readText()
230 | val newContent = content
231 | .replace("{appVersionName}", appVersionName)
232 | .replace("{gitVersionCode}", gitVersionCode.toString())
233 | config.writeText(newContent)
234 | println("Updated Config.xcconfig with version $appVersionName (Build $gitVersionCode)")
235 | }
236 | }
237 |
238 | apply(from = rootProject.file("signing.gradle"))
239 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
90 | ' "$PWD" ) || exit
91 |
92 | # Use the maximum available, or set MAX_FD != -1 to use that value.
93 | MAX_FD=maximum
94 |
95 | warn () {
96 | echo "$*"
97 | } >&2
98 |
99 | die () {
100 | echo
101 | echo "$*"
102 | echo
103 | exit 1
104 | } >&2
105 |
106 | # OS specific support (must be 'true' or 'false').
107 | cygwin=false
108 | msys=false
109 | darwin=false
110 | nonstop=false
111 | case "$( uname )" in #(
112 | CYGWIN* ) cygwin=true ;; #(
113 | Darwin* ) darwin=true ;; #(
114 | MSYS* | MINGW* ) msys=true ;; #(
115 | NONSTOP* ) nonstop=true ;;
116 | esac
117 |
118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
119 |
120 |
121 | # Determine the Java command to use to start the JVM.
122 | if [ -n "$JAVA_HOME" ] ; then
123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
124 | # IBM's JDK on AIX uses strange locations for the executables
125 | JAVACMD=$JAVA_HOME/jre/sh/java
126 | else
127 | JAVACMD=$JAVA_HOME/bin/java
128 | fi
129 | if [ ! -x "$JAVACMD" ] ; then
130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
131 |
132 | Please set the JAVA_HOME variable in your environment to match the
133 | location of your Java installation."
134 | fi
135 | else
136 | JAVACMD=java
137 | if ! command -v java >/dev/null 2>&1
138 | then
139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
140 |
141 | Please set the JAVA_HOME variable in your environment to match the
142 | location of your Java installation."
143 | fi
144 | fi
145 |
146 | # Increase the maximum file descriptors if we can.
147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
148 | case $MAX_FD in #(
149 | max*)
150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
151 | # shellcheck disable=SC2039,SC3045
152 | MAX_FD=$( ulimit -H -n ) ||
153 | warn "Could not query maximum file descriptor limit"
154 | esac
155 | case $MAX_FD in #(
156 | '' | soft) :;; #(
157 | *)
158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
159 | # shellcheck disable=SC2039,SC3045
160 | ulimit -n "$MAX_FD" ||
161 | warn "Could not set maximum file descriptor limit to $MAX_FD"
162 | esac
163 | fi
164 |
165 | # Collect all arguments for the java command, stacking in reverse order:
166 | # * args from the command line
167 | # * the main class name
168 | # * -classpath
169 | # * -D...appname settings
170 | # * --module-path (only if needed)
171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
172 |
173 | # For Cygwin or MSYS, switch paths to Windows format before running java
174 | if "$cygwin" || "$msys" ; then
175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
177 |
178 | JAVACMD=$( cygpath --unix "$JAVACMD" )
179 |
180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
181 | for arg do
182 | if
183 | case $arg in #(
184 | -*) false ;; # don't mess with options #(
185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
186 | [ -e "$t" ] ;; #(
187 | *) false ;;
188 | esac
189 | then
190 | arg=$( cygpath --path --ignore --mixed "$arg" )
191 | fi
192 | # Roll the args list around exactly as many times as the number of
193 | # args, so each arg winds up back in the position where it started, but
194 | # possibly modified.
195 | #
196 | # NB: a `for` loop captures its iteration list before it begins, so
197 | # changing the positional parameters here affects neither the number of
198 | # iterations, nor the values presented in `arg`.
199 | shift # remove old arg
200 | set -- "$@" "$arg" # push replacement arg
201 | done
202 | fi
203 |
204 |
205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
207 |
208 | # Collect all arguments for the java command:
209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
210 | # and any embedded shellness will be escaped.
211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
212 | # treated as '${Hostname}' itself on the command line.
213 |
214 | set -- \
215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
216 | -classpath "$CLASSPATH" \
217 | org.gradle.wrapper.GradleWrapperMain \
218 | "$@"
219 |
220 | # Stop when "xargs" is not available.
221 | if ! command -v xargs >/dev/null 2>&1
222 | then
223 | die "xargs is not available"
224 | fi
225 |
226 | # Use "xargs" to parse quoted args.
227 | #
228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
229 | #
230 | # In Bash we could simply go:
231 | #
232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
233 | # set -- "${ARGS[@]}" "$@"
234 | #
235 | # but POSIX shell has neither arrays nor command substitution, so instead we
236 | # post-process each arg (as a line of input to sed) to backslash-escape any
237 | # character that might be a shell metacharacter, then use eval to reverse
238 | # that process (while maintaining the separation between arguments), and wrap
239 | # the whole thing up as a single "set" statement.
240 | #
241 | # This will of course break if any of these variables contains a newline or
242 | # an unmatched quote.
243 | #
244 |
245 | eval "set -- $(
246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
247 | xargs -n1 |
248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
249 | tr '\n' ' '
250 | )" '"$@"'
251 |
252 | exec "$JAVACMD" "$@"
253 |
--------------------------------------------------------------------------------