├── 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 |
5 |
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 |
8 |
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 | 
2 | 
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------