From 561c9de3aaf138ffca0d208bea59c33c1e6c223f Mon Sep 17 00:00:00 2001
From: love19870821 <59876241+love19870821@users.noreply.github.com>
Date: Tue, 10 Feb 2026 12:40:16 +0800
Subject: [PATCH] Initial multitoolbox app
---
README.md | 60 ++++
app/build.gradle.kts | 90 ++++++
app/src/main/AndroidManifest.xml | 26 ++
.../com/toolbox/multitool/MainActivity.kt | 31 ++
.../java/com/toolbox/multitool/ToolboxApp.kt | 7 +
.../toolbox/multitool/data/db/AppDatabase.kt | 15 +
.../multitool/data/db/CalculatorHistoryDao.kt | 17 ++
.../data/db/CalculatorHistoryEntity.kt | 12 +
.../multitool/data/db/CurrencyRateDao.kt | 15 +
.../multitool/data/db/CurrencyRateEntity.kt | 11 +
.../toolbox/multitool/data/db/SettingsDao.kt | 15 +
.../multitool/data/db/SettingsEntity.kt | 13 +
.../multitool/data/network/CurrencyApi.kt | 9 +
.../data/network/CurrencyResponse.kt | 9 +
.../data/repository/CalculatorRepository.kt | 23 ++
.../data/repository/CurrencyRepository.kt | 50 ++++
.../data/repository/SettingsRepository.kt | 13 +
.../com/toolbox/multitool/di/AppModule.kt | 72 +++++
.../usecase/CalculateExpressionUseCase.kt | 16 +
.../usecase/CurrencyConversionUseCase.kt | 11 +
.../domain/usecase/UnitConversionUseCase.kt | 44 +++
.../com/toolbox/multitool/ui/Navigation.kt | 47 +++
.../toolbox/multitool/ui/home/HomeScreen.kt | 57 ++++
.../multitool/ui/image/ImageToolboxScreen.kt | 276 ++++++++++++++++++
.../ui/image/ImageToolboxViewModel.kt | 95 ++++++
.../multitool/ui/settings/SettingsScreen.kt | 55 ++++
.../ui/settings/SettingsViewModel.kt | 32 ++
.../multitool/ui/utilities/UtilitiesScreen.kt | 219 ++++++++++++++
.../ui/utilities/UtilitiesViewModel.kt | 105 +++++++
.../multitool/ui/video/VideoToolboxScreen.kt | 117 ++++++++
.../ui/video/VideoToolboxViewModel.kt | 77 +++++
.../com/toolbox/multitool/util/BitmapUtils.kt | 54 ++++
.../multitool/util/ImageComposition.kt | 68 +++++
.../toolbox/multitool/util/ImageProcessing.kt | 87 ++++++
app/src/main/res/values/strings.xml | 3 +
app/src/main/res/values/themes.xml | 6 +
.../toolbox/multitool/UtilitiesUseCaseTest.kt | 35 +++
build.gradle.kts | 6 +
gradle.properties | 4 +
gradle/wrapper/gradle-wrapper.properties | 5 +
gradlew | 275 +++++++++++++++++
gradlew.bat | 85 ++++++
settings.gradle.kts | 18 ++
43 files changed, 2285 insertions(+)
create mode 100644 README.md
create mode 100644 app/build.gradle.kts
create mode 100644 app/src/main/AndroidManifest.xml
create mode 100644 app/src/main/java/com/toolbox/multitool/MainActivity.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/ToolboxApp.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/data/db/AppDatabase.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/data/db/CalculatorHistoryDao.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/data/db/CalculatorHistoryEntity.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/data/db/CurrencyRateDao.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/data/db/CurrencyRateEntity.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/data/db/SettingsDao.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/data/db/SettingsEntity.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/data/network/CurrencyApi.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/data/network/CurrencyResponse.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/data/repository/CalculatorRepository.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/data/repository/CurrencyRepository.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/data/repository/SettingsRepository.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/di/AppModule.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/domain/usecase/CalculateExpressionUseCase.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/domain/usecase/CurrencyConversionUseCase.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/domain/usecase/UnitConversionUseCase.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/ui/Navigation.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/ui/home/HomeScreen.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/ui/image/ImageToolboxScreen.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/ui/image/ImageToolboxViewModel.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/ui/settings/SettingsScreen.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/ui/settings/SettingsViewModel.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/ui/utilities/UtilitiesScreen.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/ui/utilities/UtilitiesViewModel.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/ui/video/VideoToolboxScreen.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/ui/video/VideoToolboxViewModel.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/util/BitmapUtils.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/util/ImageComposition.kt
create mode 100644 app/src/main/java/com/toolbox/multitool/util/ImageProcessing.kt
create mode 100644 app/src/main/res/values/strings.xml
create mode 100644 app/src/main/res/values/themes.xml
create mode 100644 app/src/test/java/com/toolbox/multitool/UtilitiesUseCaseTest.kt
create mode 100644 build.gradle.kts
create mode 100644 gradle.properties
create mode 100644 gradle/wrapper/gradle-wrapper.properties
create mode 100755 gradlew
create mode 100644 gradlew.bat
create mode 100644 settings.gradle.kts
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7167f43
--- /dev/null
+++ b/README.md
@@ -0,0 +1,60 @@
+# 多功能工具箱 (Kotlin/Compose)
+
+本專案為 Android 10+ (API 29+) 的多功能工具箱示範,整合影像 OCR、圖片處理、影片裁剪、下載管理器,以及常用小工具。
+
+## 技術選型
+- Kotlin + Jetpack Compose
+- MVVM + UseCase
+- Hilt DI
+- Room (設定/歷史/匯率快取)
+- Google ML Kit 繁中 OCR
+- OpenCV (去噪/銳化/對比)
+- Media3 Transformer (影片裁剪)
+- OkHttp + Retrofit (匯率 API + 快取)
+
+## 專案結構
+```
+app/src/main/java/com/toolbox/multitool
+├── data
+│ ├── db
+│ ├── network
+│ └── repository
+├── di
+├── domain
+│ └── usecase
+├── ui
+│ ├── home
+│ ├── image
+│ ├── settings
+│ ├── utilities
+│ └── video
+└── util
+```
+
+## Build / Run
+```bash
+./gradlew assembleDebug
+```
+
+如需安裝至裝置:
+```bash
+./gradlew installDebug
+```
+
+## APK 打包
+```bash
+./gradlew assembleRelease
+```
+
+輸出路徑:
+`app/build/outputs/apk/`
+
+## 權限說明
+- 相機:拍照 OCR
+- 讀取媒體:選圖/選影片
+- 網路:匯率 API 與直連下載
+- MediaStore 寫入:輸出圖片與影片
+
+## 注意事項
+- 影片功能僅提供本機匯入裁剪與直連 URL 下載 (DownloadManager)
+- 圖片/影片處理皆在本機端完成
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..4b4323e
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,90 @@
+import org.gradle.api.JavaVersion
+
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+ id("com.google.dagger.hilt.android")
+ id("com.google.devtools.ksp")
+}
+
+android {
+ namespace = "com.toolbox.multitool"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.toolbox.multitool"
+ minSdk = 29
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0.0"
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.10"
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+ val composeBom = platform("androidx.compose:compose-bom:2024.02.01")
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+
+ implementation("androidx.core:core-ktx:1.12.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
+ implementation("androidx.activity:activity-compose:1.8.2")
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.material3:material3:1.2.0")
+ implementation("androidx.navigation:navigation-compose:2.7.7")
+ implementation("androidx.compose.material:material:1.6.2")
+
+ implementation("com.google.dagger:hilt-android:2.48")
+ ksp("com.google.dagger:hilt-android-compiler:2.48")
+ implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
+
+ implementation("androidx.room:room-runtime:2.6.1")
+ implementation("androidx.room:room-ktx:2.6.1")
+ ksp("androidx.room:room-compiler:2.6.1")
+
+ implementation("com.squareup.retrofit2:retrofit:2.11.0")
+ implementation("com.squareup.retrofit2:converter-moshi:2.11.0")
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+ implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
+
+ implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
+
+ implementation("com.google.mlkit:text-recognition-chinese:16.0.0")
+
+ implementation("androidx.media3:media3-transformer:1.3.1")
+ implementation("androidx.media3:media3-common:1.3.1")
+
+ implementation("org.opencv:opencv-android:4.8.0")
+
+ implementation("net.objecthunter:exp4j:0.4.8")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
+
+ testImplementation("junit:junit:4.13.2")
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..2fee2a6
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/toolbox/multitool/MainActivity.kt b/app/src/main/java/com/toolbox/multitool/MainActivity.kt
new file mode 100644
index 0000000..019b768
--- /dev/null
+++ b/app/src/main/java/com/toolbox/multitool/MainActivity.kt
@@ -0,0 +1,31 @@
+package com.toolbox.multitool
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+import com.toolbox.multitool.ui.NavigationRoot
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ MaterialTheme {
+ Surface {
+ NavigationRoot()
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun MainPreview() {
+ MaterialTheme {
+ NavigationRoot()
+ }
+}
diff --git a/app/src/main/java/com/toolbox/multitool/ToolboxApp.kt b/app/src/main/java/com/toolbox/multitool/ToolboxApp.kt
new file mode 100644
index 0000000..0f14574
--- /dev/null
+++ b/app/src/main/java/com/toolbox/multitool/ToolboxApp.kt
@@ -0,0 +1,7 @@
+package com.toolbox.multitool
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class ToolboxApp : Application()
diff --git a/app/src/main/java/com/toolbox/multitool/data/db/AppDatabase.kt b/app/src/main/java/com/toolbox/multitool/data/db/AppDatabase.kt
new file mode 100644
index 0000000..f1b2e48
--- /dev/null
+++ b/app/src/main/java/com/toolbox/multitool/data/db/AppDatabase.kt
@@ -0,0 +1,15 @@
+package com.toolbox.multitool.data.db
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+
+@Database(
+ entities = [CalculatorHistoryEntity::class, SettingsEntity::class, CurrencyRateEntity::class],
+ version = 1,
+ exportSchema = false
+)
+abstract class AppDatabase : RoomDatabase() {
+ abstract fun calculatorHistoryDao(): CalculatorHistoryDao
+ abstract fun settingsDao(): SettingsDao
+ abstract fun currencyRateDao(): CurrencyRateDao
+}
diff --git a/app/src/main/java/com/toolbox/multitool/data/db/CalculatorHistoryDao.kt b/app/src/main/java/com/toolbox/multitool/data/db/CalculatorHistoryDao.kt
new file mode 100644
index 0000000..10cec99
--- /dev/null
+++ b/app/src/main/java/com/toolbox/multitool/data/db/CalculatorHistoryDao.kt
@@ -0,0 +1,17 @@
+package com.toolbox.multitool.data.db
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.Query
+
+@Dao
+interface CalculatorHistoryDao {
+ @Query("SELECT * FROM calculator_history ORDER BY createdAt DESC")
+ suspend fun getAll(): List
+
+ @Insert
+ suspend fun insert(entity: CalculatorHistoryEntity)
+
+ @Query("DELETE FROM calculator_history")
+ suspend fun clear()
+}
diff --git a/app/src/main/java/com/toolbox/multitool/data/db/CalculatorHistoryEntity.kt b/app/src/main/java/com/toolbox/multitool/data/db/CalculatorHistoryEntity.kt
new file mode 100644
index 0000000..30bb972
--- /dev/null
+++ b/app/src/main/java/com/toolbox/multitool/data/db/CalculatorHistoryEntity.kt
@@ -0,0 +1,12 @@
+package com.toolbox.multitool.data.db
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "calculator_history")
+data class CalculatorHistoryEntity(
+ @PrimaryKey(autoGenerate = true) val id: Long = 0,
+ val expression: String,
+ val result: String,
+ val createdAt: Long
+)
diff --git a/app/src/main/java/com/toolbox/multitool/data/db/CurrencyRateDao.kt b/app/src/main/java/com/toolbox/multitool/data/db/CurrencyRateDao.kt
new file mode 100644
index 0000000..7e767e6
--- /dev/null
+++ b/app/src/main/java/com/toolbox/multitool/data/db/CurrencyRateDao.kt
@@ -0,0 +1,15 @@
+package com.toolbox.multitool.data.db
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+
+@Dao
+interface CurrencyRateDao {
+ @Query("SELECT * FROM currency_rates WHERE base = :base")
+ suspend fun getByBase(base: String): CurrencyRateEntity?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun upsert(entity: CurrencyRateEntity)
+}
diff --git a/app/src/main/java/com/toolbox/multitool/data/db/CurrencyRateEntity.kt b/app/src/main/java/com/toolbox/multitool/data/db/CurrencyRateEntity.kt
new file mode 100644
index 0000000..d58c5be
--- /dev/null
+++ b/app/src/main/java/com/toolbox/multitool/data/db/CurrencyRateEntity.kt
@@ -0,0 +1,11 @@
+package com.toolbox.multitool.data.db
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "currency_rates")
+data class CurrencyRateEntity(
+ @PrimaryKey val base: String,
+ val updatedAt: Long,
+ val ratesJson: String
+)
diff --git a/app/src/main/java/com/toolbox/multitool/data/db/SettingsDao.kt b/app/src/main/java/com/toolbox/multitool/data/db/SettingsDao.kt
new file mode 100644
index 0000000..c0e4f55
--- /dev/null
+++ b/app/src/main/java/com/toolbox/multitool/data/db/SettingsDao.kt
@@ -0,0 +1,15 @@
+package com.toolbox.multitool.data.db
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+
+@Dao
+interface SettingsDao {
+ @Query("SELECT * FROM settings WHERE id = 1")
+ suspend fun getSettings(): SettingsEntity?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun upsert(entity: SettingsEntity)
+}
diff --git a/app/src/main/java/com/toolbox/multitool/data/db/SettingsEntity.kt b/app/src/main/java/com/toolbox/multitool/data/db/SettingsEntity.kt
new file mode 100644
index 0000000..0ccc24b
--- /dev/null
+++ b/app/src/main/java/com/toolbox/multitool/data/db/SettingsEntity.kt
@@ -0,0 +1,13 @@
+package com.toolbox.multitool.data.db
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "settings")
+data class SettingsEntity(
+ @PrimaryKey val id: Int = 1,
+ val outputFormat: String = "PNG",
+ val defaultExportFolder: String = "多功能工具箱",
+ val morningTextSize: Float = 32f,
+ val morningTextColor: String = "#FFFFFF"
+)
diff --git a/app/src/main/java/com/toolbox/multitool/data/network/CurrencyApi.kt b/app/src/main/java/com/toolbox/multitool/data/network/CurrencyApi.kt
new file mode 100644
index 0000000..0b1bfed
--- /dev/null
+++ b/app/src/main/java/com/toolbox/multitool/data/network/CurrencyApi.kt
@@ -0,0 +1,9 @@
+package com.toolbox.multitool.data.network
+
+import retrofit2.http.GET
+import retrofit2.http.Path
+
+interface CurrencyApi {
+ @GET("v6/latest/{base}")
+ suspend fun latest(@Path("base") base: String): CurrencyResponse
+}
diff --git a/app/src/main/java/com/toolbox/multitool/data/network/CurrencyResponse.kt b/app/src/main/java/com/toolbox/multitool/data/network/CurrencyResponse.kt
new file mode 100644
index 0000000..c591c71
--- /dev/null
+++ b/app/src/main/java/com/toolbox/multitool/data/network/CurrencyResponse.kt
@@ -0,0 +1,9 @@
+package com.toolbox.multitool.data.network
+
+import com.squareup.moshi.Json
+
+data class CurrencyResponse(
+ @Json(name = "time_last_update_unix") val updatedAt: Long,
+ @Json(name = "base_code") val baseCode: String,
+ val rates: Map
+)
diff --git a/app/src/main/java/com/toolbox/multitool/data/repository/CalculatorRepository.kt b/app/src/main/java/com/toolbox/multitool/data/repository/CalculatorRepository.kt
new file mode 100644
index 0000000..918df18
--- /dev/null
+++ b/app/src/main/java/com/toolbox/multitool/data/repository/CalculatorRepository.kt
@@ -0,0 +1,23 @@
+package com.toolbox.multitool.data.repository
+
+import com.toolbox.multitool.data.db.CalculatorHistoryDao
+import com.toolbox.multitool.data.db.CalculatorHistoryEntity
+import javax.inject.Inject
+
+class CalculatorRepository @Inject constructor(
+ private val dao: CalculatorHistoryDao
+) {
+ suspend fun getHistory(): List = dao.getAll()
+
+ suspend fun insertHistory(expression: String, result: String) {
+ dao.insert(
+ CalculatorHistoryEntity(
+ expression = expression,
+ result = result,
+ createdAt = System.currentTimeMillis()
+ )
+ )
+ }
+
+ suspend fun clearHistory() = dao.clear()
+}
diff --git a/app/src/main/java/com/toolbox/multitool/data/repository/CurrencyRepository.kt b/app/src/main/java/com/toolbox/multitool/data/repository/CurrencyRepository.kt
new file mode 100644
index 0000000..f35e186
--- /dev/null
+++ b/app/src/main/java/com/toolbox/multitool/data/repository/CurrencyRepository.kt
@@ -0,0 +1,50 @@
+package com.toolbox.multitool.data.repository
+
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.Types
+import com.toolbox.multitool.data.db.CurrencyRateDao
+import com.toolbox.multitool.data.db.CurrencyRateEntity
+import com.toolbox.multitool.data.network.CurrencyApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+class CurrencyRepository @Inject constructor(
+ private val api: CurrencyApi,
+ private val dao: CurrencyRateDao,
+ private val moshi: Moshi
+) {
+ private val mapAdapter = moshi.adapter