├── .fleet └── receipt.json ├── .gitignore ├── README.md ├── build.gradle.kts ├── composeApp ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ ├── kotlin │ │ ├── Platform.android.kt │ │ └── com │ │ │ └── tangping │ │ │ └── zhoujiang │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.webp │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── values-v27 │ │ └── styles.xml │ │ ├── values-v29 │ │ └── styles.xml │ │ ├── values │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── network_security_config.xml │ ├── commonMain │ ├── composeResources │ │ ├── drawable │ │ │ ├── compose-multiplatform.xml │ │ │ ├── ic_add.xml │ │ │ ├── ic_arrow_down.xml │ │ │ ├── ic_auto.xml │ │ │ ├── ic_back.xml │ │ │ ├── ic_close.xml │ │ │ ├── ic_copy.xml │ │ │ ├── ic_countdown.xml │ │ │ ├── ic_deposit.xml │ │ │ ├── ic_deposit_goal.xml │ │ │ ├── ic_details.xml │ │ │ ├── ic_empty.xml │ │ │ ├── ic_export.xml │ │ │ ├── ic_history_overtime.xml │ │ │ ├── ic_history_run.xml │ │ │ ├── ic_logout.xml │ │ │ ├── ic_memo.xml │ │ │ ├── ic_milestone.xml │ │ │ ├── ic_next.xml │ │ │ ├── ic_overtime.xml │ │ │ ├── ic_pin.xml │ │ │ ├── ic_prev.xml │ │ │ ├── ic_schedule.xml │ │ │ ├── ic_server.xml │ │ │ ├── ic_settings.xml │ │ │ ├── ic_sync.xml │ │ │ ├── ic_tick.xml │ │ │ ├── ic_time_card.xml │ │ │ ├── ic_todo.xml │ │ │ ├── ic_todo_finished.xml │ │ │ ├── ic_work_enough.xml │ │ │ ├── ic_working.xml │ │ │ └── ic_zhou.xml │ │ └── values │ │ │ └── strings.xml │ └── kotlin │ │ ├── App.kt │ │ ├── Platform.kt │ │ ├── constant │ │ ├── RouteConstants.kt │ │ ├── TabConstants.kt │ │ └── TimeConstants.kt │ │ ├── extension │ │ ├── ModifierExt.kt │ │ ├── NumberExt.kt │ │ ├── StringExtension.kt │ │ └── TimeExt.kt │ │ ├── global │ │ ├── AppColors.kt │ │ └── AppTheme.kt │ │ ├── helper │ │ ├── DepositHelper.kt │ │ ├── MemoHelper.kt │ │ ├── NetworkHelper.kt │ │ ├── ScheduleHelper.kt │ │ ├── SyncHelper.kt │ │ ├── TimeCardHelper.kt │ │ ├── WorkHoursHelper.kt │ │ └── effect │ │ │ ├── BaseEffectObserver.kt │ │ │ ├── EffectHelper.kt │ │ │ └── EffectObservers.kt │ │ ├── model │ │ ├── DoubleToLongSerializer.kt │ │ ├── calendar │ │ │ └── MonthDay.kt │ │ ├── display │ │ │ └── MemoDisplayItem.kt │ │ ├── records │ │ │ ├── DepositRecords.kt │ │ │ ├── Goals.kt │ │ │ ├── MemoRecords.kt │ │ │ ├── ScheduleRecords.kt │ │ │ └── TimeCardRecords.kt │ │ └── request │ │ │ ├── DepositSyncRequest.kt │ │ │ ├── LoginRequest.kt │ │ │ ├── MemoSyncRequest.kt │ │ │ ├── RegisterRequest.kt │ │ │ ├── ScheduleSyncRequest.kt │ │ │ └── TimeCardSyncRequest.kt │ │ ├── store │ │ ├── AppFlowStore.kt │ │ ├── AppStore.kt │ │ └── CurrentProcessStore.kt │ │ ├── ui │ │ ├── dialog │ │ │ ├── CloudServerDialog.kt │ │ │ ├── ConfirmDialog.kt │ │ │ └── DepositGoalDialog.kt │ │ ├── fragment │ │ │ ├── DepositFragment.kt │ │ │ ├── DepositPresenter.kt │ │ │ ├── MemoFragment.kt │ │ │ ├── MemoPresenter.kt │ │ │ ├── ScheduleFragment.kt │ │ │ ├── SchedulePresenter.kt │ │ │ ├── SettingsFragment.kt │ │ │ ├── TimeCardFragment.kt │ │ │ └── TimeCardPresenter.kt │ │ ├── scene │ │ │ ├── AddSchedulePresenter.kt │ │ │ ├── AddScheduleScene.kt │ │ │ ├── DepositStatsPresenter.kt │ │ │ ├── DepositStatsScene.kt │ │ │ ├── ExportDataScene.kt │ │ │ ├── HomeScene.kt │ │ │ ├── LoginScene.kt │ │ │ ├── SignUpScene.kt │ │ │ ├── SyncScene.kt │ │ │ ├── WriteMemoPresenter.kt │ │ │ ├── WriteMemoScene.kt │ │ │ └── detail │ │ │ │ ├── DetailPresenter.kt │ │ │ │ ├── DetailScene.kt │ │ │ │ ├── HistoryFragment.kt │ │ │ │ └── TodayFragment.kt │ │ └── widget │ │ │ ├── AutoSyncIndicator.kt │ │ │ ├── BarChart.kt │ │ │ ├── BaseImmersiveScene.kt │ │ │ ├── BottomBar.kt │ │ │ ├── Divider.kt │ │ │ ├── EmptyLayout.kt │ │ │ ├── FragmentHeader.kt │ │ │ ├── HorizontalSeekBar.kt │ │ │ ├── LineChart.kt │ │ │ ├── ShimmerProgressBar.kt │ │ │ └── TitleBar.kt │ │ └── util │ │ ├── CalendarUtil.kt │ │ └── TimeUtil.kt │ └── iosMain │ └── kotlin │ ├── MainViewController.kt │ └── Platform.ios.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── iosApp ├── Configuration │ └── Config.xcconfig ├── iosApp.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── iosApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── app-icon-1024.png │ └── Contents.json │ ├── ContentView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iOSApp.swift ├── kotStore ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── tangping │ │ └── kotstore │ │ └── Platform.android.kt │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── tangping │ │ └── kotstore │ │ ├── Platform.kt │ │ ├── flow │ │ ├── FlowDelegate.kt │ │ └── FlowStore.kt │ │ ├── model │ │ ├── KotStoreFlowModel.kt │ │ └── KotStoreModel.kt │ │ └── store │ │ ├── AbstractStore.kt │ │ ├── BooleanStore.kt │ │ ├── DoubleStore.kt │ │ ├── FloatStore.kt │ │ ├── IntStore.kt │ │ ├── LongStore.kt │ │ └── StringStore.kt │ └── iosMain │ └── kotlin │ └── com │ └── tangping │ └── kotstore │ └── Platform.ios.kt └── settings.gradle.kts /.fleet/receipt.json: -------------------------------------------------------------------------------- 1 | // Project generated by Kotlin Multiplatform Wizard 2 | { 3 | "spec": { 4 | "template_id": "kmt", 5 | "targets": { 6 | "android": { 7 | "ui": [ 8 | "compose" 9 | ] 10 | }, 11 | "ios": { 12 | "ui": [ 13 | "compose" 14 | ] 15 | } 16 | } 17 | }, 18 | "timestamp": "2024-04-12T08:22:48.385227794Z" 19 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | **/build/ 4 | xcuserdata 5 | !src/**/build/ 6 | local.properties 7 | .idea 8 | .DS_Store 9 | captures 10 | .externalNativeBuild 11 | .cxx 12 | *.xcodeproj/* 13 | !*.xcodeproj/project.pbxproj 14 | !*.xcodeproj/xcshareddata/ 15 | !*.xcodeproj/project.xcworkspace/ 16 | !*.xcworkspace/contents.xcworkspacedata 17 | **/xcshareddata/WorkspaceSettings.xcsettings 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZhouTools 2 | 3 | > A work-life integration tool for modern workers. 4 | 5 | ## Information 6 | 7 | | | | 8 | | - |--------------------------------------------------| 9 | | **Supported Platforms** | Android 7.0+ / iOS (building from source needed) | 10 | | **Latest Version** | 1.4.0 | 11 | | **UI Language** | English | 12 | 13 | ## Update Notes 14 | 15 | ### Version 1.4.0 16 | 17 | - New statistics feature in `Time Card > Details > History`: Summarizing working hours by week/month/quarter/year. 18 | 19 | - New grouping functionality in `Memo` : Grouping notes and TODO tasks. 20 | 21 | - Android and iOS keyboard experience optimizations for the Memo feature. 22 | 23 | ### Version 1.3.0 24 | 25 | - New "Savings Statistics" feature page 26 | 27 | - Supports viewing total deposit amount changes by month as bar chart 28 | 29 | - Supports viewing monthly income trends as line chart 30 | 31 | - Provides multiple value filtering modes 32 | 33 | - New holiday display function in Schedule tab 34 | 35 | - Immersive experience optimization 36 | 37 | ### Version 1.2.0 38 | 39 | - Deposit Goal: It's now possible to set a total deposit goal and track your progress. 40 | 41 | - Milestone Goal: You can now set a milestone goal and see how many days remain until the target date. 42 | 43 | - `Memo > Goals` Section: Visualizes your deposit and milestone goals in one place. 44 | 45 | - Improved Data Synchronization: Redundant data synchronization operations are reduced to improve efficiency. 46 | 47 | - Improved slide-back gesture for iOS devices. 48 | 49 | ### Version 1.1.0 50 | 51 | - Login tokens are refreshed on launch to prevent expiration during data synchronization. 52 | 53 | - `Time Card > History` now displays historical data in reverse order, making it easier to view. 54 | 55 | - Introduce `Auto Sync` switch, enabling automatic data synchronization with the cloud, eliminating the need for manual operation (manual sync feature is still retained). 56 | 57 | ## Introduction 58 | 59 | This software is a mobile cross-platform work-life integration tool developed using the latest version of Compose Multiplatform. It aims to provide modern workers with a one-stop platform for managing **work hours, schedules and countdown days, memos, and monthly savings**. Data can be synchronized to the cloud via the official account system or a self-built server, ensuring 100% recovery of original records when switching devices. The UI is based on Google's Material Design 3, making the interface both beautiful and user-friendly. 60 | 61 | ### 1. Working Hours 62 | 63 | ![image](https://www.tang-ping.top/assets/assets/images/downloads/img_ztools_1.png) 64 | 65 | Record clock-in and clock-out times, summarize them monthly, and calculate the number of overtime days and the average daily working hours. 66 | 67 | ### 2. Schedules and Countdown Days 68 | 69 | ![image](https://www.tang-ping.top/assets/assets/images/downloads/img_ztools_2.png) 70 | 71 | Easily edit daily schedules in the calendar view, or create milestones to see how much time remains until a specific date or how much time has passed since that date. 72 | 73 | ### 3. Memos 74 | 75 | ![image](https://www.tang-ping.top/assets/assets/images/downloads/img_ztools_3.png) 76 | 77 | Add memos, pin important items to the top, or create TO-DO tasks and mark them as completed once done. 78 | 79 | ### 4. Monthly Savings 80 | 81 | ![image](https://www.tang-ping.top/assets/assets/images/downloads/img_ztools_4.png) 82 | 83 | Record the balance of your savings regularly each month, track your expenses by comparison, and always stay aware of how far you are from reaching your savings goal. 84 | 85 | ### 5. Settings 86 | 87 | ![image](https://www.tang-ping.top/assets/assets/images/downloads/img_ztools_5.png) 88 | 89 | Adjust the target values for working hours and overtime hours according to the specific regulations of your company. Data can also be synchronized with the official cloud service or a self-built server. -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | // this is necessary to avoid the plugins to be loaded multiple times 3 | // in each subproject's classloader 4 | alias(libs.plugins.androidApplication) apply false 5 | alias(libs.plugins.androidLibrary) apply false 6 | alias(libs.plugins.jetbrainsCompose) apply false 7 | alias(libs.plugins.compose.compiler) apply false 8 | alias(libs.plugins.kotlinMultiplatform) apply false 9 | } -------------------------------------------------------------------------------- /composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.android.build.gradle.internal.api.ApkVariantOutputImpl 2 | 3 | plugins { 4 | alias(libs.plugins.kotlinMultiplatform) 5 | alias(libs.plugins.androidApplication) 6 | alias(libs.plugins.jetbrainsCompose) 7 | alias(libs.plugins.compose.compiler) 8 | kotlin(libs.plugins.serialization.get().pluginId).version(libs.versions.serialization) 9 | } 10 | 11 | compose.resources { 12 | publicResClass = false 13 | generateResClass = always 14 | } 15 | 16 | kotlin { 17 | androidTarget { 18 | compilations.all { 19 | kotlinOptions { 20 | jvmTarget = "11" 21 | } 22 | } 23 | } 24 | 25 | listOf( 26 | iosX64(), 27 | iosArm64(), 28 | iosSimulatorArm64() 29 | ).forEach { iosTarget -> 30 | iosTarget.binaries.framework { 31 | baseName = "ComposeApp" 32 | isStatic = true 33 | } 34 | } 35 | 36 | sourceSets { 37 | 38 | androidMain.dependencies { 39 | implementation(libs.compose.ui.tooling.preview) 40 | implementation(libs.androidx.activity.compose) 41 | implementation(libs.ktor.client.android) 42 | } 43 | iosMain.dependencies { 44 | implementation(libs.ktor.client.darwin) 45 | } 46 | commonMain.dependencies { 47 | implementation(compose.runtime) 48 | implementation(compose.foundation) 49 | implementation(compose.material) 50 | implementation(compose.ui) 51 | implementation(compose.components.resources) 52 | implementation(compose.components.uiToolingPreview) 53 | // Molecule 54 | implementation(libs.molecule.runtime) 55 | // PreCompose 56 | implementation(libs.precompose) 57 | implementation(libs.precompose.molecule) 58 | // Ktor 59 | implementation(libs.ktor.client.core) 60 | implementation(libs.ktor.client.content.negotiation) 61 | implementation(libs.ktor.serialization.kotlinx.json) 62 | // DataStore 63 | implementation(libs.androidx.datastore.core) 64 | // DateTime 65 | implementation(libs.kotlinx.datetime) 66 | // Logging 67 | api(libs.km.logging) 68 | // Material3 69 | implementation(libs.material3) 70 | // KotStore 71 | implementation(project(":kotStore")) 72 | } 73 | } 74 | } 75 | 76 | android { 77 | namespace = "com.tangping.zhoujiang" 78 | compileSdk = libs.versions.android.compileSdk.get().toInt() 79 | 80 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 81 | sourceSets["main"].res.srcDirs("src/androidMain/res") 82 | sourceSets["main"].resources.srcDirs("src/commonMain/resources") 83 | 84 | defaultConfig { 85 | applicationId = "com.tangping.zhoujiang" 86 | minSdk = libs.versions.android.minSdk.get().toInt() 87 | targetSdk = libs.versions.android.targetSdk.get().toInt() 88 | versionCode = 5 89 | versionName = "1.4.0" 90 | } 91 | packaging { 92 | resources { 93 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 94 | } 95 | } 96 | buildTypes { 97 | getByName("release") { 98 | isMinifyEnabled = false 99 | } 100 | } 101 | compileOptions { 102 | sourceCompatibility = JavaVersion.VERSION_11 103 | targetCompatibility = JavaVersion.VERSION_11 104 | } 105 | dependencies { 106 | debugImplementation(libs.compose.ui.tooling) 107 | } 108 | applicationVariants.all { 109 | outputs.all { output -> 110 | if (output is ApkVariantOutputImpl) { 111 | output.outputFileName = "ZhouTools-v${versionName}.apk" 112 | } 113 | true 114 | } 115 | } 116 | } 117 | 118 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/Platform.android.kt: -------------------------------------------------------------------------------- 1 | import android.app.Activity 2 | import android.content.ClipData 3 | import android.content.ClipboardManager 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.os.Build 7 | import android.view.View 8 | import android.view.inputmethod.InputMethodManager 9 | import androidx.compose.foundation.layout.WindowInsets 10 | import androidx.compose.foundation.layout.ime 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.State 13 | import androidx.compose.runtime.derivedStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.platform.LocalDensity 16 | import com.tangping.zhoujiang.MainActivity 17 | 18 | actual fun isIOS(): Boolean = false 19 | 20 | actual fun getAppVersion(): String { 21 | val context = MainActivity.context ?: return "" 22 | return context.packageManager.getPackageInfo(context.packageName, 0).versionName 23 | } 24 | 25 | actual fun setClipboardContent(text: String) { 26 | val context = MainActivity.context ?: return 27 | val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 28 | val clip = ClipData.newPlainText("label", text) 29 | clipboard.setPrimaryClip(clip) 30 | } 31 | 32 | actual fun hideSoftwareKeyboard() { 33 | val context = MainActivity.context ?: return 34 | val imeManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 35 | val currentFocus = (context as? Activity)?.currentFocus 36 | val windowToken = currentFocus?.windowToken 37 | windowToken?.let { 38 | imeManager.hideSoftInputFromWindow(windowToken, 0) 39 | } 40 | } 41 | 42 | actual fun setStatusBarColor(colorStr: String, isLight: Boolean) { 43 | val window = MainActivity.window ?: return 44 | window.statusBarColor = Color.parseColor(colorStr) 45 | if (isLight) { 46 | window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 47 | } else { 48 | window.decorView.systemUiVisibility = window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() 49 | } 50 | } 51 | 52 | actual fun setNavigationBarColor(colorStr: String, isLight: Boolean) { 53 | val window = MainActivity.window ?: return 54 | window.navigationBarColor = Color.parseColor(colorStr) 55 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 56 | if (isLight) { 57 | window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR 58 | } else { 59 | window.decorView.systemUiVisibility = window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv() 60 | } 61 | } 62 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 63 | window.navigationBarDividerColor = Color.parseColor(colorStr) 64 | } 65 | } 66 | 67 | @Composable 68 | actual fun rememberKeyboardVisibilityState(): State { 69 | val density = LocalDensity.current 70 | val imeInsets = WindowInsets.ime 71 | val isImeVisible = remember { 72 | derivedStateOf { 73 | imeInsets.getBottom(density) > 0 74 | } 75 | } 76 | return isImeVisible 77 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/tangping/zhoujiang/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tangping.zhoujiang 2 | 3 | import App 4 | import android.annotation.SuppressLint 5 | import android.content.Context 6 | import android.os.Bundle 7 | import android.view.Window 8 | import androidx.activity.ComponentActivity 9 | import androidx.activity.compose.setContent 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.core.view.WindowCompat 13 | import com.tangping.kotstore.KotStoreAndroidBase 14 | 15 | class MainActivity : ComponentActivity() { 16 | companion object { 17 | @SuppressLint("StaticFieldLeak") 18 | var context: Context? = null 19 | var window: Window? = null 20 | } 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | context = this 25 | setTheme(R.style.TransparentSystemBars) 26 | WindowCompat.setDecorFitsSystemWindows(window, false) 27 | MainActivity.window = window 28 | KotStoreAndroidBase.init(this) 29 | 30 | setContent { 31 | App() 32 | } 33 | } 34 | } 35 | 36 | @Preview 37 | @Composable 38 | fun AppAndroidPreview() { 39 | App() 40 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AkatsukiRika/ZhouTools/d847ee5dd74e0c661cc153f9cc75550ec7a238b5/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values-v27/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values-v29/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Zhou Tools 3 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 24 | 30 | 36 | 37 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_arrow_down.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_auto.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_back.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_copy.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_countdown.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_deposit.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_deposit_goal.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_details.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_empty.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_export.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_history_overtime.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_history_run.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_logout.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_memo.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_milestone.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_next.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_overtime.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_pin.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_prev.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_schedule.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_server.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_sync.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_tick.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_time_card.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_todo.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_todo_finished.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_work_enough.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_working.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_zhou.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/App.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.animation.core.LinearEasing 2 | import androidx.compose.animation.core.tween 3 | import androidx.compose.animation.slideInHorizontally 4 | import androidx.compose.animation.slideOutHorizontally 5 | import androidx.compose.runtime.* 6 | import global.AppTheme 7 | import constant.RouteConstants 8 | import helper.NetworkHelper 9 | import model.request.LoginRequest 10 | import moe.tlaster.precompose.PreComposeApp 11 | import moe.tlaster.precompose.navigation.NavHost 12 | import moe.tlaster.precompose.navigation.SwipeProperties 13 | import moe.tlaster.precompose.navigation.path 14 | import moe.tlaster.precompose.navigation.rememberNavigator 15 | import moe.tlaster.precompose.navigation.transition.NavTransition 16 | import org.jetbrains.compose.ui.tooling.preview.Preview 17 | import org.lighthousegames.logging.logging 18 | import ui.scene.HomeScene 19 | import ui.scene.LoginScene 20 | import store.AppStore 21 | import ui.scene.AddScheduleScene 22 | import ui.scene.DepositStatsScene 23 | import ui.scene.ExportDataScene 24 | import ui.scene.SignUpScene 25 | import ui.scene.SyncScene 26 | import ui.scene.WriteMemoScene 27 | import ui.scene.detail.DetailScene 28 | 29 | val logger = logging("App") 30 | 31 | @Composable 32 | @Preview 33 | fun App() { 34 | PreComposeApp { 35 | val navigator = rememberNavigator() 36 | val currentEntry = navigator.currentEntry.collectAsState(initial = null).value 37 | var swipeProperties: SwipeProperties? = remember { SwipeProperties() } 38 | val isLogin = AppStore.loginToken.isNotBlank() 39 | val navTransition = remember { 40 | NavTransition( 41 | createTransition = slideInHorizontally(animationSpec = tween(easing = LinearEasing)) { it }, 42 | destroyTransition = slideOutHorizontally(animationSpec = tween(easing = LinearEasing)) { it }, 43 | pauseTransition = slideOutHorizontally { -it / 4 }, 44 | resumeTransition = slideInHorizontally { -it / 4 }, 45 | exitTargetContentZIndex = 1f 46 | ) 47 | } 48 | 49 | LaunchedEffect(currentEntry) { 50 | val currentRoute = currentEntry?.route?.route 51 | swipeProperties = if (currentRoute == RouteConstants.ROUTE_HOME) { 52 | null 53 | } else { 54 | SwipeProperties() 55 | } 56 | } 57 | 58 | NavHost( 59 | navigator = navigator, 60 | swipeProperties = swipeProperties, 61 | navTransition = navTransition, 62 | initialRoute = if (isLogin) RouteConstants.ROUTE_HOME else RouteConstants.ROUTE_LOGIN 63 | ) { 64 | scene( 65 | route = RouteConstants.ROUTE_LOGIN, 66 | navTransition = navTransition 67 | ) { 68 | AppTheme { 69 | LoginScene(navigator) 70 | } 71 | } 72 | 73 | scene( 74 | route = RouteConstants.ROUTE_SIGN_UP, 75 | navTransition = navTransition 76 | ) { 77 | AppTheme { 78 | SignUpScene(navigator) 79 | } 80 | } 81 | 82 | scene( 83 | route = RouteConstants.ROUTE_HOME, 84 | navTransition = navTransition 85 | ) { 86 | AppTheme { 87 | HomeScene(navigator) 88 | } 89 | } 90 | 91 | scene( 92 | route = RouteConstants.ROUTE_DETAILS, 93 | navTransition = navTransition 94 | ) { 95 | AppTheme { 96 | DetailScene(navigator) 97 | } 98 | } 99 | 100 | scene( 101 | route = RouteConstants.ROUTE_WRITE_MEMO, 102 | navTransition = navTransition 103 | ) { 104 | val isEdit = it.path("edit") ?: false 105 | 106 | AppTheme { 107 | WriteMemoScene(navigator, isEdit) 108 | } 109 | } 110 | 111 | scene( 112 | route = RouteConstants.ROUTE_ADD_SCHEDULE, 113 | navTransition = navTransition 114 | ) { 115 | AppTheme { 116 | AddScheduleScene(navigator) 117 | } 118 | } 119 | 120 | scene( 121 | route = RouteConstants.ROUTE_SYNC, 122 | navTransition = navTransition 123 | ) { 124 | val mode = it.path("mode") ?: "" 125 | 126 | AppTheme { 127 | SyncScene(navigator, mode) 128 | } 129 | } 130 | 131 | scene( 132 | route = RouteConstants.ROUTE_EXPORT, 133 | navTransition = navTransition 134 | ) { 135 | AppTheme { 136 | ExportDataScene(navigator) 137 | } 138 | } 139 | 140 | scene( 141 | route = RouteConstants.ROUTE_DEPOSIT_STATS, 142 | navTransition = navTransition 143 | ) { 144 | AppTheme { 145 | DepositStatsScene(navigator) 146 | } 147 | } 148 | } 149 | 150 | LaunchedEffect(Unit) { 151 | logger.i { "Welcome to Zhou Tools!" } 152 | checkLoginValidity() 153 | } 154 | } 155 | } 156 | 157 | /** 158 | * Check login token validity and login again when token is invalid. 159 | */ 160 | private suspend fun checkLoginValidity() { 161 | logger.i { "checkLoginValidity: loginToken=${AppStore.loginToken}, loginUsername=${AppStore.loginUsername}" } 162 | if (AppStore.loginToken.isNotEmpty() && AppStore.loginUsername.isNotEmpty() && AppStore.loginPassword.isNotEmpty()) { 163 | val isValid = NetworkHelper.checkTokenValidity(AppStore.loginToken) 164 | logger.i { "checkLoginValidity: isValid=$isValid" } 165 | if (!isValid) { 166 | // login again 167 | val loginRequest = LoginRequest(username = AppStore.loginUsername, password = AppStore.loginPassword) 168 | val loginResponse = NetworkHelper.login(loginRequest) 169 | val isSuccess = loginResponse.first 170 | logger.i { "checkLoginValidity: login isSuccess=$isSuccess" } 171 | if (isSuccess) { 172 | val token = loginResponse.second 173 | logger.i { "checkLoginValidity: new token=$token" } 174 | if (token != null) { 175 | AppStore.loginToken = token 176 | } 177 | } 178 | } 179 | } 180 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/Platform.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.Composable 2 | import androidx.compose.runtime.State 3 | 4 | internal const val PREFERENCES_NAME = "zhoutools.preferences_pb" 5 | internal const val FLOW_PREFERENCES_NAME = "zhoutools_flow.preferences_pb" 6 | 7 | expect fun isIOS(): Boolean 8 | 9 | expect fun getAppVersion(): String 10 | 11 | expect fun setClipboardContent(text: String) 12 | 13 | expect fun hideSoftwareKeyboard() 14 | 15 | expect fun setStatusBarColor(colorStr: String, isLight: Boolean) 16 | 17 | expect fun setNavigationBarColor(colorStr: String, isLight: Boolean) 18 | 19 | @Composable 20 | expect fun rememberKeyboardVisibilityState(): State -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/constant/RouteConstants.kt: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | object RouteConstants { 4 | const val PARAM_EDIT = "{edit}" 5 | const val PARAM_MODE = "{mode}" 6 | 7 | const val ROUTE_LOGIN = "/login" 8 | const val ROUTE_SIGN_UP = "/signUp" 9 | const val ROUTE_HOME = "/home" 10 | const val ROUTE_DETAILS = "/details" 11 | const val ROUTE_WRITE_MEMO = "/writeMemo/$PARAM_EDIT" 12 | const val ROUTE_ADD_SCHEDULE = "/addSchedule" 13 | const val ROUTE_SYNC = "/sync/$PARAM_MODE" 14 | const val ROUTE_EXPORT = "/export" 15 | const val ROUTE_DEPOSIT_STATS = "/deposit/stats" 16 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/constant/TabConstants.kt: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | object TabConstants { 4 | const val TAB_TIME_CARD = 0 5 | const val TAB_SCHEDULE = 1 6 | const val TAB_SETTINGS = 2 7 | const val TAB_MEMO = 3 8 | const val TAB_DEPOSIT = 4 9 | const val TAB_COUNT = 5 10 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/constant/TimeConstants.kt: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | object TimeConstants { 4 | const val DAY_MILLIS = 24 * 60 * 60 * 1000L 5 | const val WEEK_MILLIS = 7 * DAY_MILLIS 6 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/extension/ModifierExt.kt: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.Modifier 8 | 9 | @Composable 10 | fun Modifier.clickableNoRipple(onClick: () -> Unit) = Modifier.clickable( 11 | onClick = onClick, 12 | interactionSource = remember { MutableInteractionSource() }, 13 | indication = null 14 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/extension/NumberExt.kt: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import kotlin.math.pow 4 | import kotlin.math.round 5 | 6 | fun Float.roundToDecimalPlaces(n: Int): String { 7 | val multiplier = 10.0.pow(n.toDouble()) 8 | val roundedNumber = round(this * multiplier) / multiplier 9 | val numberString = roundedNumber.toString() 10 | 11 | val decimalPointIndex = numberString.indexOf('.') 12 | return if (decimalPointIndex == -1) { 13 | "${numberString}.${"0".repeat(n)}" 14 | } else { 15 | val existingDecimalCount = numberString.length - decimalPointIndex - 1 16 | if (existingDecimalCount < n) { 17 | numberString + "0".repeat(n - existingDecimalCount) 18 | } else { 19 | numberString 20 | } 21 | } 22 | } 23 | 24 | fun Long.toMoneyDisplayStr() = when { 25 | this < 10 -> { 26 | "0.0${this}" 27 | } 28 | 29 | this < 100 -> { 30 | "0.${this}" 31 | } 32 | 33 | else -> { 34 | val beforePoint = this / 100 35 | val afterPoint = this % 100 36 | val afterPointStr = if (afterPoint < 10) { 37 | "0$afterPoint" 38 | } else afterPoint.toString() 39 | "${beforePoint}.${afterPointStr}" 40 | } 41 | } 42 | 43 | fun Int.toTwoDigits() = if (this < 10) { 44 | "0$this" 45 | } else { 46 | this.toString() 47 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/extension/StringExtension.kt: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | fun String.firstCharToCapital(): String { 4 | if (this.isBlank()) { 5 | return this 6 | } 7 | return this.first().uppercase() + this.substring(1) 8 | } 9 | 10 | fun String.isBlankJson(): Boolean { 11 | return this == "{}" 12 | } 13 | 14 | fun String.isValidUrl(): Boolean { 15 | return this.startsWith("http://") || this.startsWith("https://") 16 | } 17 | 18 | fun String.isValidEmail(): Boolean { 19 | val emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\$" 20 | return this.matches(emailRegex.toRegex()) 21 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/global/AppColors.kt: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import androidx.compose.material3.FilterChipDefaults 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | 7 | object AppColors { 8 | val Background = Color(0xFFF4F4F4) 9 | val DarkBackground = Color(0xFF202124) 10 | val Theme = Color(0xFFD685A9) 11 | val LightTheme = Color(0xFFE9A6B3) 12 | val SlightTheme = Color(0xFFFFEAE3) 13 | val Divider = Color(0x26333333) 14 | val LightGreen = Color(0xFF8DECB4) 15 | val DarkGreen = Color(0xFF41B06E) 16 | val Red = Color(0xFFC40C0C) 17 | val LightGold = Color(0xFFE1B84C) 18 | val Yellow = Color(0xFFFFD95F) 19 | 20 | @Composable 21 | fun getChipColors() = FilterChipDefaults.elevatedFilterChipColors( 22 | containerColor = SlightTheme, 23 | selectedContainerColor = Theme, 24 | selectedLabelColor = Color.White 25 | ) 26 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/global/AppTheme.kt: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import androidx.compose.material.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | fun AppTheme(content: @Composable () -> Unit) { 8 | MaterialTheme( 9 | colors = MaterialTheme.colors.copy( 10 | primary = AppColors.Theme, 11 | background = AppColors.Background 12 | ) 13 | ) { 14 | content() 15 | } 16 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/helper/DepositHelper.kt: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import kotlinx.serialization.encodeToString 4 | import kotlinx.serialization.json.Json 5 | import model.records.DepositMonth 6 | import model.records.DepositRecords 7 | import model.request.DepositSyncRequest 8 | import store.AppStore 9 | 10 | object DepositHelper { 11 | fun buildSyncRequest(): DepositSyncRequest? { 12 | val months = getMonths() 13 | return try { 14 | if (AppStore.loginUsername.isEmpty()) { 15 | null 16 | } else { 17 | DepositSyncRequest(username = AppStore.loginUsername, depositMonths = months) 18 | } 19 | } catch (e: Exception) { 20 | null 21 | } 22 | } 23 | 24 | fun getMonths() = try { 25 | val depositRecords = Json.decodeFromString(AppStore.depositMonths) 26 | depositRecords.months 27 | } catch (e: Exception) { 28 | e.printStackTrace() 29 | mutableListOf() 30 | } 31 | 32 | fun addMonth(month: DepositMonth) { 33 | val months = getMonths().toMutableList() 34 | val alreadyMonth = months.find { it.monthStartTime == month.monthStartTime } 35 | if (alreadyMonth != null) { 36 | months.remove(alreadyMonth) 37 | } 38 | months.add(month) 39 | saveMonths(months) 40 | } 41 | 42 | fun removeMonth(month: DepositMonth) { 43 | val months = getMonths().toMutableList() 44 | months.remove(month) 45 | saveMonths(months) 46 | } 47 | 48 | private fun saveMonths(months: List) { 49 | val depositRecords = DepositRecords(months) 50 | AppStore.depositMonths = Json.encodeToString(depositRecords) 51 | } 52 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/helper/MemoHelper.kt: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import kotlinx.serialization.encodeToString 5 | import kotlinx.serialization.json.Json 6 | import model.display.GroupDisplayItem 7 | import model.display.IMemoDisplayItem 8 | import model.display.MemoDisplayItem 9 | import model.records.Memo 10 | import model.records.MemoRecords 11 | import model.request.MemoSyncRequest 12 | import org.jetbrains.compose.resources.getString 13 | import store.AppStore 14 | import util.TimeUtil 15 | import zhoutools.composeapp.generated.resources.Res 16 | import zhoutools.composeapp.generated.resources.unsorted 17 | 18 | object MemoHelper { 19 | fun addMemo(text: String, todo: Boolean, pin: Boolean, group: String? = null) { 20 | val time = TimeUtil.currentTimeMillis() 21 | val memo = Memo( 22 | text = text, 23 | isTodo = todo, 24 | isPin = pin, 25 | createTime = time, 26 | modifyTime = time, 27 | group = group 28 | ) 29 | val memos = getMemos() 30 | memos.add(memo) 31 | saveMemos(memos) 32 | } 33 | 34 | fun modifyMemo(memo: Memo, text: String, todo: Boolean, pin: Boolean, group: String? = null) { 35 | val memos = getMemos() 36 | val findResult = memos.find { 37 | it.text == memo.text 38 | && it.isTodo == memo.isTodo 39 | && it.isPin == memo.isPin 40 | && it.createTime == memo.createTime 41 | && it.modifyTime == memo.modifyTime 42 | && it.group == memo.group 43 | } 44 | if (findResult != null) { 45 | findResult.text = text 46 | findResult.isTodo = todo 47 | findResult.isPin = pin 48 | findResult.modifyTime = TimeUtil.currentTimeMillis() 49 | findResult.group = group 50 | } else { 51 | addMemo(text, todo, pin, group) 52 | } 53 | saveMemos(memos) 54 | } 55 | 56 | fun markDone(memo: Memo, done: Boolean) { 57 | val memos = getMemos() 58 | val findResult = memos.find { 59 | it.text == memo.text 60 | && it.isTodo == memo.isTodo 61 | && it.isPin == memo.isPin 62 | && it.createTime == memo.createTime 63 | && it.modifyTime == memo.modifyTime 64 | && it.group == memo.group 65 | } 66 | if (findResult != null) { 67 | findResult.isTodoFinished = done 68 | saveMemos(memos) 69 | } 70 | } 71 | 72 | fun deleteMemo(memo: Memo) { 73 | val memos = getMemos() 74 | memos.remove(memo) 75 | saveMemos(memos) 76 | } 77 | 78 | fun getDisplayList(): List { 79 | val memos = getMemos() 80 | val displayList = mutableListOf() 81 | val groupSet = getGroupSet().sorted() 82 | groupSet.forEach { group -> 83 | val groupDisplayItem = GroupDisplayItem(group) 84 | displayList.add(groupDisplayItem) 85 | val groupMemos = memos.filter { it.group == group } 86 | val groupPinMemos = groupMemos.filter { it.isPin } 87 | val groupNotPinMemos = groupMemos.filterNot { it.isPin } 88 | groupPinMemos.forEach { memo -> 89 | displayList.add(MemoDisplayItem(memo)) 90 | } 91 | groupNotPinMemos.forEach { memo -> 92 | displayList.add(MemoDisplayItem(memo)) 93 | } 94 | } 95 | val unsortedMemos = memos.filter { it.group == null } 96 | val unsortedPinMemos = unsortedMemos.filter { it.isPin } 97 | val unsortedNotPinMemos = unsortedMemos.filterNot { it.isPin } 98 | val unsorted = runBlocking { getString(Res.string.unsorted) } 99 | displayList.add(GroupDisplayItem(unsorted)) 100 | unsortedPinMemos.forEach { memo -> 101 | displayList.add(MemoDisplayItem(memo)) 102 | } 103 | unsortedNotPinMemos.forEach { memo -> 104 | displayList.add(MemoDisplayItem(memo)) 105 | } 106 | return displayList 107 | } 108 | 109 | fun getGroupSet(): Set { 110 | val memos = getMemos() 111 | val groupSet = mutableSetOf() 112 | memos.forEach { 113 | it.group?.let { group -> 114 | groupSet.add(group) 115 | } 116 | } 117 | return groupSet 118 | } 119 | 120 | fun buildSyncRequest(): MemoSyncRequest? { 121 | val memos = getMemos() 122 | return try { 123 | if (AppStore.loginUsername.isEmpty()) { 124 | null 125 | } else { 126 | MemoSyncRequest(username = AppStore.loginUsername, memos = memos) 127 | } 128 | } catch (e: Exception) { 129 | null 130 | } 131 | } 132 | 133 | private fun getMemos() = try { 134 | val memoRecords = Json.decodeFromString(AppStore.memos) 135 | memoRecords.memos 136 | } catch (e: Exception) { 137 | e.printStackTrace() 138 | mutableListOf() 139 | } 140 | 141 | private fun saveMemos(memos: MutableList) { 142 | val records = MemoRecords(memos) 143 | AppStore.memos = Json.encodeToString(records) 144 | } 145 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/helper/ScheduleHelper.kt: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import kotlinx.serialization.encodeToString 4 | import kotlinx.serialization.json.Json 5 | import model.records.Schedule 6 | import model.records.ScheduleRecords 7 | import model.request.ScheduleSyncRequest 8 | import store.AppStore 9 | 10 | object ScheduleHelper { 11 | fun getDisplayList(): List { 12 | val schedules = getSchedules() 13 | val displayList = mutableListOf() 14 | val milestoneList = schedules.filter { it.isMilestone } 15 | val othersList = schedules.filterNot { it.isMilestone } 16 | displayList.addAll(milestoneList) 17 | displayList.addAll(othersList) 18 | return displayList 19 | } 20 | 21 | fun addSchedule(schedule: Schedule) { 22 | val schedules = getSchedules() 23 | schedules.add(schedule) 24 | saveSchedules(schedules) 25 | } 26 | 27 | fun deleteSchedule(schedule: Schedule) { 28 | val schedules = getSchedules() 29 | schedules.remove(schedule) 30 | saveSchedules(schedules) 31 | } 32 | 33 | fun modifySchedule( 34 | schedule: Schedule, 35 | text: String, 36 | startingTime: Long, 37 | endingTime: Long, 38 | isAllDay: Boolean, 39 | isMilestone: Boolean, 40 | milestoneGoal: Long 41 | ) { 42 | val schedules = getSchedules() 43 | val match = schedules.find { it == schedule } 44 | match?.let { 45 | it.text = text 46 | it.startingTime = startingTime 47 | it.endingTime = endingTime 48 | it.isAllDay = isAllDay 49 | it.isMilestone = isMilestone 50 | it.milestoneGoal = milestoneGoal 51 | } 52 | saveSchedules(schedules) 53 | } 54 | 55 | fun buildSyncRequest(): ScheduleSyncRequest? { 56 | val schedules = getSchedules() 57 | return try { 58 | if (AppStore.loginUsername.isEmpty()) { 59 | null 60 | } else { 61 | ScheduleSyncRequest(username = AppStore.loginUsername, schedules = schedules) 62 | } 63 | } catch (e: Exception) { 64 | null 65 | } 66 | } 67 | 68 | private fun getSchedules() = try { 69 | val scheduleRecords = Json.decodeFromString(AppStore.schedules) 70 | scheduleRecords.schedules 71 | } catch (e: Exception) { 72 | e.printStackTrace() 73 | mutableListOf() 74 | } 75 | 76 | private fun saveSchedules(schedules: MutableList) { 77 | val records = ScheduleRecords(schedules) 78 | AppStore.schedules = Json.encodeToString(records) 79 | } 80 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/helper/TimeCardHelper.kt: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import extension.dayStartTime 4 | import extension.isBlankJson 5 | import kotlinx.coroutines.flow.first 6 | import kotlinx.coroutines.runBlocking 7 | import kotlinx.serialization.encodeToString 8 | import kotlinx.serialization.json.Json 9 | import model.records.TimeCardDay 10 | import model.records.TimeCardRecords 11 | import model.request.TimeCardSyncRequest 12 | import store.AppFlowStore 13 | import store.AppStore 14 | import util.TimeUtil 15 | 16 | object TimeCardHelper { 17 | fun getMinWorkingTimeMillis() = runBlocking { 18 | val hours = AppFlowStore.minWorkingHoursFlow.first() 19 | val millis = hours * 60 * 60 * 1000 20 | millis.toLong() 21 | } 22 | 23 | fun getMinOvertimeMillis() = runBlocking { 24 | val hours = AppFlowStore.minWorkingHoursFlow.first() + AppFlowStore.minOvertimeHoursFlow.first() 25 | val millis = hours * 60 * 60 * 1000 26 | millis.toLong() 27 | } 28 | 29 | fun pressTimeCard() { 30 | val curTime = TimeUtil.currentTimeMillis() 31 | val dayStartTime = curTime.dayStartTime() 32 | val timeCardDay = TimeCardDay( 33 | dayStartTime = dayStartTime, 34 | latestTimeCard = curTime 35 | ) 36 | try { 37 | val records: TimeCardRecords = Json.decodeFromString(AppStore.timeCards) 38 | val days = records.days 39 | val curDay = days.find { it.dayStartTime == dayStartTime } 40 | if (curDay != null) { 41 | curDay.latestTimeCard = curTime 42 | } else { 43 | days.add(timeCardDay) 44 | } 45 | AppStore.timeCards = Json.encodeToString(records) 46 | } catch (e: Exception) { 47 | e.printStackTrace() 48 | val days = mutableListOf() 49 | days.add(timeCardDay) 50 | val records = TimeCardRecords(days = days) 51 | AppStore.timeCards = Json.encodeToString(records) 52 | } 53 | } 54 | 55 | /** 56 | * @return isSuccess 57 | */ 58 | fun run(): Boolean { 59 | val curTime = TimeUtil.currentTimeMillis() 60 | val dayStartTime = curTime.dayStartTime() 61 | return try { 62 | val records: TimeCardRecords = Json.decodeFromString(AppStore.timeCards) 63 | val days = records.days 64 | val curDay = days.find { it.dayStartTime == dayStartTime } 65 | if (curDay != null) { 66 | curDay.latestTimeRun = curTime 67 | AppStore.timeCards = Json.encodeToString(records) 68 | true 69 | } else { 70 | false 71 | } 72 | } catch (e: Exception) { 73 | e.printStackTrace() 74 | false 75 | } 76 | } 77 | 78 | private fun hasTodayTimeCard(): Boolean { 79 | if (AppStore.timeCards.isBlankJson()) { 80 | return false 81 | } 82 | val curTime = TimeUtil.currentTimeMillis() 83 | val dayStartTime = curTime.dayStartTime() 84 | return try { 85 | val records: TimeCardRecords = Json.decodeFromString(AppStore.timeCards) 86 | val days = records.days 87 | val curDay = days.find { it.dayStartTime == dayStartTime } 88 | curDay != null 89 | } catch (e: Exception) { 90 | e.printStackTrace() 91 | false 92 | } 93 | } 94 | 95 | fun todayTimeRun(): Long? { 96 | if (AppStore.timeCards.isBlankJson()) { 97 | return null 98 | } 99 | val curTime = TimeUtil.currentTimeMillis() 100 | val dayStartTime = curTime.dayStartTime() 101 | return try { 102 | val records: TimeCardRecords = Json.decodeFromString(AppStore.timeCards) 103 | val days = records.days 104 | val curDay = days.find { it.dayStartTime == dayStartTime } 105 | curDay?.latestTimeRun 106 | } catch (e: Exception) { 107 | e.printStackTrace() 108 | null 109 | } 110 | } 111 | 112 | fun todayTimeCard(): Long? { 113 | if (!hasTodayTimeCard()) { 114 | return null 115 | } 116 | val curTime = TimeUtil.currentTimeMillis() 117 | val dayStartTime = curTime.dayStartTime() 118 | return try { 119 | val records: TimeCardRecords = Json.decodeFromString(AppStore.timeCards) 120 | val days = records.days 121 | val curDay = days.find { it.dayStartTime == dayStartTime } 122 | return curDay?.latestTimeCard 123 | } catch (e: Exception) { 124 | e.printStackTrace() 125 | null 126 | } 127 | } 128 | 129 | fun buildSyncRequest(): TimeCardSyncRequest? { 130 | return try { 131 | val records: TimeCardRecords = Json.decodeFromString(AppStore.timeCards) 132 | val username = AppStore.loginUsername 133 | if (username.isBlank()) { 134 | null 135 | } else { 136 | TimeCardSyncRequest(username, records) 137 | } 138 | } catch (e: Exception) { 139 | null 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/helper/WorkHoursHelper.kt: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | object WorkHoursHelper { 4 | val workingHoursMap = mutableMapOf( 5 | "4h" to 4f, 6 | "4.5h" to 4.5f, 7 | "5h" to 5f, 8 | "5.5h" to 5.5f, 9 | "6h" to 6f, 10 | "6.5h" to 6.5f, 11 | "7h" to 7f, 12 | "7.5h" to 7.5f, 13 | "8h" to 8f, 14 | "8.5h" to 8.5f, 15 | "9h" to 9f, 16 | "9.5h" to 9.5f, 17 | "10h" to 10f, 18 | "10.5h" to 10.5f, 19 | "11h" to 11f, 20 | "11.5h" to 11.5f, 21 | "12h" to 12f, 22 | "12.5h" to 12.5f, 23 | "13h" to 13f, 24 | "13.5h" to 13.5f, 25 | "14h" to 14f, 26 | "14.5h" to 14.5f, 27 | "15h" to 15f, 28 | "15.5h" to 15.5f, 29 | "16h" to 16f 30 | ) 31 | 32 | val overtimeHoursMap = mutableMapOf( 33 | "0.5h" to 0.5f, 34 | "1h" to 1f, 35 | "1.5h" to 1.5f, 36 | "2h" to 2f, 37 | "2.5h" to 2.5f, 38 | "3h" to 3f, 39 | "3.5h" to 3.5f, 40 | "4h" to 4f 41 | ) 42 | 43 | fun getWorkingHourString(hours: Float): String? { 44 | return workingHoursMap.entries.firstOrNull { it.value == hours }?.key 45 | } 46 | 47 | fun getOvertimeHourString(hours: Float): String? { 48 | return overtimeHoursMap.entries.firstOrNull { it.value == hours }?.key 49 | } 50 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/helper/effect/BaseEffectObserver.kt: -------------------------------------------------------------------------------- 1 | package helper.effect 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import kotlinx.coroutines.flow.MutableSharedFlow 6 | import kotlinx.coroutines.runBlocking 7 | import moe.tlaster.precompose.flow.collectAsStateWithLifecycle 8 | 9 | open class BaseEffectObserver { 10 | private val effectFlow = MutableSharedFlow(replay = 1) 11 | 12 | fun emitSync(effect: T?) { 13 | runBlocking { 14 | effectFlow.emit(effect) 15 | } 16 | } 17 | 18 | suspend fun emit(effect: T?) { 19 | effectFlow.emit(effect) 20 | } 21 | 22 | @Composable 23 | fun observeComposable(onEffect: (T) -> Unit) { 24 | val effect = effectFlow.collectAsStateWithLifecycle(null).value 25 | 26 | LaunchedEffect(effect) { 27 | if (effect != null) { 28 | onEffect(effect) 29 | emit(null) 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/helper/effect/EffectHelper.kt: -------------------------------------------------------------------------------- 1 | package helper.effect 2 | 3 | import androidx.compose.runtime.Composable 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.launch 6 | 7 | object EffectHelper { 8 | private val timeCardEffectObserver by lazy { 9 | TimeCardEffectObserver() 10 | } 11 | 12 | private val memoEffectObserver by lazy { 13 | MemoEffectObserver() 14 | } 15 | 16 | private val writeMemoEffectObserver by lazy { 17 | WriteMemoEffectObserver() 18 | } 19 | 20 | private val addScheduleEffectObserver by lazy { 21 | AddScheduleEffectObserver() 22 | } 23 | 24 | private val scheduleEffectObserver by lazy { 25 | ScheduleEffectObserver() 26 | } 27 | 28 | private val depositEffectObserver by lazy { 29 | DepositEffectObserver() 30 | } 31 | 32 | fun emitTimeCardEffect(effect: TimeCardEffect, scope: CoroutineScope? = null) { 33 | if (scope == null) { 34 | timeCardEffectObserver.emitSync(effect) 35 | } else { 36 | scope.launch { 37 | timeCardEffectObserver.emit(effect) 38 | } 39 | } 40 | } 41 | 42 | fun emitMemoEffect(effect: MemoEffect, scope: CoroutineScope? = null) { 43 | if (scope == null) { 44 | memoEffectObserver.emitSync(effect) 45 | } else { 46 | scope.launch { 47 | memoEffectObserver.emit(effect) 48 | } 49 | } 50 | } 51 | 52 | fun emitWriteMemoEffect(effect: WriteMemoEffect, scope: CoroutineScope? = null) { 53 | if (scope == null) { 54 | writeMemoEffectObserver.emitSync(effect) 55 | } else { 56 | scope.launch { 57 | writeMemoEffectObserver.emit(effect) 58 | } 59 | } 60 | } 61 | 62 | fun emitAddScheduleEffect(effect: AddScheduleEffect, scope: CoroutineScope? = null) { 63 | if (scope == null) { 64 | addScheduleEffectObserver.emitSync(effect) 65 | } else { 66 | scope.launch { 67 | addScheduleEffectObserver.emit(effect) 68 | } 69 | } 70 | } 71 | 72 | fun emitScheduleEffect(effect: ScheduleEffect, scope: CoroutineScope? = null) { 73 | if (scope == null) { 74 | scheduleEffectObserver.emitSync(effect) 75 | } else { 76 | scope.launch { 77 | scheduleEffectObserver.emit(effect) 78 | } 79 | } 80 | } 81 | 82 | fun emitDepositEffect(effect: DepositEffect, scope: CoroutineScope? = null) { 83 | if (scope == null) { 84 | depositEffectObserver.emitSync(effect) 85 | } else { 86 | scope.launch { 87 | depositEffectObserver.emit(effect) 88 | } 89 | } 90 | } 91 | 92 | @Composable 93 | fun observeTimeCardEffect(onEffect: (TimeCardEffect) -> Unit) { 94 | timeCardEffectObserver.observeComposable(onEffect) 95 | } 96 | 97 | @Composable 98 | fun observeMemoEffect(onEffect: (MemoEffect) -> Unit) { 99 | memoEffectObserver.observeComposable(onEffect) 100 | } 101 | 102 | @Composable 103 | fun observeWriteMemoEffect(onEffect: (WriteMemoEffect) -> Unit) { 104 | writeMemoEffectObserver.observeComposable(onEffect) 105 | } 106 | 107 | @Composable 108 | fun observeAddScheduleEffect(onEffect: (AddScheduleEffect) -> Unit) { 109 | addScheduleEffectObserver.observeComposable(onEffect) 110 | } 111 | 112 | @Composable 113 | fun observeScheduleEffect(onEffect: (ScheduleEffect) -> Unit) { 114 | scheduleEffectObserver.observeComposable(onEffect) 115 | } 116 | 117 | @Composable 118 | fun observeDepositEffect(onEffect: (DepositEffect) -> Unit) { 119 | depositEffectObserver.observeComposable(onEffect) 120 | } 121 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/helper/effect/EffectObservers.kt: -------------------------------------------------------------------------------- 1 | package helper.effect 2 | 3 | import model.records.Memo 4 | import model.records.Schedule 5 | 6 | class TimeCardEffectObserver : BaseEffectObserver() 7 | 8 | class MemoEffectObserver : BaseEffectObserver() 9 | 10 | class WriteMemoEffectObserver : BaseEffectObserver() 11 | 12 | class AddScheduleEffectObserver : BaseEffectObserver() 13 | 14 | class ScheduleEffectObserver : BaseEffectObserver() 15 | 16 | class DepositEffectObserver : BaseEffectObserver() 17 | 18 | sealed interface TimeCardEffect { 19 | data object RefreshTodayState : TimeCardEffect 20 | } 21 | 22 | sealed interface MemoEffect { 23 | data object RefreshData : MemoEffect 24 | } 25 | 26 | sealed interface WriteMemoEffect { 27 | data class BeginEdit(val memo: Memo) : WriteMemoEffect 28 | } 29 | 30 | sealed interface AddScheduleEffect { 31 | data class SetDate(val year: Int, val month: Int, val day: Int) : AddScheduleEffect 32 | data class BeginEdit(val schedule: Schedule) : AddScheduleEffect 33 | } 34 | 35 | sealed interface ScheduleEffect { 36 | data object RefreshData : ScheduleEffect 37 | } 38 | 39 | sealed interface DepositEffect { 40 | data object RefreshData : DepositEffect 41 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/model/DoubleToLongSerializer.kt: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.descriptors.PrimitiveKind 5 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 6 | import kotlinx.serialization.descriptors.SerialDescriptor 7 | import kotlinx.serialization.encoding.Decoder 8 | import kotlinx.serialization.encoding.Encoder 9 | 10 | object DoubleToLongSerializer : KSerializer { 11 | override val descriptor: SerialDescriptor 12 | get() = PrimitiveSerialDescriptor("DoubleToLong", PrimitiveKind.LONG) 13 | 14 | override fun deserialize(decoder: Decoder): Long { 15 | return decoder.decodeDouble().toLong() 16 | } 17 | 18 | override fun serialize(encoder: Encoder, value: Long) { 19 | encoder.encodeLong(value) 20 | } 21 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/model/calendar/MonthDay.kt: -------------------------------------------------------------------------------- 1 | package model.calendar 2 | 3 | import kotlinx.datetime.DayOfWeek 4 | import util.CalendarUtil 5 | 6 | data class MonthDay( 7 | val day: Int, // 1..31 8 | val dayOfWeek: DayOfWeek, 9 | val isHoliday: Int = CalendarUtil.NOT_HOLIDAY 10 | ) 11 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/model/display/MemoDisplayItem.kt: -------------------------------------------------------------------------------- 1 | package model.display 2 | 3 | import model.records.Memo 4 | 5 | interface IMemoDisplayItem 6 | 7 | data class GroupDisplayItem(val name: String) : IMemoDisplayItem 8 | 9 | data class MemoDisplayItem(val memo: Memo) : IMemoDisplayItem -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/model/records/DepositRecords.kt: -------------------------------------------------------------------------------- 1 | package model.records 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import model.DoubleToLongSerializer 6 | 7 | @Serializable 8 | data class DepositRecords( 9 | val months: List 10 | ) 11 | 12 | @Serializable 13 | data class DepositMonth( 14 | @Serializable(with = DoubleToLongSerializer::class) 15 | @SerialName("month_start_time") 16 | val monthStartTime: Long = 0L, 17 | @Serializable(with = DoubleToLongSerializer::class) 18 | @SerialName("current_amount") 19 | val currentAmount: Long = 0L, 20 | @Serializable(with = DoubleToLongSerializer::class) 21 | @SerialName("monthly_income") 22 | val monthlyIncome: Long = 0L, 23 | @Serializable(with = DoubleToLongSerializer::class) 24 | @SerialName("extra_deposit") 25 | val extraDeposit: Long = 0L 26 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/model/records/Goals.kt: -------------------------------------------------------------------------------- 1 | package model.records 2 | 3 | const val GOAL_TYPE_TIME = 0 4 | 5 | const val GOAL_TYPE_DEPOSIT = 1 6 | 7 | data class Goal( 8 | val type: Int, 9 | val currentValue: Long = 0L, 10 | val goalValue: Long = 0L 11 | ) { 12 | fun getProgress(): Float { 13 | return currentValue.toFloat() / goalValue 14 | } 15 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/model/records/MemoRecords.kt: -------------------------------------------------------------------------------- 1 | package model.records 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import model.DoubleToLongSerializer 6 | 7 | @Serializable 8 | data class MemoRecords( 9 | val memos: MutableList 10 | ) 11 | 12 | @Serializable 13 | data class Memo( 14 | var text: String = "", 15 | @SerialName("is_todo") 16 | var isTodo: Boolean = false, 17 | @SerialName("is_todo_finished") 18 | var isTodoFinished: Boolean = false, 19 | @SerialName("is_pin") 20 | var isPin: Boolean = false, 21 | @Serializable(with = DoubleToLongSerializer::class) 22 | @SerialName("create_time") 23 | val createTime: Long = 0L, 24 | @Serializable(with = DoubleToLongSerializer::class) 25 | @SerialName("modify_time") 26 | var modifyTime: Long = 0L, 27 | @SerialName("group") 28 | var group: String? = null 29 | ) 30 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/model/records/ScheduleRecords.kt: -------------------------------------------------------------------------------- 1 | package model.records 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import model.DoubleToLongSerializer 6 | 7 | @Serializable 8 | data class ScheduleRecords( 9 | val schedules: MutableList 10 | ) 11 | 12 | @Serializable 13 | data class Schedule( 14 | var text: String = "", 15 | @Serializable(with = DoubleToLongSerializer::class) 16 | @SerialName("day_start_time") 17 | var dayStartTime: Long = 0L, 18 | @Serializable(with = DoubleToLongSerializer::class) 19 | @SerialName("starting_time") 20 | var startingTime: Long = 0L, 21 | @Serializable(with = DoubleToLongSerializer::class) 22 | @SerialName("ending_time") 23 | var endingTime: Long = 0L, 24 | @SerialName("is_all_day") 25 | var isAllDay: Boolean = false, 26 | @SerialName("is_milestone") 27 | var isMilestone: Boolean = false, 28 | @Serializable(with = DoubleToLongSerializer::class) 29 | @SerialName("milestone_goal") 30 | var milestoneGoal: Long = 0L 31 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/model/records/TimeCardRecords.kt: -------------------------------------------------------------------------------- 1 | package model.records 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import model.DoubleToLongSerializer 6 | 7 | @Serializable 8 | data class TimeCardRecords( 9 | val days: MutableList 10 | ) 11 | 12 | @Serializable 13 | data class TimeCardDay( 14 | @Serializable(with = DoubleToLongSerializer::class) 15 | @SerialName("day_start_time") 16 | val dayStartTime: Long, 17 | @Serializable(with = DoubleToLongSerializer::class) 18 | @SerialName("latest_time_card") 19 | var latestTimeCard: Long, 20 | @Serializable(with = DoubleToLongSerializer::class) 21 | @SerialName("latest_time_run") 22 | var latestTimeRun: Long? = null 23 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/model/request/DepositSyncRequest.kt: -------------------------------------------------------------------------------- 1 | package model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | import model.records.DepositMonth 5 | 6 | @Serializable 7 | data class DepositSyncRequest( 8 | val username: String, 9 | val depositMonths: List 10 | ) 11 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/model/request/LoginRequest.kt: -------------------------------------------------------------------------------- 1 | package model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class LoginRequest( 7 | val username: String, 8 | val password: String 9 | ) 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/model/request/MemoSyncRequest.kt: -------------------------------------------------------------------------------- 1 | package model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | import model.records.Memo 5 | 6 | @Serializable 7 | data class MemoSyncRequest( 8 | val username: String, 9 | val memos: List 10 | ) 11 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/model/request/RegisterRequest.kt: -------------------------------------------------------------------------------- 1 | package model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class RegisterRequest( 7 | val username: String, 8 | val email: String, 9 | val password: String 10 | ) 11 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/model/request/ScheduleSyncRequest.kt: -------------------------------------------------------------------------------- 1 | package model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | import model.records.Schedule 5 | 6 | @Serializable 7 | data class ScheduleSyncRequest( 8 | val username: String, 9 | val schedules: List 10 | ) 11 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/model/request/TimeCardSyncRequest.kt: -------------------------------------------------------------------------------- 1 | package model.request 2 | 3 | import kotlinx.serialization.Serializable 4 | import model.records.TimeCardRecords 5 | 6 | @Serializable 7 | data class TimeCardSyncRequest( 8 | val username: String, 9 | val timeCard: TimeCardRecords 10 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/store/AppFlowStore.kt: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import FLOW_PREFERENCES_NAME 4 | import com.tangping.kotstore.model.KotStoreFlowModel 5 | 6 | object AppFlowStore : KotStoreFlowModel(storeName = FLOW_PREFERENCES_NAME) { 7 | val minWorkingHoursFlow by floatFlowStore(key = "min_working_hours_flow", default = 9.5f) 8 | val minOvertimeHoursFlow by floatFlowStore(key = "min_overtime_hours_flow", default = 1f) 9 | val autoSyncFlow by booleanFlowStore(key = "auto_sync_flow", default = false) 10 | val totalDepositGoalFlow by longFlowStore(key = "total_deposit_goal_flow", default = 0L) 11 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/store/AppStore.kt: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import PREFERENCES_NAME 4 | import com.tangping.kotstore.model.KotStoreModel 5 | import kotlinx.coroutines.runBlocking 6 | 7 | object AppStore : KotStoreModel(storeName = PREFERENCES_NAME) { 8 | var loginToken by stringStore(key = "login_token", default = "") 9 | var loginUsername by stringStore(key = "login_username", default = "") 10 | var loginPassword by stringStore(key = "login_password", default = "") 11 | var customServerUrl by stringStore(key = "custom_server_url", default = "") 12 | var timeCards by stringStore(key = "time_cards", default = "{}", syncSave = true) 13 | var memos by stringStore(key = "memos", default = "{}", syncSave = true) 14 | var schedules by stringStore(key = "schedules", default = "{}", syncSave = true) 15 | var depositMonths by stringStore(key = "deposit_months", default = "{}", syncSave = true) 16 | var lastSync by longStore(key = "last_sync", default = 0L) 17 | var minWorkingHours by floatStore(key = "min_working_hours", default = 9.5f) 18 | var minOvertimeHours by floatStore(key = "min_overtime_hours", default = 1f) 19 | var totalDepositGoal by longStore(key = "total_deposit_goal", default = 0L) 20 | 21 | fun clearCache() { 22 | customServerUrl = "" 23 | timeCards = "{}" 24 | memos = "{}" 25 | schedules = "{}" 26 | depositMonths = "{}" 27 | lastSync = 0L 28 | } 29 | 30 | fun setMinWorkingHoursWithFlow(hours: Float) { 31 | minWorkingHours = hours 32 | runBlocking { 33 | AppFlowStore.minWorkingHoursFlow.emit(hours) 34 | } 35 | } 36 | 37 | fun setMinOvertimeHoursWithFlow(hours: Float) { 38 | minOvertimeHours = hours 39 | runBlocking { 40 | AppFlowStore.minOvertimeHoursFlow.emit(hours) 41 | } 42 | } 43 | 44 | fun setTotalDepositGoalWithFlow(goal: Long) { 45 | totalDepositGoal = goal 46 | runBlocking { 47 | AppFlowStore.totalDepositGoalFlow.emit(goal) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/store/CurrentProcessStore.kt: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import kotlinx.coroutines.flow.MutableStateFlow 4 | 5 | /** 6 | * Data stored only for the current process. 7 | */ 8 | object CurrentProcessStore { 9 | var screenWidthPixels = MutableStateFlow(0) 10 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/dialog/CloudServerDialog.kt: -------------------------------------------------------------------------------- 1 | package ui.dialog 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.material.Button 12 | import androidx.compose.material.OutlinedButton 13 | import androidx.compose.material.Text 14 | import androidx.compose.material.TextField 15 | import androidx.compose.material.TextFieldDefaults 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.derivedStateOf 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.runtime.setValue 22 | import androidx.compose.ui.Alignment 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.unit.dp 27 | import androidx.compose.ui.unit.sp 28 | import androidx.compose.ui.window.Dialog 29 | import extension.isValidUrl 30 | import global.AppColors 31 | import org.jetbrains.compose.resources.stringResource 32 | import store.AppStore 33 | import zhoutools.composeapp.generated.resources.Res 34 | import zhoutools.composeapp.generated.resources.cancel 35 | import zhoutools.composeapp.generated.resources.confirm 36 | import zhoutools.composeapp.generated.resources.server_settings 37 | import zhoutools.composeapp.generated.resources.server_settings_hint 38 | 39 | @Composable 40 | fun CloudServerDialog(onCancel: () -> Unit, onConfirm: (String) -> Unit) { 41 | var inputUrl by remember { mutableStateOf(AppStore.customServerUrl) } 42 | val isValidUrl by remember(inputUrl) { 43 | derivedStateOf { inputUrl.isEmpty() || inputUrl.isValidUrl() } 44 | } 45 | 46 | Dialog(onDismissRequest = {}) { 47 | Column(modifier = Modifier 48 | .fillMaxWidth() 49 | .background(Color.White, shape = RoundedCornerShape(8.dp)) 50 | ) { 51 | Row( 52 | modifier = Modifier 53 | .padding(all = 16.dp) 54 | .fillMaxWidth(), 55 | verticalAlignment = Alignment.CenterVertically 56 | ) { 57 | Text( 58 | text = stringResource(Res.string.server_settings), 59 | fontWeight = FontWeight.Bold, 60 | fontSize = 20.sp 61 | ) 62 | 63 | Spacer(modifier = Modifier.weight(1f)) 64 | } 65 | 66 | Text( 67 | text = stringResource(Res.string.server_settings_hint), 68 | modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp) 69 | ) 70 | 71 | TextField( 72 | value = inputUrl, 73 | onValueChange = { 74 | inputUrl = it 75 | }, 76 | modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), 77 | colors = TextFieldDefaults.textFieldColors( 78 | focusedIndicatorColor = Color.Transparent, 79 | unfocusedIndicatorColor = Color.Transparent, 80 | disabledIndicatorColor = Color.Transparent, 81 | textColor = if (isValidUrl) AppColors.DarkGreen else AppColors.Red 82 | ) 83 | ) 84 | 85 | Row( 86 | modifier = Modifier 87 | .padding(start = 16.dp, top = 16.dp, bottom = 16.dp, end = 16.dp) 88 | .fillMaxWidth(), 89 | verticalAlignment = Alignment.CenterVertically 90 | ) { 91 | OutlinedButton( 92 | onClick = onCancel, 93 | modifier = Modifier.weight(1f) 94 | ) { 95 | Text( 96 | text = stringResource(Res.string.cancel).uppercase(), 97 | fontSize = 16.sp 98 | ) 99 | } 100 | 101 | Spacer(modifier = Modifier.width(16.dp)) 102 | 103 | Button( 104 | onClick = { 105 | onConfirm(inputUrl) 106 | }, 107 | modifier = Modifier.weight(1f) 108 | ) { 109 | Text( 110 | text = stringResource(Res.string.confirm).uppercase(), 111 | fontSize = 16.sp 112 | ) 113 | } 114 | } 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/dialog/ConfirmDialog.kt: -------------------------------------------------------------------------------- 1 | package ui.dialog 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.layout.width 13 | import androidx.compose.foundation.shape.CircleShape 14 | import androidx.compose.foundation.shape.RoundedCornerShape 15 | import androidx.compose.material.Button 16 | import androidx.compose.material.Icon 17 | import androidx.compose.material.OutlinedButton 18 | import androidx.compose.material.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.draw.clip 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.text.font.FontWeight 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | import androidx.compose.ui.window.Dialog 28 | import org.jetbrains.compose.resources.painterResource 29 | import org.jetbrains.compose.resources.stringResource 30 | import zhoutools.composeapp.generated.resources.Res 31 | import zhoutools.composeapp.generated.resources.cancel 32 | import zhoutools.composeapp.generated.resources.confirm 33 | import zhoutools.composeapp.generated.resources.ic_close 34 | 35 | @Composable 36 | fun ConfirmDialog( 37 | title: String, 38 | content: String, 39 | cancel: String? = null, 40 | confirm: String? = null, 41 | onCancel: () -> Unit, 42 | onConfirm: () -> Unit, 43 | onDismiss: (() -> Unit)? = null 44 | ) { 45 | Dialog(onDismissRequest = {}) { 46 | Column(modifier = Modifier 47 | .fillMaxWidth() 48 | .background(Color.White, shape = RoundedCornerShape(8.dp)) 49 | ) { 50 | Row( 51 | modifier = Modifier 52 | .padding(all = 16.dp) 53 | .fillMaxWidth(), 54 | verticalAlignment = Alignment.CenterVertically 55 | ) { 56 | Text( 57 | text = title, 58 | fontWeight = FontWeight.Bold, 59 | fontSize = 20.sp 60 | ) 61 | 62 | Spacer(modifier = Modifier.weight(1f)) 63 | 64 | if (onDismiss != null) { 65 | DismissButton(onDismiss) 66 | } 67 | } 68 | 69 | Text( 70 | text = content, 71 | fontSize = 16.sp, 72 | modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp) 73 | ) 74 | 75 | Row( 76 | modifier = Modifier 77 | .padding(start = 16.dp, top = 8.dp, bottom = 16.dp, end = 16.dp) 78 | .fillMaxWidth(), 79 | verticalAlignment = Alignment.CenterVertically 80 | ) { 81 | OutlinedButton( 82 | onClick = onCancel, 83 | modifier = Modifier.weight(1f) 84 | ) { 85 | Text( 86 | text = (cancel ?: stringResource(Res.string.cancel)).uppercase(), 87 | fontSize = 16.sp 88 | ) 89 | } 90 | 91 | Spacer(modifier = Modifier.width(16.dp)) 92 | 93 | Button( 94 | onClick = onConfirm, 95 | modifier = Modifier.weight(1f) 96 | ) { 97 | Text( 98 | text = (confirm ?: stringResource(Res.string.confirm)).uppercase(), 99 | fontSize = 16.sp 100 | ) 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | @Composable 108 | fun DismissButton(onDismiss: () -> Unit) { 109 | Box( 110 | modifier = Modifier 111 | .size(28.dp) 112 | .clip(CircleShape) 113 | .clickable { 114 | onDismiss() 115 | }, 116 | contentAlignment = Alignment.Center 117 | ) { 118 | Icon( 119 | painter = painterResource(Res.drawable.ic_close), 120 | contentDescription = null, 121 | tint = Color.Unspecified, 122 | modifier = Modifier.size(20.dp) 123 | ) 124 | } 125 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/dialog/DepositGoalDialog.kt: -------------------------------------------------------------------------------- 1 | package ui.dialog 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.foundation.text.KeyboardOptions 12 | import androidx.compose.material.Button 13 | import androidx.compose.material.OutlinedButton 14 | import androidx.compose.material.Text 15 | import androidx.compose.material.TextField 16 | import androidx.compose.material.TextFieldDefaults 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.derivedStateOf 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.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.graphics.Color 26 | import androidx.compose.ui.text.SpanStyle 27 | import androidx.compose.ui.text.buildAnnotatedString 28 | import androidx.compose.ui.text.font.FontWeight 29 | import androidx.compose.ui.text.input.KeyboardType 30 | import androidx.compose.ui.text.withStyle 31 | import androidx.compose.ui.unit.dp 32 | import androidx.compose.ui.unit.sp 33 | import androidx.compose.ui.window.Dialog 34 | import global.AppColors 35 | import org.jetbrains.compose.resources.stringResource 36 | import store.AppStore 37 | import zhoutools.composeapp.generated.resources.Res 38 | import zhoutools.composeapp.generated.resources.cancel 39 | import zhoutools.composeapp.generated.resources.confirm 40 | import zhoutools.composeapp.generated.resources.enter_your_x_below 41 | import zhoutools.composeapp.generated.resources.total_deposit_goal 42 | 43 | @Composable 44 | fun DepositGoalDialog(onCancel: () -> Unit, onConfirm: (Long?) -> Unit) { 45 | var totalDepositGoal by remember { mutableStateOf(AppStore.totalDepositGoal.toString()) } 46 | val isValidGoal by remember(totalDepositGoal) { 47 | derivedStateOf { totalDepositGoal.isNotEmpty() && totalDepositGoal.toLongOrNull() != null } 48 | } 49 | 50 | Dialog(onDismissRequest = {}) { 51 | Column(modifier = Modifier 52 | .fillMaxWidth() 53 | .background(Color.White, shape = RoundedCornerShape(8.dp)) 54 | ) { 55 | val annotatedString = buildAnnotatedString { 56 | append(stringResource(Res.string.enter_your_x_below).substringBefore("%s")) 57 | 58 | withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { 59 | append(stringResource(Res.string.total_deposit_goal)) 60 | } 61 | 62 | append(stringResource(Res.string.enter_your_x_below).substringAfter("%s")) 63 | } 64 | 65 | Text( 66 | text = annotatedString, 67 | modifier = Modifier 68 | .align(Alignment.CenterHorizontally) 69 | .padding(start = 16.dp, end = 16.dp, bottom = 16.dp, top = 16.dp), 70 | ) 71 | 72 | TextField( 73 | value = totalDepositGoal, 74 | onValueChange = { 75 | totalDepositGoal = it 76 | }, 77 | modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), 78 | colors = TextFieldDefaults.textFieldColors( 79 | focusedIndicatorColor = Color.Transparent, 80 | unfocusedIndicatorColor = Color.Transparent, 81 | disabledIndicatorColor = Color.Transparent, 82 | textColor = if (isValidGoal) AppColors.DarkGreen else AppColors.Red 83 | ), 84 | maxLines = 1, 85 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) 86 | ) 87 | 88 | Row( 89 | modifier = Modifier 90 | .padding(start = 16.dp, top = 16.dp, bottom = 16.dp, end = 16.dp) 91 | .fillMaxWidth(), 92 | verticalAlignment = Alignment.CenterVertically 93 | ) { 94 | OutlinedButton( 95 | onClick = onCancel, 96 | modifier = Modifier.weight(1f) 97 | ) { 98 | Text( 99 | text = stringResource(Res.string.cancel).uppercase(), 100 | fontSize = 16.sp 101 | ) 102 | } 103 | 104 | Spacer(modifier = Modifier.width(16.dp)) 105 | 106 | Button( 107 | onClick = { 108 | if (totalDepositGoal.isEmpty()) { 109 | onConfirm(0) 110 | } else { 111 | onConfirm(totalDepositGoal.toLongOrNull()) 112 | } 113 | }, 114 | modifier = Modifier.weight(1f) 115 | ) { 116 | Text( 117 | text = stringResource(Res.string.confirm).uppercase(), 118 | fontSize = 16.sp 119 | ) 120 | } 121 | } 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/fragment/DepositPresenter.kt: -------------------------------------------------------------------------------- 1 | package ui.fragment 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.collectAsState 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableFloatStateOf 8 | import androidx.compose.runtime.mutableLongStateOf 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.setValue 12 | import extension.toMonthYearString 13 | import helper.DepositHelper 14 | import helper.SyncHelper 15 | import kotlinx.coroutines.flow.Flow 16 | import model.records.DepositMonth 17 | import model.records.DepositRecords 18 | import moe.tlaster.precompose.molecule.collectAction 19 | import store.AppFlowStore 20 | import ui.fragment.DepositState.Companion.toDeque 21 | import util.TimeUtil 22 | 23 | @Composable 24 | fun DepositPresenter(actionFlow: Flow): DepositState { 25 | var currentAmount by remember { mutableLongStateOf(0L) } 26 | var progress by remember { mutableFloatStateOf(0f) } 27 | var displayDeque by remember { mutableStateOf(ArrayDeque()) } 28 | val depositGoal = AppFlowStore.totalDepositGoalFlow.collectAsState(initial = 0L).value 29 | 30 | fun refreshData() { 31 | val depositMonths = DepositHelper.getMonths() 32 | val deque = DepositRecords(months = depositMonths).toDeque() 33 | displayDeque = deque 34 | displayDeque.firstOrNull()?.let { 35 | currentAmount = it.currentAmount + it.extraDeposit 36 | } 37 | if (displayDeque.isEmpty()) { 38 | currentAmount = 0L 39 | } 40 | } 41 | 42 | LaunchedEffect(Unit) { 43 | refreshData() 44 | } 45 | 46 | LaunchedEffect(depositGoal, currentAmount) { 47 | progress = if (depositGoal == 0L) { 48 | 0f 49 | } else { 50 | currentAmount.toFloat() / (depositGoal.toFloat() * 100) 51 | } 52 | } 53 | 54 | actionFlow.collectAction { 55 | when (this) { 56 | is DepositAction.AddMonth -> { 57 | DepositHelper.addMonth(month) 58 | SyncHelper.autoPushDeposit() 59 | refreshData() 60 | } 61 | 62 | is DepositAction.RemoveMonth -> { 63 | DepositHelper.removeMonth(month) 64 | SyncHelper.autoPushDeposit() 65 | refreshData() 66 | } 67 | 68 | is DepositAction.RefreshData -> { 69 | refreshData() 70 | } 71 | } 72 | } 73 | 74 | return DepositState(currentAmount, progress, displayDeque) 75 | } 76 | 77 | data class DepositState( 78 | val currentAmount: Long = 0L, 79 | val progress: Float = 0f, 80 | val displayDeque: ArrayDeque = ArrayDeque() 81 | ) { 82 | companion object { 83 | fun DepositRecords.toDeque(): ArrayDeque { 84 | val sortedMonths = this.months.sortedByDescending { it.monthStartTime } 85 | val deque = ArrayDeque() 86 | sortedMonths.forEachIndexed { index, month -> 87 | val monthStr = month.monthStartTime.toMonthYearString() 88 | val currAmount = month.currentAmount 89 | val monthIncome = month.monthlyIncome 90 | val balance = currAmount - monthIncome 91 | val extraAmount = month.extraDeposit 92 | var balanceDiff: Long? = null 93 | if (index < sortedMonths.lastIndex) { 94 | val nextMonth = sortedMonths[index + 1] 95 | val nextMonthBalance = nextMonth.currentAmount - nextMonth.monthlyIncome 96 | balanceDiff = balance - nextMonthBalance 97 | } 98 | deque.add(DepositDisplayRecord(monthStr, currAmount, monthIncome, balance, extraAmount, balanceDiff)) 99 | } 100 | return deque 101 | } 102 | } 103 | } 104 | 105 | data class DepositDisplayRecord( 106 | val monthStr: String = "", 107 | val currentAmount: Long = 0L, 108 | val monthlyIncome: Long = 0L, 109 | val balance: Long = 0L, 110 | val extraDeposit: Long = 0L, 111 | val balanceDiff: Long? = null 112 | ) { 113 | fun toDepositMonth(): DepositMonth? { 114 | val monthStartTime = TimeUtil.monthYearStringToMonthStartTime(monthStr) 115 | return if (monthStartTime != null) { 116 | DepositMonth(monthStartTime, currentAmount, monthlyIncome, extraDeposit) 117 | } else { 118 | null 119 | } 120 | } 121 | } 122 | 123 | sealed interface DepositAction { 124 | data class AddMonth(val month: DepositMonth) : DepositAction 125 | data class RemoveMonth(val month: DepositMonth) : DepositAction 126 | data object RefreshData : DepositAction 127 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/fragment/MemoPresenter.kt: -------------------------------------------------------------------------------- 1 | package ui.fragment 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableIntStateOf 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.setValue 10 | import extension.dayStartTime 11 | import helper.DepositHelper 12 | import helper.MemoHelper 13 | import helper.ScheduleHelper 14 | import helper.SyncHelper 15 | import helper.effect.EffectHelper 16 | import helper.effect.WriteMemoEffect 17 | import kotlinx.coroutines.flow.Flow 18 | import logger 19 | import model.display.IMemoDisplayItem 20 | import model.records.DepositRecords 21 | import model.records.GOAL_TYPE_DEPOSIT 22 | import model.records.GOAL_TYPE_TIME 23 | import model.records.Goal 24 | import model.records.Memo 25 | import moe.tlaster.precompose.molecule.collectAction 26 | import store.AppStore 27 | import ui.fragment.DepositState.Companion.toDeque 28 | import util.TimeUtil 29 | 30 | const val MODE_MEMO = 0 31 | 32 | const val MODE_GOALS = 1 33 | 34 | @Composable 35 | fun MemoPresenter(actionFlow: Flow, onGoEdit: () -> Unit): MemoState { 36 | var displayList by remember { mutableStateOf(MemoHelper.getDisplayList()) } 37 | var curMemo by remember { mutableStateOf(null) } 38 | var showBottomSheet by remember { mutableStateOf(false) } 39 | var mode by remember { mutableIntStateOf(MODE_MEMO) } 40 | var goalList by remember { mutableStateOf(emptyList()) } 41 | 42 | fun initGoalList() { 43 | val tempGoalList = mutableListOf() 44 | tempGoalList.clear() 45 | 46 | // Deposit Goal 47 | val depositMonths = DepositHelper.getMonths() 48 | val deque = DepositRecords(months = depositMonths).toDeque() 49 | deque.firstOrNull()?.let { 50 | val currentDeposit = it.currentAmount + it.extraDeposit 51 | val goalDeposit = AppStore.totalDepositGoal * 100L 52 | if (goalDeposit > 0) { 53 | tempGoalList.add(Goal(GOAL_TYPE_DEPOSIT, currentDeposit, goalDeposit)) 54 | } 55 | } 56 | 57 | // Time Goals 58 | val scheduleList = ScheduleHelper.getDisplayList() 59 | scheduleList.forEach { schedule -> 60 | val todayStartTime = TimeUtil.currentTimeMillis().dayStartTime() 61 | val diffTime = todayStartTime - schedule.dayStartTime 62 | if (diffTime > 0 && schedule.milestoneGoal > 0) { 63 | tempGoalList.add(Goal(GOAL_TYPE_TIME, diffTime, schedule.milestoneGoal)) 64 | } 65 | } 66 | 67 | goalList = tempGoalList 68 | logger.i { "goalList: $goalList" } 69 | } 70 | 71 | fun clickMemoItem(memo: Memo) { 72 | if (memo.isTodo) { 73 | curMemo = memo 74 | showBottomSheet = true 75 | } else { 76 | EffectHelper.emitWriteMemoEffect(WriteMemoEffect.BeginEdit(memo)) 77 | onGoEdit() 78 | } 79 | } 80 | 81 | fun clickEdit() { 82 | curMemo?.let { 83 | EffectHelper.emitWriteMemoEffect(WriteMemoEffect.BeginEdit(it)) 84 | onGoEdit() 85 | } 86 | } 87 | 88 | LaunchedEffect(Unit) { 89 | initGoalList() 90 | } 91 | 92 | actionFlow.collectAction { 93 | when (this) { 94 | is MemoAction.ClickMemoItem -> { 95 | clickMemoItem(memo) 96 | } 97 | 98 | is MemoAction.ClickEdit -> { 99 | clickEdit() 100 | showBottomSheet = false 101 | } 102 | 103 | is MemoAction.MarkDone -> { 104 | curMemo?.let { 105 | MemoHelper.markDone(it, !it.isTodoFinished) 106 | SyncHelper.autoPushMemo() 107 | curMemo = it.copy(isTodoFinished = !it.isTodoFinished) 108 | displayList = MemoHelper.getDisplayList() 109 | } 110 | showBottomSheet = false 111 | } 112 | 113 | is MemoAction.HideBottomSheet -> { 114 | showBottomSheet = false 115 | } 116 | 117 | is MemoAction.RefreshDisplayList -> { 118 | displayList = MemoHelper.getDisplayList() 119 | } 120 | 121 | is MemoAction.SwitchMode -> { 122 | if (newMode != mode) { 123 | showBottomSheet = false 124 | } 125 | mode = newMode 126 | } 127 | } 128 | } 129 | 130 | return MemoState(displayList, curMemo, showBottomSheet, mode, goalList.toList()) 131 | } 132 | 133 | data class MemoState( 134 | val displayList: List = emptyList(), 135 | val curMemo: Memo? = null, 136 | val showBottomSheet: Boolean = false, 137 | val mode: Int = MODE_MEMO, 138 | val goalList: List = emptyList() 139 | ) 140 | 141 | sealed interface MemoAction { 142 | data class ClickMemoItem(val memo: Memo) : MemoAction 143 | data object ClickEdit : MemoAction 144 | data object MarkDone : MemoAction 145 | data object HideBottomSheet : MemoAction 146 | data object RefreshDisplayList : MemoAction 147 | data class SwitchMode(val newMode: Int) : MemoAction 148 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/fragment/SchedulePresenter.kt: -------------------------------------------------------------------------------- 1 | package ui.fragment 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.collectAsState 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableIntStateOf 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.runtime.rememberCoroutineScope 11 | import androidx.compose.runtime.setValue 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.IO 14 | import kotlinx.coroutines.flow.Flow 15 | import kotlinx.coroutines.launch 16 | import kotlinx.datetime.Clock 17 | import kotlinx.datetime.TimeZone 18 | import kotlinx.datetime.toLocalDateTime 19 | import model.calendar.MonthDay 20 | import moe.tlaster.precompose.molecule.collectAction 21 | import util.CalendarUtil 22 | 23 | @Composable 24 | fun SchedulePresenter(actionFlow: Flow): ScheduleState { 25 | var currYear by remember { mutableIntStateOf(0) } 26 | var currMonthOfYear by remember { mutableIntStateOf(0) } 27 | var currMonthDays by remember { mutableStateOf>(emptyList()) } 28 | var prevMonthDays by remember { mutableStateOf>(emptyList()) } 29 | var selectDate by remember { mutableStateOf(Triple(0, 1, 1)) } 30 | val holidayMap = CalendarUtil.getHolidayMap().collectAsState().value 31 | val scope = rememberCoroutineScope() 32 | 33 | fun refreshMonthDays() { 34 | if (!holidayMap.containsKey(currYear)) { 35 | scope.launch(Dispatchers.IO) { 36 | CalendarUtil.fetchHolidayMap(currYear) 37 | } 38 | } 39 | 40 | val monthDays = CalendarUtil.getMonthDays(currYear, currMonthOfYear) 41 | val monthDayList = mutableListOf() 42 | monthDays.forEach { 43 | val isHoliday = CalendarUtil.isHoliday(currYear, currMonthOfYear, it.first) 44 | monthDayList.add(MonthDay(it.first, it.second, isHoliday)) 45 | } 46 | val prevMonth = if (currMonthOfYear == 1) { 47 | CalendarUtil.getMonthDays(currYear - 1, 12) 48 | } else { 49 | CalendarUtil.getMonthDays(currYear, currMonthOfYear - 1) 50 | } 51 | val prevMonthList = mutableListOf() 52 | prevMonth.forEach { 53 | val isHoliday = CalendarUtil.isHoliday( 54 | if (currMonthOfYear == 1) currYear - 1 else currYear, 55 | if (currMonthOfYear == 1) 12 else currMonthOfYear - 1, 56 | it.first 57 | ) 58 | prevMonthList.add(MonthDay(it.first, it.second, isHoliday)) 59 | } 60 | currMonthDays = monthDayList 61 | prevMonthDays = prevMonthList 62 | } 63 | 64 | fun init() { 65 | val currentDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date 66 | currYear = currentDate.year 67 | currMonthOfYear = currentDate.monthNumber 68 | selectDate = Triple(currYear, currMonthOfYear, currentDate.dayOfMonth) 69 | refreshMonthDays() 70 | } 71 | 72 | fun goPrevMonth() { 73 | if (currMonthOfYear == 1) { 74 | currYear-- 75 | currMonthOfYear = 12 76 | } else { 77 | currMonthOfYear-- 78 | } 79 | refreshMonthDays() 80 | } 81 | 82 | fun goNextMonth() { 83 | if (currMonthOfYear == 12) { 84 | currYear++ 85 | currMonthOfYear = 1 86 | } else { 87 | currMonthOfYear++ 88 | } 89 | refreshMonthDays() 90 | } 91 | 92 | LaunchedEffect(Unit) { 93 | init() 94 | } 95 | 96 | LaunchedEffect(holidayMap) { 97 | if (holidayMap.isNotEmpty()) { 98 | refreshMonthDays() 99 | } 100 | } 101 | 102 | actionFlow.collectAction { 103 | when (this) { 104 | is ScheduleAction.GoPrevMonth -> { 105 | goPrevMonth() 106 | } 107 | is ScheduleAction.GoNextMonth -> { 108 | goNextMonth() 109 | } 110 | is ScheduleAction.SelectDay -> { 111 | selectDate = date 112 | } 113 | } 114 | } 115 | 116 | return ScheduleState(currYear, currMonthOfYear, currMonthDays, prevMonthDays, selectDate) 117 | } 118 | 119 | data class ScheduleState( 120 | val currYear: Int, 121 | val currMonthOfYear: Int, // 1..12 122 | val currMonthDays: List = emptyList(), 123 | val prevMonthDays: List = emptyList(), 124 | val selectDate: Triple // year, month (1..12), day (1..31) 125 | ) { 126 | @Composable 127 | fun getCurrMonthName(): String { 128 | val index = currMonthOfYear - 1 129 | val monthNames = CalendarUtil.getMonthNames() 130 | return if (index in monthNames.indices) { 131 | monthNames[index] 132 | } else "" 133 | } 134 | 135 | fun isToday(dayOfMonth: Int): Boolean { 136 | val todayDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date 137 | return currYear == todayDate.year && currMonthOfYear == todayDate.monthNumber && dayOfMonth == todayDate.dayOfMonth 138 | } 139 | 140 | fun isSelect(dayOfMonth: Int): Boolean { 141 | return selectDate.first == currYear && selectDate.second == currMonthOfYear && selectDate.third == dayOfMonth 142 | } 143 | } 144 | 145 | sealed interface ScheduleAction { 146 | data object GoPrevMonth : ScheduleAction 147 | data object GoNextMonth : ScheduleAction 148 | data class SelectDay(val date: Triple) : ScheduleAction 149 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/fragment/TimeCardPresenter.kt: -------------------------------------------------------------------------------- 1 | package ui.fragment 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableLongStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.runtime.setValue 9 | import helper.SyncHelper 10 | import kotlinx.coroutines.delay 11 | import kotlinx.coroutines.flow.Flow 12 | import logger 13 | import moe.tlaster.precompose.molecule.collectAction 14 | import helper.TimeCardHelper 15 | import util.TimeUtil 16 | 17 | @Composable 18 | fun TimeCardPresenter(actionFlow: Flow): TimeCardState { 19 | var currentTime by remember { mutableLongStateOf(0L) } 20 | var todayTimeCard by remember { mutableLongStateOf(0L) } 21 | var todayWorkTime by remember { mutableLongStateOf(0L) } 22 | var todayRunTime by remember { mutableLongStateOf(0L) } 23 | 24 | fun refreshTodayState() { 25 | todayTimeCard = TimeCardHelper.todayTimeCard() ?: 0L 26 | todayRunTime = TimeCardHelper.todayTimeRun() ?: 0L 27 | logger.i { "todayTimeCard=$todayTimeCard, todayRunTime=$todayRunTime" } 28 | } 29 | 30 | LaunchedEffect(Unit) { 31 | // init 32 | refreshTodayState() 33 | } 34 | 35 | LaunchedEffect(Unit) { 36 | // update time 37 | while (true) { 38 | delay(500) 39 | currentTime = TimeUtil.currentTimeMillis() 40 | if (todayTimeCard != 0L) { 41 | todayWorkTime = if (todayRunTime == 0L) { 42 | currentTime - todayTimeCard 43 | } else { 44 | todayRunTime - todayTimeCard 45 | } 46 | } 47 | } 48 | } 49 | 50 | actionFlow.collectAction { 51 | when (this) { 52 | is TimeCardAction.PressTimeCard -> { 53 | TimeCardHelper.pressTimeCard() 54 | refreshTodayState() 55 | SyncHelper.autoPushTimeCard() 56 | } 57 | 58 | is TimeCardAction.Run -> { 59 | if (TimeCardHelper.run()) { 60 | refreshTodayState() 61 | SyncHelper.autoPushTimeCard() 62 | } 63 | } 64 | 65 | is TimeCardAction.RefreshTodayState -> { 66 | refreshTodayState() 67 | } 68 | } 69 | } 70 | 71 | return TimeCardState(currentTime, todayTimeCard, todayWorkTime, todayRunTime) 72 | } 73 | 74 | data class TimeCardState( 75 | val currentTime: Long = 0L, 76 | val todayTimeCard: Long = 0L, 77 | val todayWorkTime: Long = 0L, 78 | val todayRunTime: Long = 0L 79 | ) 80 | 81 | sealed interface TimeCardAction { 82 | data object PressTimeCard : TimeCardAction 83 | data object Run : TimeCardAction 84 | data object RefreshTodayState : TimeCardAction 85 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/scene/AddSchedulePresenter.kt: -------------------------------------------------------------------------------- 1 | package ui.scene 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableIntStateOf 6 | import androidx.compose.runtime.mutableLongStateOf 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.setValue 10 | import helper.effect.ScheduleEffect 11 | import extension.dayStartTime 12 | import extension.getDayOfMonth 13 | import extension.getHour 14 | import extension.getMinute 15 | import extension.getMonthOfYear 16 | import extension.getYear 17 | import helper.ScheduleHelper 18 | import helper.SyncHelper 19 | import helper.effect.EffectHelper 20 | import kotlinx.coroutines.flow.Flow 21 | import model.records.Schedule 22 | import moe.tlaster.precompose.molecule.collectAction 23 | import util.CalendarUtil 24 | import util.TimeUtil 25 | 26 | enum class TimeEditType { 27 | START_TIME, END_TIME 28 | } 29 | 30 | @Composable 31 | fun AddSchedulePresenter(actionFlow: Flow): AddScheduleState { 32 | var year by remember { mutableIntStateOf(0) } 33 | var monthOfYear by remember { mutableIntStateOf(0) } 34 | var dayOfMonth by remember { mutableIntStateOf(0) } 35 | var text by remember { mutableStateOf("") } 36 | var startTime by remember { mutableLongStateOf(TimeUtil.currentTimeMillis()) } 37 | var endTime by remember { mutableLongStateOf(TimeUtil.currentTimeMillis()) } 38 | var isAllDay by remember { mutableStateOf(false) } 39 | var isMilestone by remember { mutableStateOf(false) } 40 | var timeEditType by remember { mutableStateOf(null) } 41 | var isEdit by remember { mutableStateOf(false) } 42 | var milestoneGoalMillis by remember { mutableLongStateOf(0L) } 43 | var editItem by remember { mutableStateOf(null) } 44 | 45 | fun setDate(dateTriple: Triple) { 46 | year = dateTriple.first 47 | monthOfYear = dateTriple.second 48 | dayOfMonth = dateTriple.third 49 | startTime = TimeUtil.toEpochMillis(year, monthOfYear, dayOfMonth, startTime.getHour(), startTime.getMinute()) 50 | endTime = TimeUtil.toEpochMillis(year, monthOfYear, dayOfMonth, endTime.getHour(), endTime.getMinute()) 51 | } 52 | 53 | fun initEditData(schedule: Schedule) { 54 | editItem = schedule 55 | year = schedule.dayStartTime.getYear() 56 | monthOfYear = schedule.dayStartTime.getMonthOfYear() 57 | dayOfMonth = schedule.dayStartTime.getDayOfMonth() 58 | text = schedule.text 59 | startTime = schedule.startingTime 60 | endTime = schedule.endingTime 61 | isAllDay = schedule.isAllDay 62 | isMilestone = schedule.isMilestone 63 | milestoneGoalMillis = schedule.milestoneGoal 64 | } 65 | 66 | fun addSchedule() { 67 | val schedule = Schedule( 68 | text = text, 69 | dayStartTime = startTime.dayStartTime(), 70 | startingTime = startTime, 71 | endingTime = endTime, 72 | isAllDay = isAllDay, 73 | isMilestone = isMilestone, 74 | milestoneGoal = milestoneGoalMillis 75 | ) 76 | ScheduleHelper.addSchedule(schedule) 77 | } 78 | 79 | fun editSchedule() { 80 | editItem?.let { 81 | ScheduleHelper.modifySchedule(it, text, startTime, endTime, isAllDay, isMilestone, milestoneGoalMillis) 82 | } 83 | EffectHelper.emitScheduleEffect(ScheduleEffect.RefreshData) 84 | } 85 | 86 | actionFlow.collectAction { 87 | when (this) { 88 | is AddScheduleAction.SetDate -> { 89 | setDate(dateTriple) 90 | } 91 | 92 | is AddScheduleAction.SetAllDay -> { 93 | isAllDay = newValue 94 | } 95 | 96 | is AddScheduleAction.SetMilestone -> { 97 | isMilestone = newValue 98 | } 99 | 100 | is AddScheduleAction.SetStartTime -> { 101 | val millis = TimeUtil.toEpochMillis(year, monthOfYear, dayOfMonth, hour, minute) 102 | startTime = millis 103 | } 104 | 105 | is AddScheduleAction.SetEndTime -> { 106 | val millis = TimeUtil.toEpochMillis(year, monthOfYear, dayOfMonth, hour, minute) 107 | endTime = millis 108 | } 109 | 110 | is AddScheduleAction.SetTimeEditType -> { 111 | timeEditType = editType 112 | } 113 | 114 | is AddScheduleAction.Confirm -> { 115 | text = this.text 116 | milestoneGoalMillis = this.milestoneGoalMillis 117 | if (isEdit) { 118 | editSchedule() 119 | } else { 120 | addSchedule() 121 | } 122 | SyncHelper.autoPushSchedule() 123 | } 124 | 125 | is AddScheduleAction.BeginEdit -> { 126 | isEdit = true 127 | initEditData(schedule) 128 | } 129 | } 130 | } 131 | 132 | return AddScheduleState(year, monthOfYear, dayOfMonth, text, startTime, endTime, isAllDay, isMilestone, timeEditType, isEdit, milestoneGoalMillis) 133 | } 134 | 135 | data class AddScheduleState( 136 | val year: Int, 137 | val monthOfYear: Int, 138 | val dayOfMonth: Int, 139 | val text: String, 140 | val startTime: Long, 141 | val endTime: Long, 142 | val isAllDay: Boolean, 143 | val isMilestone: Boolean, 144 | val timeEditType: TimeEditType?, 145 | val isEdit: Boolean, 146 | val milestoneGoalMillis: Long 147 | ) { 148 | suspend fun getDateString(): String { 149 | if (monthOfYear - 1 in CalendarUtil.getMonthNamesNonComposable().indices) { 150 | val monthName = CalendarUtil.getMonthNamesNonComposable()[monthOfYear - 1] 151 | return "$monthName $dayOfMonth, $year" 152 | } 153 | return "" 154 | } 155 | } 156 | 157 | sealed interface AddScheduleAction { 158 | data class SetDate(val dateTriple: Triple) : AddScheduleAction 159 | data class SetAllDay(val newValue: Boolean) : AddScheduleAction 160 | data class SetMilestone(val newValue: Boolean) : AddScheduleAction 161 | data class SetStartTime(val hour: Int, val minute: Int) : AddScheduleAction 162 | data class SetEndTime(val hour: Int, val minute: Int) : AddScheduleAction 163 | data class SetTimeEditType(val editType: TimeEditType) : AddScheduleAction 164 | data class Confirm(val text: String, val milestoneGoalMillis: Long) : AddScheduleAction 165 | data class BeginEdit(val schedule: Schedule) : AddScheduleAction 166 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/scene/HomeScene.kt: -------------------------------------------------------------------------------- 1 | package ui.scene 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.imePadding 9 | import androidx.compose.foundation.pager.HorizontalPager 10 | import androidx.compose.foundation.pager.rememberPagerState 11 | import androidx.compose.material.Scaffold 12 | import androidx.compose.material.SnackbarHost 13 | import androidx.compose.material.SnackbarHostState 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.rememberCoroutineScope 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.layout.onSizeChanged 19 | import constant.TabConstants 20 | import ui.fragment.SettingsFragment 21 | import ui.fragment.TimeCardFragment 22 | import global.AppColors 23 | import kotlinx.coroutines.launch 24 | import moe.tlaster.precompose.navigation.BackHandler 25 | import moe.tlaster.precompose.navigation.Navigator 26 | import store.CurrentProcessStore 27 | import ui.fragment.DepositFragment 28 | import ui.fragment.MemoFragment 29 | import ui.fragment.ScheduleFragment 30 | import ui.widget.BaseImmersiveScene 31 | import ui.widget.BottomBar 32 | 33 | @OptIn(ExperimentalFoundationApi::class) 34 | @Composable 35 | fun HomeScene(navigator: Navigator) { 36 | val scope = rememberCoroutineScope() 37 | val snackbarHostState = remember { SnackbarHostState() } 38 | 39 | fun showSnackbar(message: String) { 40 | scope.launch { 41 | snackbarHostState.showSnackbar(message) 42 | } 43 | } 44 | 45 | BaseImmersiveScene( 46 | statusBarColorStr = "#F4F4F4", 47 | navigationBarColorStr = "#FFFFFF", 48 | statusBarPadding = true, 49 | navigationBarPadding = false, 50 | modifier = Modifier 51 | .imePadding() 52 | .fillMaxSize() 53 | .background(AppColors.Background) 54 | ) { 55 | Scaffold(snackbarHost = { SnackbarHost(hostState = snackbarHostState) }) { 56 | val pagerState = rememberPagerState(initialPage = TabConstants.TAB_TIME_CARD, pageCount = { TabConstants.TAB_COUNT }) 57 | 58 | Column(modifier = Modifier.fillMaxSize()) { 59 | HorizontalPager( 60 | state = pagerState, 61 | userScrollEnabled = false, 62 | modifier = Modifier 63 | .weight(1f) 64 | .fillMaxWidth() 65 | .onSizeChanged { 66 | CurrentProcessStore.screenWidthPixels.value = it.width 67 | } 68 | ) { 69 | when (it) { 70 | TabConstants.TAB_TIME_CARD -> TimeCardFragment( 71 | modifier = Modifier.fillMaxSize(), 72 | navigator = navigator 73 | ) 74 | TabConstants.TAB_SETTINGS -> SettingsFragment( 75 | modifier = Modifier.fillMaxSize(), 76 | navigator = navigator, 77 | showSnackbar = ::showSnackbar 78 | ) 79 | TabConstants.TAB_MEMO -> MemoFragment(navigator = navigator) 80 | TabConstants.TAB_SCHEDULE -> ScheduleFragment(navigator = navigator) 81 | TabConstants.TAB_DEPOSIT -> DepositFragment(navigator = navigator) 82 | else -> TimeCardFragment( 83 | modifier = Modifier.fillMaxSize(), 84 | navigator = navigator 85 | ) 86 | } 87 | } 88 | 89 | BottomBar( 90 | selectIndex = pagerState.currentPage, 91 | onSelect = { 92 | scope.launch { 93 | pagerState.scrollToPage(page = it) 94 | } 95 | } 96 | ) 97 | } 98 | 99 | BackHandler { 100 | // Do nothing. 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/scene/WriteMemoPresenter.kt: -------------------------------------------------------------------------------- 1 | package ui.scene 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.runtime.setValue 8 | import helper.MemoHelper 9 | import helper.SyncHelper 10 | import helper.effect.EffectHelper 11 | import helper.effect.MemoEffect 12 | import kotlinx.coroutines.flow.Flow 13 | import model.records.Memo 14 | import moe.tlaster.precompose.molecule.collectAction 15 | import moe.tlaster.precompose.navigation.Navigator 16 | 17 | @Composable 18 | fun WriteMemoPresenter(actionFlow: Flow): WriteMemoState { 19 | var memo by remember { mutableStateOf(null) } 20 | var text by remember { mutableStateOf("") } 21 | var isTodo by remember { mutableStateOf(false) } 22 | var isPin by remember { mutableStateOf(false) } 23 | var allGroups by remember { mutableStateOf(MemoHelper.getGroupSet()) } 24 | var group by remember { mutableStateOf(null) } 25 | 26 | actionFlow.collectAction { 27 | when (this) { 28 | is WriteMemoAction.BeginEdit -> { 29 | memo = editMemo 30 | text = editMemo.text 31 | isTodo = editMemo.isTodo 32 | isPin = editMemo.isPin 33 | group = editMemo.group 34 | } 35 | 36 | is WriteMemoAction.SetTodo -> { 37 | isTodo = this.newTodo 38 | } 39 | 40 | is WriteMemoAction.SetPin -> { 41 | isPin = this.newPin 42 | } 43 | 44 | is WriteMemoAction.SetGroup -> { 45 | group = this.group 46 | group?.let { 47 | val newAllGroups = allGroups.toMutableSet() 48 | newAllGroups.add(it) 49 | allGroups = newAllGroups 50 | } 51 | } 52 | 53 | is WriteMemoAction.Delete -> { 54 | memo?.let { 55 | MemoHelper.deleteMemo(it) 56 | } 57 | SyncHelper.autoPushMemo() 58 | EffectHelper.emitMemoEffect(MemoEffect.RefreshData) 59 | navigator.goBack() 60 | } 61 | 62 | is WriteMemoAction.Confirm -> { 63 | text = this.text 64 | if (memo == null) { 65 | MemoHelper.addMemo(text, isTodo, isPin, group) 66 | } else { 67 | memo?.let { 68 | MemoHelper.modifyMemo(it, text, isTodo, isPin, group) 69 | } 70 | } 71 | SyncHelper.autoPushMemo() 72 | EffectHelper.emitMemoEffect(MemoEffect.RefreshData) 73 | navigator.goBack() 74 | } 75 | } 76 | } 77 | 78 | return WriteMemoState(memo, text, isTodo, isPin, allGroups, group) 79 | } 80 | 81 | data class WriteMemoState( 82 | val memo: Memo? = null, 83 | val text: String = "", 84 | val isTodo: Boolean = false, 85 | val isPin: Boolean = false, 86 | val allGroups: Set = emptySet(), 87 | val group: String? = null 88 | ) 89 | 90 | sealed interface WriteMemoAction { 91 | data class BeginEdit(val editMemo: Memo) : WriteMemoAction 92 | data class SetTodo(val newTodo: Boolean) : WriteMemoAction 93 | data class SetPin(val newPin: Boolean) : WriteMemoAction 94 | data class SetGroup(val group: String?) : WriteMemoAction 95 | data class Delete(val navigator: Navigator) : WriteMemoAction 96 | data class Confirm(val text: String, val navigator: Navigator) : WriteMemoAction 97 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/scene/detail/DetailScene.kt: -------------------------------------------------------------------------------- 1 | package ui.scene.detail 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material.Tab 7 | import androidx.compose.material.TabRow 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import global.AppColors 13 | import moe.tlaster.precompose.molecule.rememberPresenter 14 | import moe.tlaster.precompose.navigation.Navigator 15 | import org.jetbrains.compose.resources.stringResource 16 | import ui.widget.BaseImmersiveScene 17 | import ui.widget.TitleBar 18 | import zhoutools.composeapp.generated.resources.Res 19 | import zhoutools.composeapp.generated.resources.details 20 | import zhoutools.composeapp.generated.resources.history 21 | import zhoutools.composeapp.generated.resources.today 22 | 23 | @Composable 24 | fun DetailScene(navigator: Navigator) { 25 | val (state, channel) = rememberPresenter { DetailPresenter(it) } 26 | val tabs = mapOf( 27 | DETAIL_TAB_TODAY to stringResource(Res.string.today), 28 | DETAIL_TAB_HISTORY to stringResource(Res.string.history) 29 | ) 30 | 31 | BaseImmersiveScene(modifier = Modifier 32 | .fillMaxSize() 33 | .background(AppColors.Background) 34 | ) { 35 | Column { 36 | TitleBar( 37 | navigator = navigator, 38 | title = stringResource(Res.string.details) 39 | ) 40 | 41 | TabRow( 42 | selectedTabIndex = state.tab, 43 | backgroundColor = Color.White, 44 | contentColor = AppColors.Theme 45 | ) { 46 | tabs.forEach { 47 | val index = it.key 48 | val title = it.value 49 | Tab( 50 | selected = state.tab == index, 51 | onClick = { 52 | channel.trySend(DetailAction.ChangeTab(index)) 53 | }, 54 | text = { 55 | Text(text = title) 56 | } 57 | ) 58 | } 59 | } 60 | 61 | if (state.tab == DETAIL_TAB_HISTORY) { 62 | HistoryFragment(state.historyState, channel) 63 | } else { 64 | TodayFragment(state.todayState) 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/widget/AutoSyncIndicator.kt: -------------------------------------------------------------------------------- 1 | package ui.widget 2 | 3 | import androidx.compose.animation.core.LinearEasing 4 | import androidx.compose.animation.core.animate 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.material.CircularProgressIndicator 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.LaunchedEffect 13 | import androidx.compose.runtime.collectAsState 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableFloatStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.draw.alpha 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | import helper.SyncHelper 25 | import org.jetbrains.compose.resources.stringResource 26 | import zhoutools.composeapp.generated.resources.Res 27 | import zhoutools.composeapp.generated.resources.downloading 28 | import zhoutools.composeapp.generated.resources.uploading 29 | 30 | @Composable 31 | fun AutoSyncIndicator() { 32 | var textAlpha by remember { mutableFloatStateOf(1f) } 33 | val isPulling = SyncHelper.isAutoPulling.collectAsState(initial = false).value 34 | val isPushing = SyncHelper.isAutoPushing.collectAsState(initial = false).value 35 | 36 | if (isPulling || isPushing) { 37 | Row(verticalAlignment = Alignment.CenterVertically) { 38 | CircularProgressIndicator( 39 | modifier = Modifier 40 | .padding(start = 8.dp) 41 | .size(20.dp), 42 | color = Color.LightGray, 43 | strokeWidth = 2.dp 44 | ) 45 | 46 | Text( 47 | text = stringResource( 48 | if (isPulling) Res.string.downloading else Res.string.uploading 49 | ), 50 | color = Color.Gray, 51 | modifier = Modifier 52 | .padding(start = 8.dp) 53 | .alpha(textAlpha), 54 | fontSize = 14.sp 55 | ) 56 | } 57 | 58 | LaunchedEffect(Unit) { 59 | // Text blinking effect 60 | while (true) { 61 | animate(1f, 0f, animationSpec = tween(500, delayMillis = 500, easing = LinearEasing)) { value, _ -> 62 | textAlpha = value 63 | } 64 | animate(0f, 1f, animationSpec = tween(500, easing = LinearEasing)) { value, _ -> 65 | textAlpha = value 66 | } 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/widget/BaseImmersiveScene.kt: -------------------------------------------------------------------------------- 1 | package ui.widget 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.navigationBarsPadding 5 | import androidx.compose.foundation.layout.statusBarsPadding 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | 9 | /** 10 | * Scene for independent pages 11 | * Immersive status & navigation bar support for Android and iOS. 12 | */ 13 | @Composable 14 | fun BaseImmersiveScene( 15 | modifier: Modifier = Modifier, 16 | statusBarColorStr: String = "#FFFFFF", 17 | navigationBarColorStr: String = "#F4F4F4", 18 | statusBarPadding: Boolean = false, 19 | navigationBarPadding: Boolean = true, 20 | content: @Composable () -> Unit 21 | ) { 22 | var rootModifier = modifier 23 | if (statusBarPadding) { 24 | rootModifier = rootModifier.statusBarsPadding() 25 | } 26 | if (navigationBarPadding) { 27 | rootModifier = rootModifier.navigationBarsPadding() 28 | } 29 | 30 | Box(modifier = rootModifier) { 31 | content() 32 | } 33 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/widget/BottomBar.kt: -------------------------------------------------------------------------------- 1 | package ui.widget 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.navigationBarsPadding 10 | import androidx.compose.foundation.layout.offset 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.material.Icon 13 | import androidx.compose.material.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | import constant.TabConstants 21 | import extension.clickableNoRipple 22 | import global.AppColors 23 | import org.jetbrains.compose.resources.DrawableResource 24 | import org.jetbrains.compose.resources.StringResource 25 | import org.jetbrains.compose.resources.painterResource 26 | import org.jetbrains.compose.resources.stringResource 27 | import zhoutools.composeapp.generated.resources.Res 28 | import zhoutools.composeapp.generated.resources.deposit 29 | import zhoutools.composeapp.generated.resources.ic_deposit 30 | import zhoutools.composeapp.generated.resources.ic_memo 31 | import zhoutools.composeapp.generated.resources.ic_schedule 32 | import zhoutools.composeapp.generated.resources.ic_settings 33 | import zhoutools.composeapp.generated.resources.ic_time_card 34 | import zhoutools.composeapp.generated.resources.memo 35 | import zhoutools.composeapp.generated.resources.schedule 36 | import zhoutools.composeapp.generated.resources.settings 37 | import zhoutools.composeapp.generated.resources.time_card 38 | 39 | @Composable 40 | fun BottomBar( 41 | modifier: Modifier = Modifier, 42 | selectIndex: Int, 43 | onSelect: (Int) -> Unit 44 | ) { 45 | val rootModifier = modifier 46 | .background(Color.White) 47 | .navigationBarsPadding() 48 | .fillMaxWidth() 49 | .height(58.dp) 50 | 51 | Row( 52 | modifier = rootModifier, 53 | verticalAlignment = Alignment.CenterVertically 54 | ) { 55 | Spacer(modifier = Modifier.weight(1f)) 56 | 57 | BottomBarItem( 58 | index = TabConstants.TAB_TIME_CARD, 59 | selectIndex = selectIndex, 60 | icon = Res.drawable.ic_time_card, 61 | name = Res.string.time_card 62 | ) { 63 | onSelect(it) 64 | } 65 | 66 | Spacer(modifier = Modifier.weight(1f)) 67 | 68 | BottomBarItem( 69 | index = TabConstants.TAB_SCHEDULE, 70 | selectIndex = selectIndex, 71 | icon = Res.drawable.ic_schedule, 72 | name = Res.string.schedule 73 | ) { 74 | onSelect(it) 75 | } 76 | 77 | Spacer(modifier = Modifier.weight(1f)) 78 | 79 | BottomBarItem( 80 | index = TabConstants.TAB_MEMO, 81 | selectIndex = selectIndex, 82 | icon = Res.drawable.ic_memo, 83 | name = Res.string.memo 84 | ) { 85 | onSelect(it) 86 | } 87 | 88 | Spacer(modifier = Modifier.weight(1f)) 89 | 90 | BottomBarItem( 91 | index = TabConstants.TAB_DEPOSIT, 92 | selectIndex = selectIndex, 93 | icon = Res.drawable.ic_deposit, 94 | name = Res.string.deposit 95 | ) { 96 | onSelect(it) 97 | } 98 | 99 | Spacer(modifier = Modifier.weight(1f)) 100 | 101 | BottomBarItem( 102 | index = TabConstants.TAB_SETTINGS, 103 | selectIndex = selectIndex, 104 | icon = Res.drawable.ic_settings, 105 | name = Res.string.settings 106 | ) { 107 | onSelect(it) 108 | } 109 | 110 | Spacer(modifier = Modifier.weight(1f)) 111 | } 112 | } 113 | 114 | @Composable 115 | private fun BottomBarItem(index: Int, selectIndex: Int, icon: DrawableResource, name: StringResource, onSelect: (Int) -> Unit) { 116 | Column( 117 | horizontalAlignment = Alignment.CenterHorizontally, 118 | modifier = Modifier 119 | .clickableNoRipple { 120 | onSelect(index) 121 | } 122 | .padding(horizontal = 4.dp) 123 | ) { 124 | Icon( 125 | painter = painterResource(icon), 126 | modifier = Modifier.height(32.dp).offset(y = 4.dp), 127 | contentDescription = null, 128 | tint = if (selectIndex == index) AppColors.Theme else Color.Unspecified 129 | ) 130 | 131 | Text( 132 | text = stringResource(name), 133 | fontSize = 11.sp, 134 | color = if (selectIndex == index) AppColors.Theme else Color.Black 135 | ) 136 | } 137 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/widget/Divider.kt: -------------------------------------------------------------------------------- 1 | package ui.widget 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | import global.AppColors 11 | 12 | @Composable 13 | fun VerticalDivider() { 14 | Box(modifier = Modifier 15 | .fillMaxWidth() 16 | .height(1.dp) 17 | .background(AppColors.Divider) 18 | ) 19 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/widget/EmptyLayout.kt: -------------------------------------------------------------------------------- 1 | package ui.widget 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.material.Icon 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.alpha 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.unit.dp 17 | import androidx.compose.ui.unit.sp 18 | import org.jetbrains.compose.resources.painterResource 19 | import zhoutools.composeapp.generated.resources.Res 20 | import zhoutools.composeapp.generated.resources.ic_empty 21 | 22 | @Composable 23 | fun EmptyLayout(modifier: Modifier = Modifier, description: String) { 24 | Column( 25 | modifier = modifier 26 | .fillMaxWidth() 27 | .padding(top = 64.dp) 28 | .alpha(0.5f), 29 | horizontalAlignment = Alignment.CenterHorizontally 30 | ) { 31 | Icon( 32 | painter = painterResource(Res.drawable.ic_empty), 33 | contentDescription = null, 34 | tint = Color.Unspecified, 35 | modifier = Modifier.size(64.dp) 36 | ) 37 | 38 | Spacer(modifier = Modifier.height(8.dp)) 39 | 40 | Text( 41 | text = description, 42 | fontSize = 16.sp 43 | ) 44 | } 45 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/widget/FragmentHeader.kt: -------------------------------------------------------------------------------- 1 | package ui.widget 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.text.font.FontWeight 12 | import androidx.compose.ui.unit.dp 13 | import androidx.compose.ui.unit.sp 14 | 15 | @Composable 16 | fun FragmentHeader(title: String, endingSlot: @Composable (() -> Unit)? = null) { 17 | Row( 18 | modifier = Modifier 19 | .fillMaxWidth() 20 | .padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 12.dp), 21 | verticalAlignment = Alignment.CenterVertically 22 | ) { 23 | Text( 24 | text = title.uppercase(), 25 | fontSize = 24.sp, 26 | fontWeight = FontWeight.ExtraBold 27 | ) 28 | 29 | AutoSyncIndicator() 30 | 31 | Spacer(modifier = Modifier.weight(1f)) 32 | 33 | endingSlot?.invoke() 34 | } 35 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/widget/HorizontalSeekBar.kt: -------------------------------------------------------------------------------- 1 | package ui.widget 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.lazy.LazyRow 10 | import androidx.compose.foundation.lazy.itemsIndexed 11 | import androidx.compose.foundation.lazy.rememberLazyListState 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.LaunchedEffect 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableIntStateOf 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.runtime.snapshotFlow 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.layout.onSizeChanged 25 | import androidx.compose.ui.platform.LocalDensity 26 | import androidx.compose.ui.text.font.FontWeight 27 | import androidx.compose.ui.text.style.TextAlign 28 | import androidx.compose.ui.unit.Dp 29 | import androidx.compose.ui.unit.dp 30 | import androidx.compose.ui.unit.sp 31 | import global.AppColors 32 | import kotlinx.coroutines.delay 33 | import org.jetbrains.compose.resources.painterResource 34 | import zhoutools.composeapp.generated.resources.Res 35 | import zhoutools.composeapp.generated.resources.ic_arrow_down 36 | import kotlin.math.roundToInt 37 | 38 | @Composable 39 | fun HorizontalSeekBar( 40 | modifier: Modifier = Modifier, 41 | itemList: List, 42 | itemWidth: Dp, 43 | defaultSelectItem: String? = null, 44 | onSelectItem: ((String) -> Unit)? = null 45 | ) { 46 | fun getDefaultSelectIndex(): Int { 47 | val index = itemList.indexOf(defaultSelectItem) 48 | return if (index >= 0) index else 0 49 | } 50 | 51 | val density = LocalDensity.current 52 | val toPx = { dp: Dp -> 53 | density.run { dp.toPx() } 54 | } 55 | val toDp = { px: Float -> 56 | density.run { px.toDp() } 57 | } 58 | var thisWidth by remember { mutableIntStateOf(0) } 59 | val lazyListState = rememberLazyListState() 60 | val paddingHorizontal = thisWidth / 2 - toPx(itemWidth) / 2 61 | var selectIndex by remember { mutableIntStateOf(getDefaultSelectIndex()) } 62 | var isInitialScroll by remember { mutableStateOf(true) } 63 | 64 | fun selectItem(index: Int) { 65 | if (index in itemList.indices) { 66 | selectIndex = index 67 | onSelectItem?.invoke(itemList[index]) 68 | } 69 | } 70 | 71 | LaunchedEffect(Unit) { 72 | lazyListState.animateScrollToItem(selectIndex) 73 | isInitialScroll = false 74 | } 75 | 76 | LaunchedEffect(lazyListState.firstVisibleItemIndex, lazyListState.firstVisibleItemScrollOffset) { 77 | if (!isInitialScroll) { 78 | lazyListState.apply { 79 | val totalOffset = firstVisibleItemIndex * toPx(itemWidth) + firstVisibleItemScrollOffset 80 | selectItem((totalOffset / toPx(itemWidth)).roundToInt()) 81 | } 82 | } 83 | } 84 | 85 | LaunchedEffect(lazyListState) { 86 | snapshotFlow { lazyListState.isScrollInProgress }.collect { isScrolling -> 87 | if (!isScrolling) { 88 | delay(100) 89 | if (!lazyListState.isScrollInProgress) { 90 | lazyListState.animateScrollToItem(selectIndex) 91 | } 92 | } 93 | } 94 | } 95 | 96 | Column( 97 | horizontalAlignment = Alignment.CenterHorizontally, 98 | modifier = modifier 99 | .fillMaxWidth() 100 | .onSizeChanged { 101 | thisWidth = it.width 102 | } 103 | ) { 104 | Icon( 105 | painter = painterResource(Res.drawable.ic_arrow_down), 106 | tint = AppColors.Theme, 107 | contentDescription = null, 108 | modifier = Modifier.size(24.dp) 109 | ) 110 | 111 | LazyRow( 112 | modifier = Modifier.fillMaxWidth(), 113 | verticalAlignment = Alignment.CenterVertically, 114 | state = lazyListState, 115 | contentPadding = PaddingValues(horizontal = if (paddingHorizontal > 0) toDp(paddingHorizontal) else 0.dp) 116 | ) { 117 | itemsIndexed(itemList) { index, it -> 118 | Text( 119 | text = it, 120 | fontSize = if (index == selectIndex) 20.sp else 16.sp, 121 | textAlign = TextAlign.Center, 122 | fontWeight = if (index == selectIndex) FontWeight.ExtraBold else FontWeight.Normal, 123 | modifier = Modifier 124 | .width(itemWidth) 125 | .padding(top = 4.dp) 126 | ) 127 | } 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/widget/ShimmerProgressBar.kt: -------------------------------------------------------------------------------- 1 | package ui.widget 2 | 3 | import androidx.annotation.FloatRange 4 | import androidx.compose.animation.core.RepeatMode 5 | import androidx.compose.animation.core.animateFloat 6 | import androidx.compose.animation.core.infiniteRepeatable 7 | import androidx.compose.animation.core.rememberInfiniteTransition 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.foundation.background 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.fillMaxHeight 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.layout.fillMaxWidth 14 | import androidx.compose.foundation.layout.offset 15 | import androidx.compose.material.LinearProgressIndicator 16 | import androidx.compose.material.MaterialTheme 17 | import androidx.compose.material.ProgressIndicatorDefaults 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.mutableIntStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.graphics.Brush 25 | import androidx.compose.ui.graphics.Color 26 | import androidx.compose.ui.layout.onSizeChanged 27 | import androidx.compose.ui.platform.LocalDensity 28 | 29 | @Composable 30 | fun ShimmerProgressBar( 31 | @FloatRange(from = 0.0, to = 1.0) 32 | progress: Float, 33 | modifier: Modifier = Modifier, 34 | color: Color = MaterialTheme.colors.primary, 35 | backgroundColor: Color = color.copy(alpha = ProgressIndicatorDefaults.IndicatorBackgroundOpacity), 36 | durationMillis: Int = 3000 37 | ) { 38 | var width by remember { mutableIntStateOf(0) } 39 | val infiniteTransition = rememberInfiniteTransition() 40 | val animatedOffset by infiniteTransition.animateFloat( 41 | initialValue = -1f, 42 | targetValue = 2f, 43 | animationSpec = infiniteRepeatable( 44 | animation = tween(durationMillis), 45 | repeatMode = RepeatMode.Restart 46 | ) 47 | ) 48 | 49 | Box(modifier = modifier.onSizeChanged { 50 | width = it.width 51 | }) { 52 | LinearProgressIndicator( 53 | progress = progress, 54 | color = color, 55 | backgroundColor = backgroundColor, 56 | modifier = Modifier.fillMaxSize() 57 | ) 58 | 59 | // Infinite transition shimmer effect 60 | Box(modifier = Modifier 61 | .fillMaxWidth(0.3f) 62 | .fillMaxHeight() 63 | .offset(x = with(LocalDensity.current) { (animatedOffset * width).toDp() }) 64 | .background( 65 | brush = Brush.horizontalGradient( 66 | colors = listOf( 67 | Color.White.copy(alpha = 0f), 68 | Color.White.copy(alpha = 0.3f), 69 | Color.White.copy(alpha = 0f) 70 | ) 71 | ) 72 | ) 73 | ) 74 | } 75 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/widget/TitleBar.kt: -------------------------------------------------------------------------------- 1 | package ui.widget 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.layout.statusBarsPadding 12 | import androidx.compose.foundation.layout.width 13 | import androidx.compose.foundation.shape.CircleShape 14 | import androidx.compose.material.Icon 15 | import androidx.compose.material.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.clip 20 | import androidx.compose.ui.graphics.Color 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | import moe.tlaster.precompose.navigation.Navigator 25 | import org.jetbrains.compose.resources.painterResource 26 | import zhoutools.composeapp.generated.resources.Res 27 | import zhoutools.composeapp.generated.resources.ic_back 28 | 29 | @Composable 30 | fun TitleBar(navigator: Navigator, title: String) { 31 | val rootModifier = Modifier 32 | .background(Color.White) 33 | .statusBarsPadding() 34 | .fillMaxWidth() 35 | 36 | Row( 37 | modifier = rootModifier, 38 | verticalAlignment = Alignment.CenterVertically 39 | ) { 40 | Spacer(modifier = Modifier.width(12.dp)) 41 | 42 | Box(modifier = Modifier 43 | .size(36.dp) 44 | .clip(CircleShape) 45 | .clickable { 46 | navigator.goBack() 47 | } 48 | ) { 49 | Icon( 50 | painter = painterResource(Res.drawable.ic_back), 51 | contentDescription = null, 52 | tint = Color.Unspecified, 53 | modifier = Modifier 54 | .align(Alignment.Center) 55 | .size(24.dp) 56 | ) 57 | } 58 | 59 | Spacer(modifier = Modifier.width(12.dp)) 60 | 61 | Text( 62 | text = title, 63 | fontSize = 20.sp, 64 | fontWeight = FontWeight.Bold, 65 | modifier = Modifier.padding(vertical = 12.dp) 66 | ) 67 | } 68 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/util/CalendarUtil.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import androidx.compose.runtime.Composable 4 | import extension.getYear 5 | import extension.toTwoDigits 6 | import helper.NetworkHelper 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.IO 10 | import kotlinx.coroutines.SupervisorJob 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.StateFlow 13 | import kotlinx.coroutines.flow.update 14 | import kotlinx.coroutines.launch 15 | import kotlinx.datetime.DateTimeUnit 16 | import kotlinx.datetime.DayOfWeek 17 | import kotlinx.datetime.LocalDate 18 | import kotlinx.datetime.daysUntil 19 | import kotlinx.datetime.plus 20 | import kotlinx.serialization.json.JsonObject 21 | import kotlinx.serialization.json.booleanOrNull 22 | import kotlinx.serialization.json.jsonObject 23 | import kotlinx.serialization.json.jsonPrimitive 24 | import org.jetbrains.compose.resources.getString 25 | import org.jetbrains.compose.resources.stringResource 26 | import zhoutools.composeapp.generated.resources.Res 27 | import zhoutools.composeapp.generated.resources.april 28 | import zhoutools.composeapp.generated.resources.august 29 | import zhoutools.composeapp.generated.resources.december 30 | import zhoutools.composeapp.generated.resources.february 31 | import zhoutools.composeapp.generated.resources.sunday 32 | import zhoutools.composeapp.generated.resources.monday 33 | import zhoutools.composeapp.generated.resources.tuesday 34 | import zhoutools.composeapp.generated.resources.wednesday 35 | import zhoutools.composeapp.generated.resources.thursday 36 | import zhoutools.composeapp.generated.resources.friday 37 | import zhoutools.composeapp.generated.resources.january 38 | import zhoutools.composeapp.generated.resources.july 39 | import zhoutools.composeapp.generated.resources.june 40 | import zhoutools.composeapp.generated.resources.march 41 | import zhoutools.composeapp.generated.resources.may 42 | import zhoutools.composeapp.generated.resources.november 43 | import zhoutools.composeapp.generated.resources.october 44 | import zhoutools.composeapp.generated.resources.saturday 45 | import zhoutools.composeapp.generated.resources.september 46 | import kotlin.math.abs 47 | 48 | object CalendarUtil { 49 | const val NOT_HOLIDAY = 0 50 | const val DAY_OFF = 1 51 | const val WORK_DAY = 2 52 | 53 | const val KEY_IS_OFF_DAY = "isOffDay" 54 | 55 | private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) 56 | 57 | private val holidayMap = MutableStateFlow(mapOf()) 58 | 59 | init { 60 | coroutineScope.launch(Dispatchers.IO) { 61 | fetchHolidayMap() 62 | } 63 | } 64 | 65 | /** 66 | * @param year null for current year 67 | */ 68 | suspend fun fetchHolidayMap(year: Int? = null) { 69 | val currentYear = TimeUtil.currentTimeMillis().getYear() 70 | val useYear = year ?: currentYear 71 | val holidays = NetworkHelper.getHolidays(useYear) 72 | if (holidays != null) { 73 | holidayMap.update { 74 | it + (useYear to holidays) 75 | } 76 | } 77 | } 78 | 79 | @Composable 80 | fun getWeekDays(): List = listOf( 81 | stringResource(Res.string.sunday), 82 | stringResource(Res.string.monday), 83 | stringResource(Res.string.tuesday), 84 | stringResource(Res.string.wednesday), 85 | stringResource(Res.string.thursday), 86 | stringResource(Res.string.friday), 87 | stringResource(Res.string.saturday) 88 | ) 89 | 90 | @Composable 91 | fun getMonthNames(): List = listOf( 92 | stringResource(Res.string.january), 93 | stringResource(Res.string.february), 94 | stringResource(Res.string.march), 95 | stringResource(Res.string.april), 96 | stringResource(Res.string.may), 97 | stringResource(Res.string.june), 98 | stringResource(Res.string.july), 99 | stringResource(Res.string.august), 100 | stringResource(Res.string.september), 101 | stringResource(Res.string.october), 102 | stringResource(Res.string.november), 103 | stringResource(Res.string.december), 104 | ) 105 | 106 | suspend fun getMonthNamesNonComposable(): List = listOf( 107 | getString(Res.string.january), 108 | getString(Res.string.february), 109 | getString(Res.string.march), 110 | getString(Res.string.april), 111 | getString(Res.string.may), 112 | getString(Res.string.june), 113 | getString(Res.string.july), 114 | getString(Res.string.august), 115 | getString(Res.string.september), 116 | getString(Res.string.october), 117 | getString(Res.string.november), 118 | getString(Res.string.december), 119 | ) 120 | 121 | /** 122 | * @return pair.first: day of month (1..31); pair.second: day of week 123 | */ 124 | fun getMonthDays(year: Int, month: Int): List> { 125 | val monthStart = LocalDate(year, month, 1) 126 | val nextMonthStart = monthStart.plus(1, DateTimeUnit.MonthBased(1)) 127 | val daysInMonth = abs(monthStart.daysUntil(nextMonthStart)) 128 | val resultList = mutableListOf>() 129 | for (day in 1..daysInMonth) { 130 | val date = LocalDate(year, month, day) 131 | resultList.add(Pair(date.dayOfMonth, date.dayOfWeek)) 132 | } 133 | return resultList 134 | } 135 | 136 | fun isHoliday(year: Int, month: Int, day: Int): Int { 137 | runCatching { 138 | val dateStr = "${year}-${month.toTwoDigits()}-${day.toTwoDigits()}" 139 | val holidays = holidayMap.value[year] ?: return NOT_HOLIDAY 140 | val holiday = holidays[dateStr]?.jsonObject ?: return NOT_HOLIDAY 141 | val isOffDay = holiday[KEY_IS_OFF_DAY]?.jsonPrimitive?.booleanOrNull 142 | return if (isOffDay == true) DAY_OFF else WORK_DAY 143 | }.onFailure { 144 | it.printStackTrace() 145 | } 146 | return NOT_HOLIDAY 147 | } 148 | 149 | fun getHolidayMap(): StateFlow> = holidayMap 150 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/util/TimeUtil.kt: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import kotlinx.datetime.Clock 5 | import kotlinx.datetime.LocalDateTime 6 | import kotlinx.datetime.TimeZone 7 | import kotlinx.datetime.toInstant 8 | 9 | object TimeUtil { 10 | fun currentTimeMillis(): Long { 11 | val now = Clock.System.now() 12 | return now.toEpochMilliseconds() 13 | } 14 | 15 | fun toEpochMillis(year: Int, month: Int, day: Int, hour: Int, minute: Int): Long { 16 | val localDateTime = LocalDateTime(year, month, day, hour, minute) 17 | return localDateTime.toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() 18 | } 19 | 20 | fun monthYearStringToMonthStartTime(monthYearString: String): Long? { 21 | return try { 22 | val (monthStr, yearStr) = monthYearString.split(" ") 23 | val monthNames = runBlocking { CalendarUtil.getMonthNamesNonComposable() } 24 | val monthNumber = monthNames.indexOf(monthStr) + 1 25 | val year = yearStr.toInt() 26 | toEpochMillis(year, monthNumber, 1, 0, 0) 27 | } catch (e: Exception) { 28 | e.printStackTrace() 29 | null 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/MainViewController.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.uikit.OnFocusBehavior 2 | import androidx.compose.ui.window.ComposeUIViewController 3 | 4 | fun MainViewController() = ComposeUIViewController(configure = { 5 | onFocusBehavior = OnFocusBehavior.DoNothing 6 | }) { App() } -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/Platform.ios.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.Composable 2 | import androidx.compose.runtime.DisposableEffect 3 | import androidx.compose.runtime.State 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.remember 6 | import platform.Foundation.NSBundle 7 | import platform.Foundation.NSNotificationCenter 8 | import platform.Foundation.NSOperationQueue 9 | import platform.Foundation.NSString 10 | import platform.UIKit.UIApplication 11 | import platform.UIKit.UIKeyboardWillHideNotification 12 | import platform.UIKit.UIKeyboardWillShowNotification 13 | import platform.UIKit.UIPasteboard 14 | import platform.UIKit.endEditing 15 | 16 | actual fun isIOS(): Boolean = true 17 | 18 | actual fun getAppVersion(): String { 19 | val nsBundle = NSBundle.mainBundle() 20 | val version = nsBundle.objectForInfoDictionaryKey("CFBundleShortVersionString") as? NSString 21 | return version?.toString() ?: "" 22 | } 23 | 24 | actual fun setClipboardContent(text: String) { 25 | val pasteboard = UIPasteboard.generalPasteboard() 26 | pasteboard.string = text 27 | } 28 | 29 | actual fun hideSoftwareKeyboard() { 30 | val sharedApp = UIApplication.sharedApplication() 31 | val window = sharedApp.keyWindow 32 | window?.endEditing(true) 33 | } 34 | 35 | actual fun setStatusBarColor(colorStr: String, isLight: Boolean) {} 36 | 37 | actual fun setNavigationBarColor(colorStr: String, isLight: Boolean) {} 38 | 39 | @Composable 40 | actual fun rememberKeyboardVisibilityState(): State { 41 | val isKeyboardVisible = remember { mutableStateOf(false) } 42 | 43 | DisposableEffect(Unit) { 44 | val notificationCenter = NSNotificationCenter.defaultCenter 45 | 46 | val keyboardWillShowObserver = notificationCenter.addObserverForName( 47 | UIKeyboardWillShowNotification, 48 | null, 49 | NSOperationQueue.mainQueue 50 | ) { _ -> 51 | isKeyboardVisible.value = true 52 | } 53 | 54 | val keyboardWillHideObserver = notificationCenter.addObserverForName( 55 | UIKeyboardWillHideNotification, 56 | null, 57 | NSOperationQueue.mainQueue 58 | ) { _ -> 59 | isKeyboardVisible.value = false 60 | } 61 | 62 | onDispose { 63 | notificationCenter.removeObserver(keyboardWillShowObserver) 64 | notificationCenter.removeObserver(keyboardWillHideObserver) 65 | } 66 | } 67 | return isKeyboardVisible 68 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | 3 | #Gradle 4 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" 5 | 6 | 7 | #Android 8 | android.nonTransitiveRClass=true 9 | android.useAndroidX=true 10 | 11 | #MPP 12 | kotlin.mpp.androidSourceSetLayoutVersion=2 13 | kotlin.mpp.enableCInteropCommonization=true 14 | 15 | #Development 16 | development=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.2.0" 3 | android-compileSdk = "34" 4 | android-minSdk = "24" 5 | android-targetSdk = "34" 6 | androidx-activityCompose = "1.9.3" 7 | compose = "1.6.7" 8 | compose-plugin = "1.6.10" 9 | kotlin = "2.0.21" 10 | precompose = "1.6.2" 11 | ktor = "2.3.7" 12 | serialization = "1.9.23" 13 | androidx-datastore = "1.1.1" 14 | datetime = "0.5.0" 15 | logging = "1.4.2" 16 | molecule = "1.4.1" 17 | material3 = "1.6.10" 18 | 19 | [libraries] 20 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 21 | compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } 22 | compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } 23 | precompose = { module = "moe.tlaster:precompose", version.ref = "precompose" } 24 | precompose-molecule = { module = "moe.tlaster:precompose-molecule", version.ref = "precompose" } 25 | ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } 26 | ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } 27 | ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } 28 | ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } 29 | ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } 30 | androidx-datastore-core = { module = "androidx.datastore:datastore-preferences-core", version.ref = "androidx-datastore" } 31 | kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } 32 | km-logging = { module = "org.lighthousegames:logging", version.ref = "logging" } 33 | molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } 34 | material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" } 35 | 36 | [plugins] 37 | androidApplication = { id = "com.android.application", version.ref = "agp" } 38 | androidLibrary = { id = "com.android.library", version.ref = "agp" } 39 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } 40 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 41 | serialization = { id = "plugin.serialization", version.ref = "serialization" } 42 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AkatsukiRika/ZhouTools/d847ee5dd74e0c661cc153f9cc75550ec7a238b5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 09 11:03:38 CST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /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 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID=T8MA9395PV 2 | BUNDLE_ID=com.tangping.zhoujiang.ZhouTools 3 | APP_NAME=ZhouTools -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AkatsukiRika/ZhouTools/d847ee5dd74e0c661cc153f9cc75550ec7a238b5/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import ComposeApp 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | MainViewControllerKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea() 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDisplayName 6 | Zhou Tools 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.4.0 21 | CFBundleVersion 22 | 5 23 | LSRequiresIPhoneOS 24 | 25 | CADisableMinimumFrameDurationOnPhone 26 | 27 | UIApplicationSceneManifest 28 | 29 | UIApplicationSupportsMultipleScenes 30 | 31 | 32 | UILaunchScreen 33 | 34 | UIRequiredDeviceCapabilities 35 | 36 | armv7 37 | 38 | UISupportedInterfaceOrientations 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOSApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /kotStore/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinMultiplatform) 3 | alias(libs.plugins.androidLibrary) 4 | } 5 | 6 | kotlin { 7 | androidTarget { 8 | compilations.all { 9 | kotlinOptions { 10 | jvmTarget = "1.8" 11 | } 12 | } 13 | } 14 | 15 | listOf( 16 | iosX64(), 17 | iosArm64(), 18 | iosSimulatorArm64() 19 | ).forEach { 20 | it.binaries.framework { 21 | baseName = "kotStore" 22 | isStatic = true 23 | } 24 | } 25 | 26 | sourceSets { 27 | commonMain.dependencies { 28 | implementation(libs.androidx.datastore.core) 29 | } 30 | } 31 | } 32 | 33 | android { 34 | namespace = "com.tangping.kotstore" 35 | compileSdk = 34 36 | defaultConfig { 37 | minSdk = 24 38 | } 39 | compileOptions { 40 | sourceCompatibility = JavaVersion.VERSION_1_8 41 | targetCompatibility = JavaVersion.VERSION_1_8 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /kotStore/src/androidMain/kotlin/com/tangping/kotstore/Platform.android.kt: -------------------------------------------------------------------------------- 1 | package com.tangping.kotstore 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import androidx.datastore.core.DataStore 6 | import androidx.datastore.preferences.core.Preferences 7 | import java.io.File 8 | 9 | @SuppressLint("StaticFieldLeak") 10 | object KotStoreAndroidBase { 11 | var context: Context? = null 12 | private set 13 | 14 | fun init(context: Context) { 15 | this.context = context 16 | } 17 | } 18 | 19 | actual fun createDataStore(name: String): DataStore { 20 | val context = KotStoreAndroidBase.context ?: throw IllegalStateException("Android Context must be initialized!") 21 | return createDataStoreWithDefaults { 22 | File(context.applicationContext.filesDir, name).path 23 | } 24 | } -------------------------------------------------------------------------------- /kotStore/src/commonMain/kotlin/com/tangping/kotstore/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.tangping.kotstore 2 | 3 | import androidx.datastore.core.DataMigration 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler 6 | import androidx.datastore.preferences.core.PreferenceDataStoreFactory 7 | import androidx.datastore.preferences.core.Preferences 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.IO 11 | import kotlinx.coroutines.SupervisorJob 12 | import okio.Path.Companion.toPath 13 | 14 | internal fun createDataStoreWithDefaults( 15 | corruptionHandler: ReplaceFileCorruptionHandler? = null, 16 | coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()), 17 | migrations: List> = emptyList(), 18 | path: () -> String 19 | ) = PreferenceDataStoreFactory.createWithPath( 20 | corruptionHandler = corruptionHandler, 21 | scope = coroutineScope, 22 | migrations = migrations, 23 | produceFile = { 24 | path().toPath() 25 | } 26 | ) 27 | 28 | expect fun createDataStore(name: String): DataStore -------------------------------------------------------------------------------- /kotStore/src/commonMain/kotlin/com/tangping/kotstore/flow/FlowDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.tangping.kotstore.flow 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.launch 6 | 7 | class FlowDelegate internal constructor( 8 | data: Flow, 9 | private val onSave: suspend (TYPE) -> Unit 10 | ) : Flow by data { 11 | suspend fun emit(value: TYPE) { 12 | onSave(value) 13 | } 14 | 15 | fun emitIn(scope: CoroutineScope, value: TYPE) { 16 | scope.launch { 17 | emit(value) 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /kotStore/src/commonMain/kotlin/com/tangping/kotstore/flow/FlowStore.kt: -------------------------------------------------------------------------------- 1 | package com.tangping.kotstore.flow 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import androidx.datastore.preferences.core.edit 6 | import com.tangping.kotstore.model.KotStoreFlowModel 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.map 9 | import kotlin.properties.ReadOnlyProperty 10 | import kotlin.reflect.KProperty 11 | 12 | class FlowStore internal constructor( 13 | private val dataStore: DataStore, 14 | private val key: String, 15 | private val defaultValue: TYPE, 16 | preferenceKeyFactory: (String) -> Preferences.Key 17 | ) : ReadOnlyProperty, Flow> { 18 | private val preferenceKey: Preferences.Key by lazy { 19 | preferenceKeyFactory(key) 20 | } 21 | 22 | override fun getValue(thisRef: KotStoreFlowModel, property: KProperty<*>): FlowDelegate { 23 | return FlowDelegate( 24 | dataStore.data.map { it[preferenceKey] ?: defaultValue } 25 | ) { 26 | dataStore.edit { settings -> 27 | settings[preferenceKey] = it 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /kotStore/src/commonMain/kotlin/com/tangping/kotstore/model/KotStoreFlowModel.kt: -------------------------------------------------------------------------------- 1 | package com.tangping.kotstore.model 2 | 3 | import androidx.datastore.preferences.core.Preferences 4 | import androidx.datastore.preferences.core.booleanPreferencesKey 5 | import androidx.datastore.preferences.core.doublePreferencesKey 6 | import androidx.datastore.preferences.core.floatPreferencesKey 7 | import androidx.datastore.preferences.core.intPreferencesKey 8 | import androidx.datastore.preferences.core.longPreferencesKey 9 | import androidx.datastore.preferences.core.stringPreferencesKey 10 | import com.tangping.kotstore.flow.FlowStore 11 | 12 | abstract class KotStoreFlowModel(storeName: String) : KotStoreModel(storeName = storeName) { 13 | private fun flowStore( 14 | defaultValue: TYPE, 15 | key: String, 16 | preferenceKeyFactory: (String) -> Preferences.Key 17 | ) = FlowStore( 18 | dataStore, 19 | key, 20 | defaultValue, 21 | preferenceKeyFactory 22 | ) 23 | 24 | fun intFlowStore(key: String, default: Int = 0): FlowStore = 25 | flowStore(default, key, ::intPreferencesKey) 26 | 27 | fun longFlowStore(key: String, default: Long = 0L): FlowStore = 28 | flowStore(default, key, ::longPreferencesKey) 29 | 30 | fun floatFlowStore(key: String, default: Float = 0f): FlowStore = 31 | flowStore(default, key, ::floatPreferencesKey) 32 | 33 | fun doubleFlowStore(key: String, default: Double = 0.0): FlowStore = 34 | flowStore(default, key, ::doublePreferencesKey) 35 | 36 | fun booleanFlowStore(key: String, default: Boolean = false): FlowStore = 37 | flowStore(default, key, ::booleanPreferencesKey) 38 | 39 | fun stringFlowStore(key: String, default: String = ""): FlowStore = 40 | flowStore(default, key, ::stringPreferencesKey) 41 | } -------------------------------------------------------------------------------- /kotStore/src/commonMain/kotlin/com/tangping/kotstore/model/KotStoreModel.kt: -------------------------------------------------------------------------------- 1 | package com.tangping.kotstore.model 2 | 3 | import com.tangping.kotstore.createDataStore 4 | import com.tangping.kotstore.store.BooleanStore 5 | import com.tangping.kotstore.store.DoubleStore 6 | import com.tangping.kotstore.store.FloatStore 7 | import com.tangping.kotstore.store.IntStore 8 | import com.tangping.kotstore.store.LongStore 9 | import com.tangping.kotstore.store.StringStore 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.IO 13 | import kotlinx.coroutines.SupervisorJob 14 | import kotlin.properties.ReadWriteProperty 15 | 16 | abstract class KotStoreModel( 17 | val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()), 18 | val storeName: String 19 | ) { 20 | internal val dataStore by lazy { 21 | createDataStore(storeName) 22 | } 23 | 24 | open val syncSaveAllProperties: Boolean = false 25 | 26 | protected fun stringStore( 27 | key: String, 28 | default: String = "", 29 | syncSave: Boolean = syncSaveAllProperties 30 | ): ReadWriteProperty = StringStore(key, default, syncSave) 31 | 32 | protected fun intStore( 33 | key: String, 34 | default: Int = 0, 35 | syncSave: Boolean = syncSaveAllProperties 36 | ): ReadWriteProperty = IntStore(key, default, syncSave) 37 | 38 | protected fun longStore( 39 | key: String, 40 | default: Long = 0L, 41 | syncSave: Boolean = syncSaveAllProperties 42 | ): ReadWriteProperty = LongStore(key, default, syncSave) 43 | 44 | protected fun floatStore( 45 | key: String, 46 | default: Float = 0f, 47 | syncSave: Boolean = syncSaveAllProperties 48 | ): ReadWriteProperty = FloatStore(key, default, syncSave) 49 | 50 | protected fun booleanStore( 51 | key: String, 52 | default: Boolean = false, 53 | syncSave: Boolean = syncSaveAllProperties 54 | ): ReadWriteProperty = BooleanStore(key, default, syncSave) 55 | 56 | protected fun doubleStore( 57 | key: String, 58 | default: Double = 0.0, 59 | syncSave: Boolean = syncSaveAllProperties 60 | ): ReadWriteProperty = DoubleStore(key, default, syncSave) 61 | } -------------------------------------------------------------------------------- /kotStore/src/commonMain/kotlin/com/tangping/kotstore/store/AbstractStore.kt: -------------------------------------------------------------------------------- 1 | package com.tangping.kotstore.store 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import androidx.datastore.preferences.core.edit 6 | import com.tangping.kotstore.model.KotStoreModel 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.flow.first 9 | import kotlinx.coroutines.launch 10 | import kotlinx.coroutines.runBlocking 11 | import kotlin.properties.ReadWriteProperty 12 | import kotlin.reflect.KProperty 13 | 14 | abstract class AbstractStore : ReadWriteProperty { 15 | abstract val key: String 16 | abstract val default: T 17 | abstract val syncSave: Boolean 18 | 19 | abstract fun getPreferencesKey(): Preferences.Key 20 | 21 | override operator fun getValue(thisRef: KotStoreModel, property: KProperty<*>): T { 22 | val preferencesKey = getPreferencesKey() 23 | var value = default 24 | runBlocking { 25 | thisRef.dataStore.data.first { 26 | value = it[preferencesKey] ?: default 27 | true 28 | } 29 | } 30 | return value 31 | } 32 | 33 | override operator fun setValue(thisRef: KotStoreModel, property: KProperty<*>, value: T) { 34 | saveToStore(thisRef.dataStore, thisRef.scope, getPreferencesKey(), value) 35 | } 36 | 37 | private fun saveToStore( 38 | dataStore: DataStore, 39 | scope: CoroutineScope, 40 | preferencesKey: Preferences.Key, 41 | value: T 42 | ) { 43 | if (syncSave) { 44 | runBlocking { 45 | dataStore.edit { mutablePreferences -> 46 | mutablePreferences[preferencesKey] = value 47 | } 48 | } 49 | } else { 50 | scope.launch { 51 | dataStore.edit { mutablePreferences -> 52 | mutablePreferences[preferencesKey] = value 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /kotStore/src/commonMain/kotlin/com/tangping/kotstore/store/BooleanStore.kt: -------------------------------------------------------------------------------- 1 | package com.tangping.kotstore.store 2 | 3 | import androidx.datastore.preferences.core.Preferences 4 | import androidx.datastore.preferences.core.booleanPreferencesKey 5 | 6 | internal class BooleanStore( 7 | override val key: String, 8 | override val default: Boolean, 9 | override val syncSave: Boolean 10 | ) : AbstractStore() { 11 | override fun getPreferencesKey(): Preferences.Key { 12 | return booleanPreferencesKey(key) 13 | } 14 | } -------------------------------------------------------------------------------- /kotStore/src/commonMain/kotlin/com/tangping/kotstore/store/DoubleStore.kt: -------------------------------------------------------------------------------- 1 | package com.tangping.kotstore.store 2 | 3 | import androidx.datastore.preferences.core.Preferences 4 | import androidx.datastore.preferences.core.doublePreferencesKey 5 | 6 | internal class DoubleStore( 7 | override val key: String, 8 | override val default: Double, 9 | override val syncSave: Boolean 10 | ) : AbstractStore() { 11 | override fun getPreferencesKey(): Preferences.Key { 12 | return doublePreferencesKey(key) 13 | } 14 | } -------------------------------------------------------------------------------- /kotStore/src/commonMain/kotlin/com/tangping/kotstore/store/FloatStore.kt: -------------------------------------------------------------------------------- 1 | package com.tangping.kotstore.store 2 | 3 | import androidx.datastore.preferences.core.Preferences 4 | import androidx.datastore.preferences.core.floatPreferencesKey 5 | 6 | internal class FloatStore( 7 | override val key: String, 8 | override val default: Float, 9 | override val syncSave: Boolean 10 | ) : AbstractStore() { 11 | override fun getPreferencesKey(): Preferences.Key { 12 | return floatPreferencesKey(key) 13 | } 14 | } -------------------------------------------------------------------------------- /kotStore/src/commonMain/kotlin/com/tangping/kotstore/store/IntStore.kt: -------------------------------------------------------------------------------- 1 | package com.tangping.kotstore.store 2 | 3 | import androidx.datastore.preferences.core.Preferences 4 | import androidx.datastore.preferences.core.intPreferencesKey 5 | 6 | internal class IntStore( 7 | override val key: String, 8 | override val default: Int, 9 | override val syncSave: Boolean 10 | ) : AbstractStore() { 11 | override fun getPreferencesKey(): Preferences.Key { 12 | return intPreferencesKey(key) 13 | } 14 | } -------------------------------------------------------------------------------- /kotStore/src/commonMain/kotlin/com/tangping/kotstore/store/LongStore.kt: -------------------------------------------------------------------------------- 1 | package com.tangping.kotstore.store 2 | 3 | import androidx.datastore.preferences.core.Preferences 4 | import androidx.datastore.preferences.core.longPreferencesKey 5 | 6 | internal class LongStore( 7 | override val key: String, 8 | override val default: Long, 9 | override val syncSave: Boolean 10 | ) : AbstractStore() { 11 | override fun getPreferencesKey(): Preferences.Key { 12 | return longPreferencesKey(key) 13 | } 14 | } -------------------------------------------------------------------------------- /kotStore/src/commonMain/kotlin/com/tangping/kotstore/store/StringStore.kt: -------------------------------------------------------------------------------- 1 | package com.tangping.kotstore.store 2 | 3 | import androidx.datastore.preferences.core.Preferences 4 | import androidx.datastore.preferences.core.stringPreferencesKey 5 | 6 | internal class StringStore( 7 | override val key: String, 8 | override val default: String, 9 | override val syncSave: Boolean 10 | ) : AbstractStore() { 11 | override fun getPreferencesKey(): Preferences.Key { 12 | return stringPreferencesKey(key) 13 | } 14 | } -------------------------------------------------------------------------------- /kotStore/src/iosMain/kotlin/com/tangping/kotstore/Platform.ios.kt: -------------------------------------------------------------------------------- 1 | package com.tangping.kotstore 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import kotlinx.cinterop.ExperimentalForeignApi 6 | import platform.Foundation.NSDocumentDirectory 7 | import platform.Foundation.NSFileManager 8 | import platform.Foundation.NSURL 9 | import platform.Foundation.NSUserDomainMask 10 | 11 | @OptIn(ExperimentalForeignApi::class) 12 | actual fun createDataStore(name: String): DataStore { 13 | return createDataStoreWithDefaults { 14 | val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory( 15 | directory = NSDocumentDirectory, 16 | inDomain = NSUserDomainMask, 17 | appropriateForURL = null, 18 | create = false, 19 | error = null, 20 | ) 21 | (requireNotNull(documentDirectory).path + "/$name") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ZhouTools" 2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 3 | 4 | pluginManagement { 5 | repositories { 6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 7 | google() 8 | gradlePluginPortal() 9 | mavenCentral() 10 | } 11 | } 12 | 13 | dependencyResolutionManagement { 14 | repositories { 15 | google() 16 | mavenCentral() 17 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 18 | } 19 | } 20 | 21 | include(":composeApp") 22 | include(":kotStore") 23 | --------------------------------------------------------------------------------