├── app ├── .gitignore ├── src │ ├── main │ │ ├── ic_app_launcher-playstore.png │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_app_launcher.webp │ │ │ │ └── ic_app_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_app_launcher.webp │ │ │ │ └── ic_app_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_app_launcher.webp │ │ │ │ └── ic_app_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_app_launcher.webp │ │ │ │ └── ic_app_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_app_launcher.webp │ │ │ │ └── ic_app_launcher_round.webp │ │ │ ├── values │ │ │ │ ├── ic_app_launcher_background.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── themes.xml │ │ │ │ └── strings.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_app_launcher.xml │ │ │ │ └── ic_app_launcher_round.xml │ │ │ ├── xml │ │ │ │ ├── backup_rules.xml │ │ │ │ └── data_extraction_rules.xml │ │ │ └── drawable │ │ │ │ └── ic_app_launcher_foreground.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── github │ │ │ │ └── jing332 │ │ │ │ └── image_processor │ │ │ │ ├── ui │ │ │ │ ├── theme │ │ │ │ │ ├── Color.kt │ │ │ │ │ ├── Type.kt │ │ │ │ │ └── Theme.kt │ │ │ │ ├── widgets │ │ │ │ │ ├── Widgets.kt │ │ │ │ │ ├── LabelSlider.kt │ │ │ │ │ ├── LoadingDialog.kt │ │ │ │ │ ├── ExpandableText.kt │ │ │ │ │ └── DropMenuTextField.kt │ │ │ │ ├── AboutDialog.kt │ │ │ │ ├── ProcessorViewModel.kt │ │ │ │ └── MainActivity.kt │ │ │ │ ├── App.kt │ │ │ │ ├── utils │ │ │ │ ├── StringUtils.kt │ │ │ │ └── ASFUriUtils.kt │ │ │ │ └── help │ │ │ │ └── AppConfig.kt │ │ └── AndroidManifest.xml │ ├── debug │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── imageconv │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── example │ │ └── imageconv │ │ └── ExampleInstrumentedTest.kt ├── build.gradle.kts └── proguard-rules.pro ├── .idea ├── .name ├── .gitignore ├── compiler.xml ├── kotlinc.xml ├── vcs.xml ├── misc.xml ├── gradle.xml └── inspectionProfiles │ └── Project_Default.xml ├── CHANGELOG.md ├── images └── 1.jpg ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle.kts ├── README.md ├── gradle.properties ├── .github └── workflows │ ├── test.yaml │ └── release.yaml ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Image Processor -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | - 新的图标 2 | - 修复暗色模式下的状态栏颜色问题 -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /images/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/ImageProcessor/HEAD/images/1.jpg -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/ImageProcessor/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/ic_app_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/ImageProcessor/HEAD/app/src/main/ic_app_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | DB·图像处理器 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_app_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/ImageProcessor/HEAD/app/src/main/res/mipmap-hdpi/ic_app_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_app_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/ImageProcessor/HEAD/app/src/main/res/mipmap-mdpi/ic_app_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_app_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/ImageProcessor/HEAD/app/src/main/res/mipmap-xhdpi/ic_app_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_app_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/ImageProcessor/HEAD/app/src/main/res/mipmap-xxhdpi/ic_app_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_app_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/ImageProcessor/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_app_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_app_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/ImageProcessor/HEAD/app/src/main/res/mipmap-hdpi/ic_app_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_app_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/ImageProcessor/HEAD/app/src/main/res/mipmap-mdpi/ic_app_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_app_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/ImageProcessor/HEAD/app/src/main/res/mipmap-xhdpi/ic_app_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_app_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/ImageProcessor/HEAD/app/src/main/res/mipmap-xxhdpi/ic_app_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_app_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jing332/ImageProcessor/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_app_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/ic_app_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jul 18 16:15:24 CST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.jks 2 | *.iml 3 | .gradle 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_app_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_app_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/image_processor/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.image_processor.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/image_processor/App.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.image_processor 2 | 3 | import android.app.Application 4 | 5 | val app by lazy { App.instance } 6 | 7 | class App : Application() { 8 | companion object { 9 | lateinit var instance: App 10 | } 11 | 12 | override fun onCreate() { 13 | super.onCreate() 14 | 15 | instance = this 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven("https://jitpack.io") 14 | } 15 | } 16 | 17 | rootProject.name = "Image Processor" 18 | include(":app") 19 | -------------------------------------------------------------------------------- /app/src/test/java/com/example/imageconv/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.image_processor 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub release](https://img.shields.io/github/downloads/jing332/ImageProcessor/total) 2 | ![GitHub release (latest by date)](https://img.shields.io/github/downloads/jing332/ImageProcessor/latest/total) 3 | 4 | # ImageProcessor 5 | 6 | 安卓图片批量 jpeg png webp 互转,以及尺寸调节。 7 | 8 | ## Download 9 | 10 | - [Github Actions](https://github.com/jing332/ImageProcessor/actions) 11 | - [Github Release](https://github.com/jing332/ImageProcessor/releases) 12 | 13 | ## ScreenShoots 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/image_processor/utils/StringUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.image_processor.utils 2 | 3 | import kotlin.math.ln 4 | import kotlin.math.pow 5 | 6 | object StringUtils { 7 | fun formatFileSize(bytes: Long): String { 8 | val unit = 1024 9 | if (bytes < unit) return "$bytes B" 10 | val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt() 11 | val pre = "KMGTPE"[exp - 1] + "i" 12 | return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_app_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/image_processor/help/AppConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.image_processor.help 2 | 3 | import com.funny.data_saver.core.DataSaverPreferences 4 | import com.funny.data_saver.core.mutableDataSaverStateOf 5 | import com.github.jing332.image_processor.app 6 | 7 | object AppConfig { 8 | val dataSaverPref = DataSaverPreferences(app.getSharedPreferences("app", 0)) 9 | 10 | val sourceDirectory = mutableDataSaverStateOf( 11 | dataSaverInterface = dataSaverPref, 12 | key = "source_directory", 13 | initialValue = "" 14 | ) 15 | 16 | val targetFolderName = mutableDataSaverStateOf( 17 | dataSaverInterface = dataSaverPref, 18 | key = "target_folder_name", 19 | initialValue = "outputs" 20 | ) 21 | } -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/image_processor/ui/widgets/Widgets.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.image_processor.ui.widgets 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.SideEffect 6 | import androidx.compose.ui.graphics.Color 7 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 8 | 9 | 10 | @Composable 11 | fun TransparentSystemBars() { 12 | val systemUiController = rememberSystemUiController() 13 | val useDarkIcons = !isSystemInDarkTheme() 14 | SideEffect { 15 | systemUiController.setSystemBarsColor( 16 | color = Color.Transparent, 17 | darkIcons = useDarkIcons, 18 | isNavigationBarContrastEnforced = false, 19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/imageconv/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.image_processor 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.github.jing332.image_process", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 图像处理器 3 | 展开 4 | 收起 5 | 目标格式 6 | 高 (像素) 7 | 宽 (像素) 8 | 源目录 9 | 选择文件夹 10 | 单击右侧按钮选择目录 ➡ 11 | 使用上次目录:%1$s 12 | 开始 13 | 警告 14 | 1. 执行过程中请保证主界面可见,否则可能会导致异常。\n\n2. 转换后的文件保存在 源目录中的 %s 文件夹中。\n\n 3. 对于目标格式WEBP,有几率出现bug输出空文件。 15 | 关于 16 | APP版本:%s \nAPP构建时间:%s\n\n项目开源地址:https://github.com/jing332/ImageProcessor 17 | 更多选项 18 | 图像质量: %1$s 19 | 设置目标文件夹 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/image_processor/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.image_processor.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | 25 | android.enableR8.fullMode=false -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/image_processor/ui/AboutDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.image_processor.ui 2 | 3 | import androidx.compose.foundation.text.selection.SelectionContainer 4 | import androidx.compose.material3.AlertDialog 5 | import androidx.compose.material3.Text 6 | import androidx.compose.material3.TextButton 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.res.stringResource 9 | import com.github.jing332.image_processor.BuildConfig 10 | import com.github.jing332.image_processor.R 11 | import java.text.SimpleDateFormat 12 | import java.util.Locale 13 | 14 | @Composable 15 | fun AboutDialog(onDismissRequest: () -> Unit) { 16 | AlertDialog(onDismissRequest = onDismissRequest, confirmButton = { 17 | TextButton(onClick = { onDismissRequest() }) { 18 | Text(stringResource(id = android.R.string.ok)) 19 | } 20 | }, 21 | title = { Text(stringResource(id = R.string.about)) }, 22 | text = { 23 | SelectionContainer { 24 | Text( 25 | stringResource( 26 | id = R.string.about_content, 27 | "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", 28 | SimpleDateFormat( 29 | "yyyy-MM-dd HH:mm:ss", 30 | Locale.getDefault() 31 | ).format(BuildConfig.BUILD_TIME * 1000) 32 | ) 33 | ) 34 | } 35 | } 36 | ) 37 | } -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | paths-ignore: 8 | - 'README.md' 9 | 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | env: 16 | output: "${{ github.workspace }}/app/build/outputs/apk/release" 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | 22 | - uses: actions/setup-java@v3 23 | with: 24 | distribution: temurin 25 | java-version: 17 26 | 27 | - name: Setup Gradle 28 | uses: gradle/gradle-build-action@v2.4.2 29 | 30 | - name: Init Signature 31 | run: | 32 | touch local.properties 33 | echo ALIAS_NAME='${{ secrets.ALIAS_NAME }}' >> local.properties 34 | echo ALIAS_PASSWORD='${{ secrets.ALIAS_PASSWORD }}' >> local.properties 35 | echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' >> local.properties 36 | echo KEY_PATH='./key.jks' >> local.properties 37 | # 从Secrets读取无换行符Base64解码, 然后保存到到app/key.jks 38 | echo ${{ secrets.KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/app/key.jks 39 | 40 | - name: Grant execute permission for gradlew 41 | run: chmod +x gradlew 42 | - name: Build with Gradle 43 | run: ./gradlew assembleRelease -build-cache --parallel --daemon --warning-mode all 44 | 45 | - name: Init APP Version Name 46 | run: | 47 | echo "ver_name=$(grep -m 1 'versionName' ${{ env.output }}/output-metadata.json | cut -d\" -f4)" >> $GITHUB_ENV 48 | 49 | - name: Upload App To Artifact 50 | if: success () || failure () 51 | uses: actions/upload-artifact@v3 52 | with: 53 | name: "ImageProcessor_${{env.ver_name}}" 54 | path: ${{ env.output }}/*.apk 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/image_processor/ui/widgets/LabelSlider.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.text_searcher.ui.widgets 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.BoxScope 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.wrapContentHeight 7 | import androidx.compose.material3.Slider 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.compose.ui.unit.dp 13 | import androidx.constraintlayout.compose.ConstraintLayout 14 | 15 | @Composable 16 | fun LabelSlider( 17 | value: Float, 18 | onValueChange: (Float) -> Unit, 19 | modifier: Modifier = Modifier, 20 | enabled: Boolean = true, 21 | valueRange: ClosedFloatingPointRange = 0f..1f, 22 | steps: Int = 0, 23 | onValueChangeFinished: (() -> Unit)? = null, 24 | text: @Composable BoxScope.() -> Unit 25 | ) { 26 | ConstraintLayout(modifier) { 27 | val (textRef, sliderRef) = createRefs() 28 | Box( 29 | modifier = Modifier 30 | .constrainAs(textRef) { 31 | start.linkTo(parent.start) 32 | top.linkTo(parent.top) 33 | end.linkTo(parent.end) 34 | } 35 | .wrapContentHeight() 36 | ) { text() } 37 | Slider( 38 | modifier = Modifier 39 | .fillMaxWidth() 40 | .constrainAs(sliderRef) { 41 | start.linkTo(parent.start) 42 | end.linkTo(parent.end) 43 | 44 | top.linkTo(textRef.bottom, margin = (-16).dp) 45 | }, 46 | value = value, 47 | onValueChange = onValueChange, 48 | enabled = enabled, 49 | valueRange = valueRange, 50 | steps = steps, 51 | onValueChangeFinished = onValueChangeFinished 52 | ) 53 | } 54 | } 55 | 56 | @Preview 57 | @Composable 58 | fun PreviewSlider() { 59 | LabelSlider(value = 0f, onValueChange = {}) { 60 | Text("Hello World") 61 | } 62 | } -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | paths: 8 | - "CHANGELOG.md" 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | env: 15 | output: "${{ github.workspace }}/app/build/outputs/apk/release" 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | - uses: actions/setup-java@v3 21 | with: 22 | distribution: temurin 23 | java-version: 17 24 | 25 | - name: Setup Gradle 26 | uses: gradle/gradle-build-action@v2.4.2 27 | 28 | - name: Init Signature 29 | run: | 30 | touch local.properties 31 | echo ALIAS_NAME='${{ secrets.ALIAS_NAME }}' >> local.properties 32 | echo ALIAS_PASSWORD='${{ secrets.ALIAS_PASSWORD }}' >> local.properties 33 | echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' >> local.properties 34 | echo KEY_PATH='./key.jks' >> local.properties 35 | # 从Secrets读取无换行符Base64解码, 然后保存到到app/key.jks 36 | echo ${{ secrets.KEY_STORE }} | base64 --decode > $GITHUB_WORKSPACE/app/key.jks 37 | 38 | - name: Grant execute permission for gradlew 39 | run: chmod +x gradlew 40 | - name: Build with Gradle 41 | run: ./gradlew assembleRelease -build-cache --parallel --daemon --warning-mode all 42 | 43 | - name: Init APP Version Name 44 | run: | 45 | echo "ver_name=$(grep -m 1 'versionName' ${{ env.output }}/output-metadata.json | cut -d\" -f4)" >> $GITHUB_ENV 46 | 47 | - name: Upload App To Artifact 48 | if: success () || failure () 49 | uses: actions/upload-artifact@v3 50 | with: 51 | name: "ImageProcessor_${{env.ver_name}}" 52 | path: ${{ env.output }}/*.apk 53 | 54 | - uses: softprops/action-gh-release@v0.1.15 55 | with: 56 | name: ${{ env.ver_name }} 57 | tag_name: ${{ env.ver_name }} 58 | body_path: ${{ github.workspace }}/CHANGELOG.md 59 | draft: false 60 | prerelease: false 61 | files: ${{ env.output }}/*.apk 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.TOKEN }} -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/image_processor/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.image_processor.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun ImageConvTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | 53 | darkTheme -> DarkColorScheme 54 | else -> LightColorScheme 55 | } 56 | val view = LocalView.current 57 | if (!view.isInEditMode) { 58 | SideEffect { 59 | val window = (view.context as Activity).window 60 | window.statusBarColor = colorScheme.primary.toArgb() 61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 62 | } 63 | } 64 | 65 | MaterialTheme( 66 | colorScheme = colorScheme, 67 | typography = Typography, 68 | content = content 69 | ) 70 | } -------------------------------------------------------------------------------- /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 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/image_processor/ui/widgets/LoadingDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.text_searcher.ui.widgets 2 | 3 | import androidx.compose.animation.core.animateFloat 4 | import androidx.compose.animation.core.infiniteRepeatable 5 | import androidx.compose.animation.core.keyframes 6 | import androidx.compose.animation.core.rememberInfiniteTransition 7 | import androidx.compose.foundation.border 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.Spacer 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.shape.CircleShape 14 | import androidx.compose.material3.CircularProgressIndicator 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Surface 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.draw.rotate 25 | import androidx.compose.ui.graphics.Brush 26 | import androidx.compose.ui.graphics.Color 27 | import androidx.compose.ui.tooling.preview.Preview 28 | import androidx.compose.ui.unit.Dp 29 | import androidx.compose.ui.unit.dp 30 | import androidx.compose.ui.window.Dialog 31 | 32 | 33 | @Composable 34 | fun ProgressIndicatorLoading(progressIndicatorSize: Dp, progressIndicatorColor: Color) { 35 | val infiniteTransition = rememberInfiniteTransition(label = "") 36 | 37 | val angle by infiniteTransition.animateFloat( 38 | initialValue = 0f, 39 | targetValue = 360f, 40 | animationSpec = infiniteRepeatable( 41 | animation = keyframes { 42 | durationMillis = 600 43 | } 44 | ), label = "" 45 | ) 46 | 47 | CircularProgressIndicator( 48 | progress = 1f, 49 | modifier = Modifier 50 | .size(progressIndicatorSize) 51 | .rotate(angle) 52 | .border( 53 | 12.dp, 54 | brush = Brush.sweepGradient( 55 | listOf( 56 | Color.Transparent, 57 | progressIndicatorColor.copy(alpha = 0.1f), 58 | progressIndicatorColor 59 | ) 60 | ), 61 | shape = CircleShape 62 | ), 63 | strokeWidth = 1.dp, 64 | color = Color.Transparent 65 | ) 66 | } 67 | 68 | @Composable 69 | fun LoadingDialog(onDismissRequest: () -> Unit) { 70 | Dialog(onDismissRequest = onDismissRequest) { 71 | Surface( 72 | tonalElevation = 4.dp, 73 | shape = MaterialTheme.shapes.medium, 74 | ) { 75 | Column(modifier = Modifier.padding(horizontal = 48.dp, vertical = 12.dp)) { 76 | ProgressIndicatorLoading( 77 | progressIndicatorSize = 64.dp, 78 | progressIndicatorColor = MaterialTheme.colorScheme.primary 79 | ) 80 | Spacer( 81 | modifier = Modifier 82 | .height(16.dp) 83 | ) 84 | Text( 85 | text = "加载中", 86 | ) 87 | } 88 | } 89 | } 90 | } 91 | 92 | @Preview 93 | @Composable 94 | fun PreviewDialog() { 95 | var isShow by remember { mutableStateOf(true) } 96 | if (isShow) 97 | LoadingDialog(onDismissRequest = { isShow = false }) 98 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/image_processor/ui/widgets/ExpandableText.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.image_processor.ui.widgets 2 | 3 | import androidx.compose.animation.animateContentSize 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.material3.LocalTextStyle 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableIntStateOf 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.setValue 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.text.SpanStyle 18 | import androidx.compose.ui.text.TextStyle 19 | import androidx.compose.ui.text.buildAnnotatedString 20 | import androidx.compose.ui.text.font.FontStyle 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.text.style.TextAlign 23 | import androidx.compose.ui.text.withStyle 24 | import androidx.compose.ui.unit.TextUnit 25 | import com.github.jing332.image_processor.R 26 | 27 | 28 | // from https://stackoverflow.com/a/72982110/13197001 29 | @Composable 30 | fun ExpandableText( 31 | modifier: Modifier = Modifier, 32 | textModifier: Modifier = Modifier, 33 | style: TextStyle = LocalTextStyle.current, 34 | fontStyle: FontStyle? = null, 35 | fontSize: TextUnit = LocalTextStyle.current.fontSize, 36 | fontWeight: FontWeight = LocalTextStyle.current.fontWeight ?: FontWeight.Normal, 37 | lineHeight: TextUnit = LocalTextStyle.current.lineHeight, 38 | text: String, 39 | collapsedMaxLine: Int = 2, 40 | showMoreText: String = stringResource(R.string.expandble_text_more), 41 | showMoreStyle: SpanStyle = SpanStyle(fontWeight = FontWeight.ExtraBold), 42 | showLessText: String = stringResource(R.string.expandble_text_less), 43 | showLessStyle: SpanStyle = showMoreStyle, 44 | textAlign: TextAlign? = null, 45 | ) { 46 | var isExpanded by remember { mutableStateOf(false) } 47 | var clickable by remember { mutableStateOf(false) } 48 | var lastCharIndex by remember { mutableIntStateOf(0) } 49 | Box(modifier = Modifier 50 | .clickable(clickable) { 51 | isExpanded = !isExpanded 52 | } 53 | .then(modifier)) { 54 | Text( 55 | modifier = textModifier 56 | .fillMaxWidth() 57 | .animateContentSize(), 58 | text = buildAnnotatedString { 59 | if (clickable) { 60 | if (isExpanded) { 61 | append(text) 62 | withStyle(style = showLessStyle) { append(showLessText) } 63 | } else { 64 | val adjustText = text.substring(startIndex = 0, endIndex = lastCharIndex) 65 | .dropLast(showMoreText.length) 66 | .dropLastWhile { Character.isWhitespace(it) || it == '.' } 67 | append(adjustText) 68 | withStyle(style = showMoreStyle) { append(showMoreText) } 69 | } 70 | } else { 71 | append(text) 72 | } 73 | }, 74 | maxLines = if (isExpanded) Int.MAX_VALUE else collapsedMaxLine, 75 | fontStyle = fontStyle, 76 | onTextLayout = { textLayoutResult -> 77 | if (!isExpanded && textLayoutResult.hasVisualOverflow) { 78 | clickable = true 79 | lastCharIndex = textLayoutResult.getLineEnd(collapsedMaxLine - 1) 80 | } 81 | }, 82 | style = style, 83 | textAlign = textAlign, 84 | fontSize = fontSize, 85 | fontWeight = fontWeight, 86 | lineHeight = lineHeight, 87 | ) 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/image_processor/ui/widgets/DropMenuTextField.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.image_processor.ui.widgets 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.interaction.MutableInteractionSource 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.filled.ArrowDropDown 8 | import androidx.compose.material.icons.filled.ArrowDropUp 9 | import androidx.compose.material.ripple.rememberRipple 10 | import androidx.compose.material3.DropdownMenu 11 | import androidx.compose.material3.DropdownMenuItem 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.OutlinedTextField 15 | import androidx.compose.material3.Text 16 | import androidx.compose.material3.TextFieldDefaults 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableIntStateOf 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.text.font.FontWeight 26 | import androidx.compose.ui.text.input.TextFieldValue 27 | import androidx.compose.ui.tooling.preview.Preview 28 | import kotlin.math.max 29 | 30 | 31 | /** 32 | * 下拉框菜单 33 | */ 34 | @Composable 35 | fun DropMenuTextField( 36 | modifier: Modifier = Modifier, 37 | label: @Composable() (() -> Unit), 38 | key: Any, 39 | keys: List, 40 | values: List, 41 | onKeyChange: (key: Any) -> Unit, 42 | ) { 43 | var value = values.getOrNull(max(0, keys.indexOf(key))) ?: "" 44 | var menuExpanded by remember { mutableStateOf(false) } 45 | 46 | OutlinedTextField( 47 | value = TextFieldValue(value), 48 | onValueChange = { value = "" }, 49 | label = label, 50 | readOnly = true, 51 | enabled = false, 52 | colors = TextFieldDefaults.colors( 53 | disabledContainerColor = MaterialTheme.colorScheme.surface, 54 | disabledTextColor = MaterialTheme.colorScheme.onSurface, 55 | disabledLabelColor = MaterialTheme.colorScheme.onSurface, 56 | disabledTrailingIconColor = MaterialTheme.colorScheme.onSurface, 57 | ), 58 | modifier = modifier 59 | .clickable( 60 | interactionSource = remember { MutableInteractionSource() }, 61 | indication = rememberRipple(bounded = true), 62 | onClick = { menuExpanded = true } 63 | ), 64 | trailingIcon = { 65 | Icon(if (menuExpanded) Icons.Filled.ArrowDropUp else Icons.Filled.ArrowDropDown, null) 66 | } 67 | ) 68 | 69 | DropdownMenu( 70 | expanded = menuExpanded, 71 | onDismissRequest = { menuExpanded = false }) { 72 | values.forEachIndexed { index, s -> 73 | val checked = key == keys[index] 74 | DropdownMenuItem( 75 | text = { 76 | Text( 77 | s, fontWeight = if (checked) FontWeight.Bold else FontWeight.Normal 78 | ) 79 | }, 80 | onClick = { 81 | menuExpanded = false 82 | value = s 83 | onKeyChange.invoke(keys[index]) 84 | }, modifier = Modifier.background( 85 | if (checked) MaterialTheme.colorScheme.surfaceVariant 86 | else Color.Transparent 87 | ) 88 | ) 89 | } 90 | } 91 | } 92 | 93 | @Preview 94 | @Composable 95 | fun PreviewDropMenu() { 96 | var key by remember { mutableIntStateOf(1) } 97 | DropMenuTextField( 98 | label = { Text("所属分组") }, 99 | key = key, 100 | keys = listOf(1, 2, 3), 101 | values = listOf("1", "2", "3"), 102 | ) { 103 | key = it as Int 104 | } 105 | } -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.android.build.gradle.internal.api.ApkVariantOutputImpl 2 | import java.util.Properties 3 | import java.io.FileInputStream 4 | import java.text.SimpleDateFormat 5 | import java.util.Date 6 | import java.util.TimeZone 7 | 8 | plugins { 9 | id("com.android.application") 10 | id("org.jetbrains.kotlin.android") 11 | } 12 | 13 | fun releaseTime(): String { 14 | val dateFormat = SimpleDateFormat("yy.MMddHH") 15 | dateFormat.timeZone = TimeZone.getTimeZone("GMT+8") 16 | return dateFormat.format(Date()) 17 | } 18 | 19 | // 秒时间戳 20 | fun buildTime(): Long { 21 | return Date().time / 1000 22 | } 23 | 24 | fun executeCommand(command: String): String { 25 | val process = Runtime.getRuntime().exec(command) 26 | process.waitFor() 27 | val output = process.inputStream.bufferedReader().use { it.readText() } 28 | return output.trim() 29 | } 30 | 31 | val name = "ImageProcessor" 32 | val version = "1.${releaseTime()}" 33 | val gitCommits: Int = executeCommand("git rev-list HEAD --count").trim().toInt() 34 | 35 | android { 36 | namespace = "com.github.jing332.image_processor" 37 | compileSdk = 33 38 | 39 | defaultConfig { 40 | applicationId = "com.github.jing332.image_processor" 41 | minSdk = 24 42 | targetSdk = 33 43 | versionCode = gitCommits 44 | versionName = version 45 | 46 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 47 | vectorDrawables { 48 | useSupportLibrary = true 49 | } 50 | 51 | // 写入构建 秒时间戳 52 | buildConfigField("long", "BUILD_TIME", "${buildTime()}") 53 | } 54 | 55 | 56 | signingConfigs { 57 | val pro = Properties() 58 | val input = FileInputStream(project.rootProject.file("local.properties")) 59 | pro.load(input) 60 | 61 | create("release") { 62 | storeFile = file(pro.getProperty("KEY_PATH")) 63 | storePassword = pro.getProperty("KEY_PASSWORD") 64 | keyAlias = pro.getProperty("ALIAS_NAME") 65 | keyPassword = pro.getProperty("ALIAS_PASSWORD") 66 | } 67 | } 68 | 69 | buildTypes { 70 | release { 71 | signingConfig = signingConfigs.getByName("release") 72 | isMinifyEnabled = true 73 | proguardFiles( 74 | getDefaultProguardFile("proguard-android-optimize.txt"), 75 | "proguard-rules.pro" 76 | ) 77 | } 78 | 79 | debug { 80 | signingConfig = signingConfigs.getByName("release") 81 | isMinifyEnabled = false 82 | applicationIdSuffix = ".debug" 83 | versionNameSuffix = "_debug" 84 | } 85 | } 86 | compileOptions { 87 | sourceCompatibility = JavaVersion.VERSION_17 88 | targetCompatibility = JavaVersion.VERSION_17 89 | } 90 | kotlinOptions { 91 | jvmTarget = "17" 92 | } 93 | buildFeatures { 94 | compose = true 95 | buildConfig = true 96 | } 97 | composeOptions { 98 | kotlinCompilerExtensionVersion = "1.4.3" 99 | } 100 | packaging { 101 | resources { 102 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 103 | } 104 | } 105 | 106 | android.applicationVariants.configureEach { 107 | outputs.configureEach { 108 | if (this is ApkVariantOutputImpl) outputFileName = "${name}-v${versionName}.apk" 109 | } 110 | } 111 | } 112 | 113 | dependencies { 114 | val accompanistVersion = "0.31.3-beta" 115 | implementation("com.google.accompanist:accompanist-systemuicontroller:${accompanistVersion}") 116 | implementation("com.google.accompanist:accompanist-navigation-animation:${accompanistVersion}") 117 | 118 | implementation("androidx.savedstate:savedstate:1.2.1") 119 | 120 | // 图片加载 121 | implementation("io.coil-kt:coil-compose:2.4.0") 122 | 123 | // 图片预览 124 | implementation("com.github.jvziyaoyao:ImageViewer:1.0.2-alpha.4") 125 | 126 | // 数据持久化 127 | implementation("com.github.FunnySaltyFish.ComposeDataSaver:data-saver:v1.1.5") 128 | 129 | 130 | implementation("androidx.documentfile:documentfile:1.0.1") 131 | implementation("androidx.compose.material3:material3:1.1.1") 132 | implementation("androidx.compose.material:material-icons-extended:1.4.3") 133 | implementation("androidx.compose.material3:material3-window-size-class:1.1.1") 134 | 135 | implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1") 136 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1") 137 | 138 | implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1") 139 | implementation("androidx.navigation:navigation-compose:2.6.0") 140 | 141 | implementation("androidx.core:core-ktx:1.10.1") 142 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") 143 | implementation("androidx.activity:activity-compose:1.7.2") 144 | implementation(platform("androidx.compose:compose-bom:2023.03.00")) 145 | implementation("androidx.compose.ui:ui") 146 | implementation("androidx.compose.ui:ui-graphics") 147 | implementation("androidx.compose.ui:ui-tooling-preview") 148 | implementation("androidx.compose.material3:material3") 149 | testImplementation("junit:junit:4.13.2") 150 | androidTestImplementation("androidx.test.ext:junit:1.1.5") 151 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") 152 | androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00")) 153 | androidTestImplementation("androidx.compose.ui:ui-test-junit4") 154 | debugImplementation("androidx.compose.ui:ui-tooling") 155 | debugImplementation("androidx.compose.ui:ui-test-manifest") 156 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/image_processor/utils/ASFUriUtils.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.image_processor.utils 2 | 3 | import android.content.ContentUris 4 | import android.content.Context 5 | import android.database.Cursor 6 | import android.net.Uri 7 | import android.os.Environment 8 | import android.provider.DocumentsContract 9 | import android.provider.MediaStore 10 | import androidx.core.net.toUri 11 | import java.io.File 12 | 13 | 14 | @Suppress("MemberVisibilityCanBePrivate") 15 | object ASFUriUtils { 16 | fun toUri(path: String): Uri { 17 | return File(path).toUri() 18 | } 19 | 20 | /** 21 | * Content URI 转为 绝对路径 22 | * @return 绝对路径, 如果失败则为 null 23 | */ 24 | fun Context.getPath(uri: Uri?, isTree: Boolean = false): String? { 25 | if (uri == null) return null 26 | 27 | kotlin.runCatching { 28 | if (uri.toString().startsWith("/")) return uri.toString() 29 | return if (isTree) 30 | getPathFromTree(this, uri) 31 | else 32 | getPath(this, uri) 33 | } 34 | 35 | return null 36 | } 37 | 38 | fun getPathFromTree(context: Context, uri: Uri?): String? { 39 | if (uri == null) return null 40 | 41 | val docUri = DocumentsContract.buildDocumentUriUsingTree( 42 | uri, DocumentsContract.getTreeDocumentId(uri) 43 | ) 44 | return getPath(context, docUri) 45 | } 46 | 47 | fun getPath(context: Context, uri: Uri?): String? { 48 | if (uri == null) return null 49 | 50 | val isKitKat = true 51 | // DocumentProvider 52 | if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { 53 | // ExternalStorageProvider 54 | if (isExternalStorageDocument(uri)) { 55 | val docId = DocumentsContract.getDocumentId(uri) 56 | val split = docId.split(":").toTypedArray() 57 | val type = split[0] 58 | if ("primary".equals(type, ignoreCase = true)) { 59 | return Environment.getExternalStorageDirectory().toString() + "/" + split[1] 60 | } 61 | 62 | // TODO handle non-primary volumes 63 | } else if (isDownloadsDocument(uri)) { 64 | val id = DocumentsContract.getDocumentId(uri) 65 | val contentUri: Uri = ContentUris.withAppendedId( 66 | Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id) 67 | ) 68 | return getDataColumn(context, contentUri, null, null) 69 | } else if (isMediaDocument(uri)) { 70 | val docId = DocumentsContract.getDocumentId(uri) 71 | val split = docId.split(":").toTypedArray() 72 | val type = split[0] 73 | val contentUri: Uri? = if ("image" == type) { 74 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI 75 | } else if ("video" == type) { 76 | MediaStore.Video.Media.EXTERNAL_CONTENT_URI 77 | } else if ("audio" == type) { 78 | MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 79 | } else 80 | MediaStore.Files.getContentUri("external") 81 | 82 | val selection = "_id=?" 83 | val selectionArgs = arrayOf( 84 | split[1] 85 | ) 86 | return getDataColumn(context, contentUri, selection, selectionArgs) 87 | } 88 | } else if ("content".equals(uri.getScheme(), ignoreCase = true)) { 89 | 90 | // Return the remote address 91 | return if (isGooglePhotosUri(uri)) uri.getLastPathSegment() else getDataColumn( 92 | context, uri, null, null 93 | ) 94 | } else if ("file".equals(uri.getScheme(), ignoreCase = true)) { 95 | return uri.getPath() 96 | } 97 | return null 98 | } 99 | 100 | fun getDataColumn( 101 | context: Context, uri: Uri?, selection: String?, selectionArgs: Array? 102 | ): String? { 103 | var cursor: Cursor? = null 104 | val column = "_data" 105 | val projection = arrayOf( 106 | column 107 | ) 108 | try { 109 | cursor = context.contentResolver.query( 110 | uri!!, projection, selection, selectionArgs, null 111 | ) 112 | if (cursor != null && cursor.moveToFirst()) { 113 | val index: Int = cursor.getColumnIndexOrThrow(column) 114 | return cursor.getString(index) 115 | } 116 | } finally { 117 | cursor?.close() 118 | } 119 | return null 120 | } 121 | 122 | 123 | /** 124 | * @param uri The Uri to check. 125 | * @return Whether the Uri authority is ExternalStorageProvider. 126 | */ 127 | fun isExternalStorageDocument(uri: Uri): Boolean { 128 | return "com.android.externalstorage.documents" == uri.getAuthority() 129 | } 130 | 131 | /** 132 | * @param uri The Uri to check. 133 | * @return Whether the Uri authority is DownloadsProvider. 134 | */ 135 | fun isDownloadsDocument(uri: Uri): Boolean { 136 | return "com.android.providers.downloads.documents" == uri.getAuthority() 137 | } 138 | 139 | /** 140 | * @param uri The Uri to check. 141 | * @return Whether the Uri authority is MediaProvider. 142 | */ 143 | fun isMediaDocument(uri: Uri): Boolean { 144 | return "com.android.providers.media.documents" == uri.getAuthority() 145 | } 146 | 147 | /** 148 | * @param uri The Uri to check. 149 | * @return Whether the Uri authority is Google Photos. 150 | */ 151 | fun isGooglePhotosUri(uri: Uri): Boolean { 152 | return "com.google.android.apps.photos.content" == uri.getAuthority() 153 | } 154 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/image_processor/ui/ProcessorViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.image_processor.ui 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.Bitmap.CompressFormat 6 | import android.graphics.BitmapFactory 7 | import android.graphics.Canvas 8 | import android.graphics.Matrix 9 | import android.graphics.Paint 10 | import android.net.Uri 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateListOf 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.setValue 15 | import androidx.core.net.toUri 16 | import androidx.documentfile.provider.DocumentFile 17 | import androidx.lifecycle.ViewModel 18 | import androidx.lifecycle.viewModelScope 19 | import com.github.jing332.image_processor.utils.StringUtils 20 | import kotlinx.coroutines.Dispatchers 21 | import kotlinx.coroutines.launch 22 | import kotlinx.coroutines.withContext 23 | 24 | class ProcessorViewModel : ViewModel() { 25 | var srcDir by mutableStateOf("") 26 | var running by mutableStateOf(false) 27 | var process by mutableStateOf(0f) 28 | 29 | var format by mutableStateOf(CompressFormat.PNG.name) 30 | var width by mutableStateOf("0") 31 | var height by mutableStateOf("0") 32 | 33 | val files = mutableStateListOf() 34 | 35 | fun loadDir(context: Context, dir: Uri) { 36 | viewModelScope.launch(Dispatchers.IO) { 37 | withContext(Dispatchers.Main) { files.clear() } 38 | 39 | val tree = DocumentFile.fromTreeUri(context, dir) 40 | tree!!.listFiles().forEach { 41 | DocumentFile.fromSingleUri(context, it.uri)!!.let { doc -> 42 | if (doc.type?.contains("image") == true) { 43 | files.add( 44 | FileModel( 45 | uri = doc.uri, 46 | uriString = doc.uri.toString(), 47 | name = doc.name ?: "", 48 | processState = ProcessState.IDLE, 49 | size = StringUtils.formatFileSize(doc.length()), 50 | ) 51 | ) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | fun executeConv( 59 | context: Context, 60 | srcUri: Uri, 61 | folderName: String = "outputs", 62 | format: CompressFormat, 63 | quality: Int, 64 | width: Int = 0, 65 | height: Int = 0, 66 | ) { 67 | viewModelScope.launch(Dispatchers.IO) { 68 | running = true 69 | files.toList().forEachIndexed { index, fileModel -> 70 | files[index] = fileModel.copy(processState = ProcessState.IDLE) 71 | } 72 | 73 | val fileList = files.toList() 74 | val count = fileList.size 75 | fileList.forEachIndexed { index, fileModel -> 76 | files[index] = fileModel.copy(processState = ProcessState.PROCESSING) 77 | 78 | runCatching { 79 | val srcDir = DocumentFile.fromTreeUri(context, srcUri)!! 80 | val target = 81 | srcDir.listFiles().find { it.name == folderName } ?: srcDir.createDirectory( 82 | folderName 83 | ) 84 | 85 | context.contentResolver.openInputStream(fileModel.uriString.toUri())?.use { 86 | val extName = format.name.lowercase().replace(Regex("_.*"), "") 87 | val newFileName = 88 | fileModel.name.substringBeforeLast(".") + ".${extName}" 89 | 90 | target!!.findFile(newFileName)?.delete() 91 | target.createFile("image/${extName}", newFileName)?.let { file -> 92 | file.uri.let { uri -> 93 | context.contentResolver.openOutputStream(uri)?.use { outputStream -> 94 | var bitmap = BitmapFactory.decodeStream(it) 95 | if (width > 0 && height > 0) 96 | bitmap = resizeBitmap(bitmap, width, height) 97 | 98 | bitmap.compress(format, quality, outputStream) 99 | } 100 | } 101 | } 102 | } 103 | }.onFailure { 104 | it.printStackTrace() 105 | files[index] = fileModel.copy(processState = ProcessState.ERROR(it)) 106 | }.onSuccess { 107 | files[index] = fileModel.copy(processState = ProcessState.DONE) 108 | } 109 | 110 | process = (index + 1) / count.toFloat() 111 | println(process) 112 | } 113 | 114 | running = false 115 | process = 0.0f 116 | } 117 | } 118 | 119 | private fun resizeBitmap(bm: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap? { 120 | val srcWidth = bm.width 121 | val srcHeight = bm.height 122 | val widthScale = targetWidth * 1f / srcWidth 123 | val heightScale = targetHeight * 1f / srcHeight 124 | val matrix = Matrix() 125 | matrix.postScale(widthScale, heightScale, 0f, 0f) 126 | // 如需要可自行设置 Bitmap.Config.RGB_8888 等等 127 | val bmpRet = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.RGB_565) 128 | val canvas = Canvas(bmpRet) 129 | val paint = Paint() 130 | canvas.drawBitmap(bm, matrix, paint) 131 | return bmpRet 132 | } 133 | 134 | data class FileModel( 135 | val name: String, 136 | val uri: Uri, 137 | val uriString: String, 138 | val processState: ProcessState, 139 | val size: String = "0 KB", 140 | ) 141 | 142 | } 143 | 144 | 145 | sealed class ProcessState(id: Int) { 146 | object IDLE : ProcessState(0) 147 | object PROCESSING : ProcessState(1) 148 | object DONE : ProcessState(2) 149 | data class ERROR(val t: Throwable) : ProcessState(3) 150 | } 151 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or 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 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | # Add project specific ProGuard rules here. 24 | # You can control the set of applied configuration files using the 25 | # proguardFiles setting in build.gradle. 26 | # 27 | # For more details, see 28 | # http://developer.android.com/guide/developing/tools/proguard.html 29 | 30 | # If your project uses WebView with JS, uncomment the following 31 | # and specify the fully qualified class name to the JavaScript interface 32 | # class: 33 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 34 | # public *; 35 | #} 36 | 37 | # Uncomment this to preserve the line number information for 38 | # debugging stack traces. 39 | #-keepattributes SourceFile,LineNumberTable 40 | 41 | # If you keep the line number information, uncomment this to 42 | # hide the original source file name. 43 | #-renamesourcefileattribute SourceFile 44 | 45 | ## Rhino 46 | #-keep class javax.script.** { *; } 47 | #-keep class com.sun.script.javascript.** { *; } 48 | #-keep class org.mozilla.javascript.** { *; } 49 | #-keep class com.script.javascript.** { *; } 50 | 51 | #-keep class cn.hutool.crypto.** { *; } 52 | #-keep class cn.hutool.core.** { *; } 53 | -keep class com.aallam.openai.api.exception.** { *; } 54 | 55 | #-------------- 去掉所有打印 ------------- 56 | 57 | -assumenosideeffects class android.util.Log { 58 | public static *** d(...); 59 | 60 | # public static *** e(...); 61 | 62 | public static *** i(...); 63 | 64 | public static *** v(...); 65 | 66 | public static *** println(...); 67 | 68 | public static *** w(...); 69 | 70 | public static *** wtf(...); 71 | 72 | } 73 | 74 | -assumenosideeffects class android.util.Log { 75 | public static *** d(...); 76 | 77 | public static *** v(...); 78 | 79 | } 80 | 81 | -assumenosideeffects class android.util.Log { 82 | # public static *** e(...); 83 | 84 | public static *** v(...); 85 | 86 | } 87 | 88 | -assumenosideeffects class android.util.Log { 89 | public static *** i(...); 90 | 91 | public static *** v(...); 92 | 93 | } 94 | 95 | -assumenosideeffects class android.util.Log { 96 | public static *** w(...); 97 | 98 | public static *** v(...); 99 | 100 | } 101 | 102 | -assumenosideeffects class java.io.PrintStream { 103 | public *** println(...); 104 | 105 | public *** print(...); 106 | 107 | } 108 | 109 | # Keep `Companion` object fields of serializable classes. 110 | # This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. 111 | -if @kotlinx.serialization.Serializable class ** 112 | -keepclassmembers class <1> { 113 | static <1>$Companion Companion; 114 | } 115 | 116 | # Keep `serializer()` on companion objects (both default and named) of serializable classes. 117 | -if @kotlinx.serialization.Serializable class ** { 118 | static **$* *; 119 | } 120 | -keepclassmembers class <2>$<3> { 121 | kotlinx.serialization.KSerializer serializer(...); 122 | } 123 | 124 | # Keep `INSTANCE.serializer()` of serializable objects. 125 | -if @kotlinx.serialization.Serializable class ** { 126 | public static ** INSTANCE; 127 | } 128 | -keepclassmembers class <1> { 129 | public static <1> INSTANCE; 130 | kotlinx.serialization.KSerializer serializer(...); 131 | } 132 | 133 | # @Serializable and @Polymorphic are used at runtime for polymorphic serialization. 134 | -keepattributes RuntimeVisibleAnnotations,AnnotationDefault 135 | 136 | 137 | 138 | -dontwarn com.bumptech.glide.Glide 139 | -dontwarn com.bumptech.glide.RequestBuilder 140 | -dontwarn com.bumptech.glide.RequestManager 141 | -dontwarn com.bumptech.glide.request.BaseRequestOptions 142 | -dontwarn com.bumptech.glide.request.target.ViewTarget 143 | -dontwarn com.squareup.picasso.Picasso 144 | -dontwarn com.squareup.picasso.RequestCreator 145 | -dontwarn java.awt.AWTException 146 | -dontwarn java.awt.AlphaComposite 147 | -dontwarn java.awt.BasicStroke 148 | -dontwarn java.awt.Color 149 | -dontwarn java.awt.Composite 150 | -dontwarn java.awt.Desktop 151 | -dontwarn java.awt.Dimension 152 | -dontwarn java.awt.Font 153 | -dontwarn java.awt.FontFormatException 154 | -dontwarn java.awt.FontMetrics 155 | -dontwarn java.awt.Graphics2D 156 | -dontwarn java.awt.Graphics 157 | -dontwarn java.awt.GraphicsConfiguration 158 | -dontwarn java.awt.GraphicsDevice 159 | -dontwarn java.awt.GraphicsEnvironment 160 | -dontwarn java.awt.Image 161 | -dontwarn java.awt.Point 162 | -dontwarn java.awt.Rectangle 163 | -dontwarn java.awt.RenderingHints$Key 164 | -dontwarn java.awt.RenderingHints 165 | -dontwarn java.awt.Robot 166 | -dontwarn java.awt.Shape 167 | -dontwarn java.awt.Stroke 168 | -dontwarn java.awt.Toolkit 169 | -dontwarn java.awt.color.ColorSpace 170 | -dontwarn java.awt.datatransfer.Clipboard 171 | -dontwarn java.awt.datatransfer.ClipboardOwner 172 | -dontwarn java.awt.datatransfer.DataFlavor 173 | -dontwarn java.awt.datatransfer.StringSelection 174 | -dontwarn java.awt.datatransfer.Transferable 175 | -dontwarn java.awt.datatransfer.UnsupportedFlavorException 176 | -dontwarn java.awt.font.FontRenderContext 177 | -dontwarn java.awt.geom.AffineTransform 178 | -dontwarn java.awt.geom.Ellipse2D$Double 179 | -dontwarn java.awt.geom.Rectangle2D 180 | -dontwarn java.awt.geom.RoundRectangle2D$Double 181 | -dontwarn java.awt.image.AffineTransformOp 182 | -dontwarn java.awt.image.BufferedImage 183 | -dontwarn java.awt.image.BufferedImageOp 184 | -dontwarn java.awt.image.ColorConvertOp 185 | -dontwarn java.awt.image.ColorModel 186 | -dontwarn java.awt.image.CropImageFilter 187 | -dontwarn java.awt.image.DataBuffer 188 | -dontwarn java.awt.image.DataBufferByte 189 | -dontwarn java.awt.image.DataBufferInt 190 | -dontwarn java.awt.image.FilteredImageSource 191 | -dontwarn java.awt.image.ImageFilter 192 | -dontwarn java.awt.image.ImageObserver 193 | -dontwarn java.awt.image.ImageProducer 194 | -dontwarn java.awt.image.RenderedImage 195 | -dontwarn java.awt.image.SampleModel 196 | -dontwarn java.awt.image.WritableRaster 197 | -dontwarn java.beans.BeanInfo 198 | -dontwarn java.beans.FeatureDescriptor 199 | -dontwarn java.beans.IntrospectionException 200 | -dontwarn java.beans.Introspector 201 | -dontwarn java.beans.PropertyDescriptor 202 | -dontwarn java.beans.PropertyEditor 203 | -dontwarn java.beans.PropertyEditorManager 204 | -dontwarn java.beans.Transient 205 | -dontwarn java.beans.XMLEncoder 206 | -dontwarn java.lang.management.ManagementFactory 207 | -dontwarn java.lang.management.RuntimeMXBean 208 | -dontwarn javax.imageio.IIOImage 209 | -dontwarn javax.imageio.ImageIO 210 | -dontwarn javax.imageio.ImageReader 211 | -dontwarn javax.imageio.ImageTypeSpecifier 212 | -dontwarn javax.imageio.ImageWriteParam 213 | -dontwarn javax.imageio.ImageWriter 214 | -dontwarn javax.imageio.metadata.IIOMetadata 215 | -dontwarn javax.imageio.stream.ImageInputStream 216 | -dontwarn javax.imageio.stream.ImageOutputStream 217 | -dontwarn javax.naming.InitialContext 218 | -dontwarn javax.naming.NamingEnumeration 219 | -dontwarn javax.naming.NamingException 220 | -dontwarn javax.naming.directory.Attribute 221 | -dontwarn javax.naming.directory.Attributes 222 | -dontwarn javax.naming.directory.InitialDirContext 223 | -dontwarn javax.swing.ImageIcon 224 | -dontwarn javax.tools.DiagnosticCollector 225 | -dontwarn javax.tools.DiagnosticListener 226 | -dontwarn javax.tools.FileObject 227 | -dontwarn javax.tools.ForwardingJavaFileManager 228 | -dontwarn javax.tools.JavaCompiler$CompilationTask 229 | -dontwarn javax.tools.JavaCompiler 230 | -dontwarn javax.tools.JavaFileManager$Location 231 | -dontwarn javax.tools.JavaFileManager 232 | -dontwarn javax.tools.JavaFileObject$Kind 233 | -dontwarn javax.tools.JavaFileObject 234 | -dontwarn javax.tools.SimpleJavaFileObject 235 | -dontwarn javax.tools.StandardJavaFileManager 236 | -dontwarn javax.tools.StandardLocation 237 | -dontwarn javax.tools.ToolProvider 238 | -dontwarn javax.xml.bind.JAXBContext 239 | -dontwarn javax.xml.bind.Marshaller 240 | -dontwarn javax.xml.bind.Unmarshaller 241 | -dontwarn org.bouncycastle.asn1.ASN1Encodable 242 | -dontwarn org.bouncycastle.asn1.ASN1InputStream 243 | -dontwarn org.bouncycastle.asn1.ASN1Object 244 | -dontwarn org.bouncycastle.asn1.ASN1ObjectIdentifier 245 | -dontwarn org.bouncycastle.asn1.ASN1Primitive 246 | -dontwarn org.bouncycastle.asn1.ASN1Sequence 247 | -dontwarn org.bouncycastle.asn1.BERSequence 248 | -dontwarn org.bouncycastle.asn1.DERSequence 249 | -dontwarn org.bouncycastle.asn1.DLSequence 250 | -dontwarn org.bouncycastle.asn1.gm.GMNamedCurves 251 | -dontwarn org.bouncycastle.asn1.pkcs.PrivateKeyInfo 252 | -dontwarn org.bouncycastle.asn1.sec.ECPrivateKey 253 | -dontwarn org.bouncycastle.asn1.util.ASN1Dump 254 | -dontwarn org.bouncycastle.asn1.x509.AlgorithmIdentifier 255 | -dontwarn org.bouncycastle.asn1.x509.SubjectPublicKeyInfo 256 | -dontwarn org.bouncycastle.asn1.x9.X9ECParameters 257 | -dontwarn org.bouncycastle.asn1.x9.X9ObjectIdentifiers 258 | -dontwarn org.bouncycastle.cert.X509CertificateHolder 259 | -dontwarn org.bouncycastle.crypto.AlphabetMapper 260 | -dontwarn org.bouncycastle.crypto.BlockCipher 261 | -dontwarn org.bouncycastle.crypto.CipherParameters 262 | -dontwarn org.bouncycastle.crypto.CryptoException 263 | -dontwarn org.bouncycastle.crypto.Digest 264 | -dontwarn org.bouncycastle.crypto.InvalidCipherTextException 265 | -dontwarn org.bouncycastle.crypto.Mac 266 | -dontwarn org.bouncycastle.crypto.digests.SM3Digest 267 | -dontwarn org.bouncycastle.crypto.engines.SM2Engine$Mode 268 | -dontwarn org.bouncycastle.crypto.engines.SM2Engine 269 | -dontwarn org.bouncycastle.crypto.engines.SM4Engine 270 | -dontwarn org.bouncycastle.crypto.macs.CBCBlockCipherMac 271 | -dontwarn org.bouncycastle.crypto.macs.HMac 272 | -dontwarn org.bouncycastle.crypto.params.AsymmetricKeyParameter 273 | -dontwarn org.bouncycastle.crypto.params.ECDomainParameters 274 | -dontwarn org.bouncycastle.crypto.params.ECPrivateKeyParameters 275 | -dontwarn org.bouncycastle.crypto.params.ECPublicKeyParameters 276 | -dontwarn org.bouncycastle.crypto.params.KeyParameter 277 | -dontwarn org.bouncycastle.crypto.params.ParametersWithID 278 | -dontwarn org.bouncycastle.crypto.params.ParametersWithIV 279 | -dontwarn org.bouncycastle.crypto.params.ParametersWithRandom 280 | -dontwarn org.bouncycastle.crypto.signers.DSAEncoding 281 | -dontwarn org.bouncycastle.crypto.signers.PlainDSAEncoding 282 | -dontwarn org.bouncycastle.crypto.signers.SM2Signer 283 | -dontwarn org.bouncycastle.crypto.signers.StandardDSAEncoding 284 | -dontwarn org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey 285 | -dontwarn org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey 286 | -dontwarn org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util 287 | -dontwarn org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil 288 | -dontwarn org.bouncycastle.jcajce.spec.FPEParameterSpec 289 | -dontwarn org.bouncycastle.jcajce.spec.OpenSSHPrivateKeySpec 290 | -dontwarn org.bouncycastle.jcajce.spec.OpenSSHPublicKeySpec 291 | -dontwarn org.bouncycastle.jce.provider.BouncyCastleProvider 292 | -dontwarn org.bouncycastle.jce.spec.ECNamedCurveSpec 293 | -dontwarn org.bouncycastle.jce.spec.ECParameterSpec 294 | -dontwarn org.bouncycastle.jsse.BCSSLParameters 295 | -dontwarn org.bouncycastle.jsse.BCSSLSocket 296 | -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider 297 | -dontwarn org.bouncycastle.math.ec.ECCurve 298 | -dontwarn org.bouncycastle.math.ec.ECPoint 299 | -dontwarn org.bouncycastle.math.ec.FixedPointCombMultiplier 300 | -dontwarn org.bouncycastle.openssl.PEMDecryptorProvider 301 | -dontwarn org.bouncycastle.openssl.PEMEncryptedKeyPair 302 | -dontwarn org.bouncycastle.openssl.PEMException 303 | -dontwarn org.bouncycastle.openssl.PEMKeyPair 304 | -dontwarn org.bouncycastle.openssl.PEMParser 305 | -dontwarn org.bouncycastle.openssl.X509TrustedCertificateBlock 306 | -dontwarn org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter 307 | -dontwarn org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder 308 | -dontwarn org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder 309 | -dontwarn org.bouncycastle.operator.InputDecryptorProvider 310 | -dontwarn org.bouncycastle.operator.OperatorCreationException 311 | -dontwarn org.bouncycastle.pkcs.PKCS10CertificationRequest 312 | -dontwarn org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo 313 | -dontwarn org.bouncycastle.pkcs.PKCSException 314 | -dontwarn org.bouncycastle.util.Arrays 315 | -dontwarn org.bouncycastle.util.BigIntegers 316 | -dontwarn org.bouncycastle.util.encoders.Hex 317 | -dontwarn org.bouncycastle.util.io.pem.PemObject 318 | -dontwarn org.bouncycastle.util.io.pem.PemObjectGenerator 319 | -dontwarn org.bouncycastle.util.io.pem.PemReader 320 | -dontwarn org.bouncycastle.util.io.pem.PemWriter 321 | -dontwarn org.conscrypt.Conscrypt$Version 322 | -dontwarn org.conscrypt.Conscrypt 323 | -dontwarn org.conscrypt.ConscryptHostnameVerifier 324 | -dontwarn org.openjsse.javax.net.ssl.SSLParameters 325 | -dontwarn org.openjsse.javax.net.ssl.SSLSocket 326 | -dontwarn org.openjsse.net.ssl.OpenJSSE 327 | -dontwarn org.slf4j.impl.StaticLoggerBinder 328 | -dontwarn org.slf4j.impl.StaticMDCBinder -------------------------------------------------------------------------------- /app/src/main/java/com/github/jing332/image_processor/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.jing332.image_processor.ui 2 | 3 | import android.content.Intent 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.net.Uri 7 | import android.os.Bundle 8 | import androidx.activity.ComponentActivity 9 | import androidx.activity.compose.BackHandler 10 | import androidx.activity.compose.rememberLauncherForActivityResult 11 | import androidx.activity.compose.setContent 12 | import androidx.activity.result.contract.ActivityResultContracts 13 | import androidx.compose.animation.Crossfade 14 | import androidx.compose.animation.core.tween 15 | import androidx.compose.foundation.clickable 16 | import androidx.compose.foundation.interaction.MutableInteractionSource 17 | import androidx.compose.foundation.layout.Arrangement 18 | import androidx.compose.foundation.layout.Box 19 | import androidx.compose.foundation.layout.Column 20 | import androidx.compose.foundation.layout.Row 21 | import androidx.compose.foundation.layout.Spacer 22 | import androidx.compose.foundation.layout.fillMaxHeight 23 | import androidx.compose.foundation.layout.fillMaxSize 24 | import androidx.compose.foundation.layout.fillMaxWidth 25 | import androidx.compose.foundation.layout.height 26 | import androidx.compose.foundation.layout.imePadding 27 | import androidx.compose.foundation.layout.padding 28 | import androidx.compose.foundation.layout.size 29 | import androidx.compose.foundation.lazy.LazyColumn 30 | import androidx.compose.foundation.lazy.items 31 | import androidx.compose.foundation.text.KeyboardOptions 32 | import androidx.compose.material.icons.Icons 33 | import androidx.compose.material.icons.filled.ErrorOutline 34 | import androidx.compose.material.icons.filled.FileOpen 35 | import androidx.compose.material.icons.filled.MoreVert 36 | import androidx.compose.material.ripple.rememberRipple 37 | import androidx.compose.material3.AlertDialog 38 | import androidx.compose.material3.Divider 39 | import androidx.compose.material3.DropdownMenu 40 | import androidx.compose.material3.DropdownMenuItem 41 | import androidx.compose.material3.ElevatedButton 42 | import androidx.compose.material3.ExperimentalMaterial3Api 43 | import androidx.compose.material3.Icon 44 | import androidx.compose.material3.IconButton 45 | import androidx.compose.material3.LinearProgressIndicator 46 | import androidx.compose.material3.MaterialTheme 47 | import androidx.compose.material3.OutlinedTextField 48 | import androidx.compose.material3.Scaffold 49 | import androidx.compose.material3.Text 50 | import androidx.compose.material3.TextButton 51 | import androidx.compose.material3.TopAppBar 52 | import androidx.compose.runtime.Composable 53 | import androidx.compose.runtime.getValue 54 | import androidx.compose.runtime.mutableIntStateOf 55 | import androidx.compose.runtime.mutableStateOf 56 | import androidx.compose.runtime.remember 57 | import androidx.compose.runtime.rememberCoroutineScope 58 | import androidx.compose.runtime.setValue 59 | import androidx.compose.ui.Alignment 60 | import androidx.compose.ui.Modifier 61 | import androidx.compose.ui.draw.clip 62 | import androidx.compose.ui.graphics.ImageBitmap 63 | import androidx.compose.ui.graphics.asImageBitmap 64 | import androidx.compose.ui.platform.LocalContext 65 | import androidx.compose.ui.res.stringResource 66 | import androidx.compose.ui.text.font.FontStyle 67 | import androidx.compose.ui.text.font.FontWeight 68 | import androidx.compose.ui.text.input.KeyboardType 69 | import androidx.compose.ui.text.style.TextOverflow 70 | import androidx.compose.ui.unit.dp 71 | import androidx.core.net.toUri 72 | import androidx.core.view.WindowCompat 73 | import androidx.lifecycle.viewmodel.compose.viewModel 74 | import coil.compose.AsyncImage 75 | import com.github.jing332.image_processor.R 76 | import com.github.jing332.image_processor.help.AppConfig 77 | import com.github.jing332.image_processor.ui.theme.ImageConvTheme 78 | import com.github.jing332.image_processor.ui.widgets.DropMenuTextField 79 | import com.github.jing332.image_processor.utils.ASFUriUtils.getPath 80 | import com.github.jing332.text_searcher.ui.widgets.LabelSlider 81 | import com.github.jing332.image_processor.ui.widgets.TransparentSystemBars 82 | import com.origeek.imageViewer.viewer.ImageViewer 83 | import com.origeek.imageViewer.viewer.rememberViewerState 84 | 85 | class MainActivity : ComponentActivity() { 86 | @OptIn(ExperimentalMaterial3Api::class) 87 | override fun onCreate(savedInstanceState: Bundle?) { 88 | super.onCreate(savedInstanceState) 89 | 90 | WindowCompat.setDecorFitsSystemWindows(window, false) 91 | setContent { 92 | ImageConvTheme { 93 | TransparentSystemBars() 94 | var previewImageBitmap by remember { mutableStateOf(null) } 95 | var showPreviewImage by remember { mutableStateOf(false) } 96 | 97 | val scope = rememberCoroutineScope() 98 | // 渐入渐出 99 | Crossfade( 100 | targetState = showPreviewImage, animationSpec = tween(500), label = "" 101 | ) { isShow -> 102 | if (isShow) { // 预览大图 103 | BackHandler { showPreviewImage = false } 104 | val state = rememberViewerState() 105 | ImageViewer( 106 | state = state, 107 | model = previewImageBitmap ?: ImageBitmap(20, 20), 108 | modifier = Modifier.fillMaxSize(), 109 | ) 110 | } else { 111 | var showAboutDialog by remember { mutableStateOf(false) } 112 | if (showAboutDialog) 113 | AboutDialog { showAboutDialog = false } 114 | 115 | var showFolderEditDialog by remember { mutableStateOf(false) } 116 | if (showFolderEditDialog) { 117 | var folderName by remember { mutableStateOf(AppConfig.targetFolderName.value) } 118 | AlertDialog( 119 | onDismissRequest = { showFolderEditDialog = false }, 120 | confirmButton = { 121 | TextButton(onClick = { 122 | AppConfig.targetFolderName.value = folderName 123 | showFolderEditDialog = false 124 | }) { 125 | Text(stringResource(android.R.string.ok)) 126 | } 127 | }, 128 | text = { 129 | OutlinedTextField( 130 | value = folderName, 131 | onValueChange = { folderName = it }, 132 | label = { Text(stringResource(id = R.string.edit_target_folder)) } 133 | ) 134 | }, 135 | title = { 136 | Text(stringResource(R.string.edit_target_folder)) 137 | } 138 | ) 139 | } 140 | Scaffold( 141 | modifier = Modifier.imePadding(), 142 | topBar = { 143 | TopAppBar(title = { Text(stringResource(id = R.string.app_name)) }, 144 | actions = { 145 | var showMoreOptions by remember { 146 | mutableStateOf( 147 | false 148 | ) 149 | } 150 | DropdownMenu( 151 | expanded = showMoreOptions, 152 | onDismissRequest = { 153 | showMoreOptions = false 154 | }) { 155 | DropdownMenuItem( 156 | text = { Text(stringResource(id = R.string.about)) }, 157 | onClick = { 158 | showAboutDialog = true 159 | showMoreOptions = false 160 | } 161 | ) 162 | 163 | DropdownMenuItem( 164 | text = { Text(stringResource(id = R.string.edit_target_folder)) }, 165 | onClick = { 166 | showFolderEditDialog = true 167 | showMoreOptions = false 168 | } 169 | ) 170 | } 171 | 172 | IconButton(onClick = { 173 | showMoreOptions = true 174 | }) { 175 | Icon( 176 | Icons.Default.MoreVert, 177 | contentDescription = stringResource(R.string.more_options) 178 | ) 179 | } 180 | }) 181 | } 182 | ) { 183 | ProcessorScreen( 184 | Modifier 185 | .padding(it) 186 | .fillMaxHeight() 187 | .padding(start = 8.dp, end = 8.dp, bottom = 8.dp), 188 | previewImage = { 189 | previewImageBitmap = it 190 | showPreviewImage = true 191 | } 192 | ) 193 | } 194 | } 195 | } 196 | } 197 | } 198 | } 199 | } 200 | 201 | @Composable 202 | fun ProcessorScreen( 203 | modifier: Modifier, 204 | vm: ProcessorViewModel = viewModel(), 205 | previewImage: (ImageBitmap) -> Unit 206 | ) { 207 | val context = LocalContext.current 208 | var lastSrcDir by remember { AppConfig.sourceDirectory } 209 | 210 | val dirSelection = 211 | rememberLauncherForActivityResult(contract = ActivityResultContracts.OpenDocumentTree()) { uri -> 212 | if (uri != null) { 213 | context.contentResolver.takePersistableUriPermission( 214 | uri, 215 | Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION 216 | ) 217 | vm.srcDir = uri.toString() 218 | lastSrcDir = vm.srcDir 219 | 220 | vm.loadDir(context, uri) 221 | } 222 | } 223 | 224 | Column(modifier = modifier) { 225 | OutlinedTextField( 226 | modifier = Modifier.fillMaxWidth(), 227 | value = context.getPath( 228 | vm.srcDir.toUri(), true 229 | ) ?: vm.srcDir, 230 | onValueChange = { vm.srcDir = it }, 231 | label = { Text(stringResource(R.string.source_directory)) }, 232 | readOnly = true, 233 | trailingIcon = { 234 | IconButton(onClick = { dirSelection.launch(null) }) { 235 | Icon( 236 | Icons.Filled.FileOpen, 237 | contentDescription = stringResource(R.string.select_folder) 238 | ) 239 | } 240 | }, 241 | placeholder = { Text(stringResource(R.string.click_button_select_dir)) } 242 | ) 243 | 244 | if (vm.srcDir.isEmpty() && lastSrcDir.isNotEmpty()) { 245 | Box( 246 | Modifier 247 | .clip(MaterialTheme.shapes.small) 248 | .clickable( 249 | interactionSource = remember { MutableInteractionSource() }, 250 | indication = rememberRipple() 251 | ) { 252 | vm.srcDir = lastSrcDir 253 | vm.loadDir(context, lastSrcDir.toUri()) 254 | }, 255 | ) { 256 | Text( 257 | modifier = Modifier.padding(2.dp), 258 | text = stringResource( 259 | R.string.last_directory, 260 | context.getPath(lastSrcDir.toUri(), true) ?: lastSrcDir 261 | ), 262 | color = MaterialTheme.colorScheme.primary, 263 | fontWeight = FontWeight.Bold, 264 | fontStyle = FontStyle.Italic, 265 | overflow = TextOverflow.Visible, 266 | ) 267 | } 268 | } 269 | 270 | 271 | LazyColumn(Modifier.weight(1f)) { 272 | items(vm.files, { it.uriString }) { file -> 273 | FileItemScreen( 274 | uri = file.uriString.toUri(), 275 | name = file.name, 276 | state = file.processState, 277 | size = file.size, 278 | ) { 279 | val img = BitmapFactory.decodeStream( 280 | context.contentResolver.openInputStream(file.uri) 281 | ) 282 | previewImage(img.asImageBitmap()) 283 | } 284 | Divider( 285 | Modifier 286 | .height(0.8.dp) 287 | .padding(horizontal = 4.dp) 288 | ) 289 | } 290 | } 291 | 292 | Column { 293 | if (vm.process > 0) 294 | LinearProgressIndicator(progress = vm.process, modifier = Modifier.fillMaxWidth()) 295 | 296 | Row() { 297 | DropMenuTextField( 298 | modifier = Modifier 299 | .weight(1f) 300 | .padding(horizontal = 2.dp), 301 | label = { Text(stringResource(R.string.target_format)) }, 302 | key = vm.format, 303 | keys = Bitmap.CompressFormat.values().map { it.name }, 304 | values = Bitmap.CompressFormat.values().map { it.name }, 305 | onKeyChange = { 306 | vm.format = it.toString() 307 | } 308 | ) 309 | 310 | OutlinedTextField( 311 | modifier = Modifier 312 | .weight(1f) 313 | .padding(horizontal = 2.dp), 314 | value = vm.width, 315 | onValueChange = { vm.width = it }, 316 | label = { Text(stringResource(R.string.width)) }, 317 | ) 318 | 319 | Spacer(modifier = Modifier.height(4.dp)) 320 | 321 | OutlinedTextField( 322 | modifier = Modifier 323 | .weight(1f) 324 | .padding(horizontal = 2.dp), 325 | value = vm.height, 326 | onValueChange = { 327 | vm.height = it 328 | }, 329 | label = { Text(stringResource(R.string.height)) }, 330 | keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) 331 | ) 332 | } 333 | 334 | var quality by remember { mutableIntStateOf(100) } 335 | if (vm.format != Bitmap.CompressFormat.PNG.name) 336 | LabelSlider( 337 | value = quality.toFloat(), 338 | onValueChange = { quality = it.toInt() }, 339 | valueRange = 0f..100f 340 | ) { 341 | Text(stringResource(R.string.image_quality, quality)) 342 | } 343 | 344 | var showWarnDialog by remember { mutableStateOf(false) } 345 | if (showWarnDialog) 346 | ExecuteWarnDialog(onDismissRequest = { showWarnDialog = false }) { 347 | vm.executeConv( 348 | context, 349 | srcUri = vm.srcDir.toUri(), 350 | format = Bitmap.CompressFormat.valueOf(vm.format), 351 | quality = quality, 352 | width = try { 353 | vm.width.toInt() 354 | } catch (_: Exception) { 355 | 0 356 | }, 357 | height = try { 358 | vm.height.toInt() 359 | } catch (_: Exception) { 360 | 0 361 | }, 362 | folderName = AppConfig.targetFolderName.value, 363 | ) 364 | } 365 | 366 | ElevatedButton( 367 | enabled = !vm.running, 368 | modifier = Modifier.fillMaxWidth(), 369 | onClick = { 370 | showWarnDialog = true 371 | }) { 372 | Text("执行", fontWeight = FontWeight.Bold) 373 | } 374 | } 375 | } 376 | } 377 | 378 | @Composable 379 | fun ExecuteWarnDialog(onDismissRequest: () -> Unit, onStart: () -> Unit) { 380 | AlertDialog( 381 | onDismissRequest = onDismissRequest, 382 | title = { 383 | Row { 384 | Icon( 385 | modifier = Modifier.align(Alignment.CenterVertically), 386 | imageVector = Icons.Filled.ErrorOutline, 387 | contentDescription = null 388 | ) 389 | Text(stringResource(R.string.warn)) 390 | } 391 | }, 392 | text = { 393 | Text( 394 | stringResource(R.string.warn_msg, AppConfig.targetFolderName.value), 395 | style = MaterialTheme.typography.titleMedium 396 | ) 397 | }, 398 | confirmButton = { 399 | TextButton(onClick = { 400 | onDismissRequest() 401 | onStart() 402 | }) { 403 | Text(stringResource(R.string.start)) 404 | } 405 | } 406 | ) 407 | } 408 | 409 | @Composable 410 | fun FileItemScreen( 411 | uri: Uri, 412 | name: String, 413 | state: ProcessState, 414 | size: String, 415 | onClick: () -> Unit 416 | ) { 417 | Row( 418 | modifier = Modifier 419 | .clip(MaterialTheme.shapes.extraSmall) 420 | .clickable( 421 | interactionSource = remember { MutableInteractionSource() }, 422 | indication = rememberRipple(), 423 | onClick = onClick 424 | ) 425 | .padding(horizontal = 4.dp, vertical = 8.dp), 426 | ) { 427 | AsyncImage(model = uri, contentDescription = "Image", Modifier.size(48.dp)) 428 | 429 | Column(modifier = Modifier.align(Alignment.CenterVertically)) { 430 | Text(text = name, style = MaterialTheme.typography.titleMedium) 431 | 432 | Row( 433 | horizontalArrangement = Arrangement.SpaceBetween, 434 | modifier = Modifier.fillMaxWidth() 435 | ) { 436 | Text( 437 | text = when (state) { 438 | is ProcessState.DONE -> "完成" 439 | is ProcessState.ERROR -> { 440 | "错误: ${state.t}" 441 | } 442 | 443 | is ProcessState.PROCESSING -> "处理中" 444 | 445 | else -> "就绪" 446 | }, 447 | style = MaterialTheme.typography.titleSmall, 448 | color = when (state) { 449 | is ProcessState.DONE -> MaterialTheme.colorScheme.primary 450 | is ProcessState.ERROR -> MaterialTheme.colorScheme.error 451 | is ProcessState.PROCESSING -> MaterialTheme.colorScheme.secondary 452 | 453 | else -> MaterialTheme.colorScheme.tertiary 454 | }, 455 | fontStyle = FontStyle.Italic, 456 | ) 457 | 458 | Text( 459 | text = size, 460 | style = MaterialTheme.typography.titleSmall, 461 | modifier = Modifier.align(Alignment.CenterVertically) 462 | ) 463 | } 464 | } 465 | } 466 | } --------------------------------------------------------------------------------