diff --git a/app/build.gradle b/app/build.gradle index 578f063..6d0d873 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "lying.fengfeng.foodrecords" minSdk 26 targetSdk 34 - versionCode 6 - versionName "1.5" + versionCode 7 + versionName "1.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index f3b28bf..b00f654 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,8 +11,8 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 2, - "versionName": "1.1", + "versionCode": 7, + "versionName": "1.6", "outputFile": "app-release.apk" } ], diff --git a/app/schemas/lying.fengfeng.foodrecords.repository.FoodInfoDatabase/3.json b/app/schemas/lying.fengfeng.foodrecords.repository.FoodInfoDatabase/3.json new file mode 100644 index 0000000..f1cfcc0 --- /dev/null +++ b/app/schemas/lying.fengfeng.foodrecords.repository.FoodInfoDatabase/3.json @@ -0,0 +1,70 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "2353f822fae49423025f80357796df97", + "entities": [ + { + "tableName": "FoodInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`foodName` TEXT NOT NULL, `productionDate` TEXT NOT NULL, `foodType` TEXT NOT NULL, `shelfLife` TEXT NOT NULL, `expirationDate` TEXT NOT NULL, `uuid` TEXT NOT NULL, `tips` TEXT NOT NULL, PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "foodName", + "columnName": "foodName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "productionDate", + "columnName": "productionDate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "foodType", + "columnName": "foodType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shelfLife", + "columnName": "shelfLife", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "expirationDate", + "columnName": "expirationDate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tips", + "columnName": "tips", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2353f822fae49423025f80357796df97')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/lying/fengfeng/foodrecords/MigrationTest.kt b/app/src/androidTest/java/lying/fengfeng/foodrecords/MigrationTest.kt index b52267b..28906fc 100644 --- a/app/src/androidTest/java/lying/fengfeng/foodrecords/MigrationTest.kt +++ b/app/src/androidTest/java/lying/fengfeng/foodrecords/MigrationTest.kt @@ -1,16 +1,24 @@ package lying.fengfeng.foodrecords +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import android.util.Log import androidx.room.migration.Migration import androidx.room.testing.MigrationTestHelper import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteQuery +import androidx.sqlite.db.SupportSQLiteQueryBuilder import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import lying.fengfeng.foodrecords.repository.AppRepo import lying.fengfeng.foodrecords.repository.FoodInfoDatabase import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Locale @RunWith(AndroidJUnit4::class) class MigrationTest { @@ -45,4 +53,53 @@ class MigrationTest { // MigrationTestHelper automatically verifies the schema changes, // but you need to validate that the data was migrated properly. } + + @Test + @Throws(IOException::class) + fun migrate2To3() { + var db = helper.createDatabase("FoodInfoDatabase", 2).apply { + + val contentValues = ContentValues().apply { + put("foodName", "FoodName") + put("productionDate", "24-07-30") + put("foodType", "Test") + put("shelfLife", "5") + put("expirationDate", "--") + put("uuid", "uuid-test") + put("tips", "") + } + + insert("FoodInfo", SQLiteDatabase. CONFLICT_REPLACE, contentValues) + close() + } + + val MIGRATION_2_3 = object : Migration(2, 3) { + override fun migrate(db: SupportSQLiteDatabase) { + + val cursor = db.query("SELECT uuid, productionDate, expirationDate FROM FoodInfo", + emptyArray()) + if (cursor.moveToFirst()) { + do { + val uuid = cursor.getString(cursor.getColumnIndex("uuid")) + val productionDate = cursor.getString(cursor.getColumnIndex("productionDate")) + val expirationDate = cursor.getString(cursor.getColumnIndex("expirationDate")) + + val dateFormatter = SimpleDateFormat("yy-MM-dd", Locale.getDefault()) + val productionDateTimestamp = dateFormatter.parse(productionDate)?.time ?: 0 + val expirationDateTimestamp = if (expirationDate == "--") 0 else dateFormatter.parse(expirationDate)?.time ?: 0 + val updateQuery = "UPDATE FoodInfo SET productionDate = ?, expirationDate = ? " + + "WHERE uuid = ?" + + db.execSQL(updateQuery, arrayOf(productionDateTimestamp, expirationDateTimestamp, + uuid)) + + } while (cursor.moveToNext()) + } + cursor.close() + + } + } + + db = helper.runMigrationsAndValidate("FoodInfoDatabase", 3, true, MIGRATION_2_3) + } } \ No newline at end of file diff --git a/app/src/main/java/lying/fengfeng/foodrecords/Constants.kt b/app/src/main/java/lying/fengfeng/foodrecords/Constants.kt index 9e16220..652a6b6 100644 --- a/app/src/main/java/lying/fengfeng/foodrecords/Constants.kt +++ b/app/src/main/java/lying/fengfeng/foodrecords/Constants.kt @@ -6,7 +6,7 @@ object Constants { const val DB_NAME_FOOD_INFO = "FoodInfoDatabase" const val DB_NAME_FOOD_TYPE_INFO = "FoodTypeInfoDatabase" const val DB_NAME_SHELF_LIFE_INFO = "ShelfLifeInfoDatabase" - const val FOOD_INFO_DB_VERSION = 2 + const val FOOD_INFO_DB_VERSION = 3 const val FOOD_TYPE_DB_VERSION = 1 const val SHELF_LIFE_DB_VERSION = 1 } \ No newline at end of file diff --git a/app/src/main/java/lying/fengfeng/foodrecords/repository/AppRepo.kt b/app/src/main/java/lying/fengfeng/foodrecords/repository/AppRepo.kt index 80b6f97..d49fecf 100644 --- a/app/src/main/java/lying/fengfeng/foodrecords/repository/AppRepo.kt +++ b/app/src/main/java/lying/fengfeng/foodrecords/repository/AppRepo.kt @@ -1,5 +1,6 @@ package lying.fengfeng.foodrecords.repository +import android.annotation.SuppressLint import android.app.Application import android.content.Context import android.content.SharedPreferences @@ -17,6 +18,8 @@ import lying.fengfeng.foodrecords.R import lying.fengfeng.foodrecords.entities.FoodInfo import lying.fengfeng.foodrecords.entities.FoodTypeInfo import lying.fengfeng.foodrecords.entities.ShelfLifeInfo +import java.text.SimpleDateFormat +import java.util.Locale object AppRepo { @@ -38,6 +41,31 @@ object AppRepo { db.execSQL("ALTER TABLE FoodInfo ADD COLUMN tips TEXT NOT NULL DEFAULT '' ") } } + private val MIGRATION_2_3 = object : Migration(2, 3) { + @SuppressLint("Range") + override fun migrate(db: SupportSQLiteDatabase) { + val cursor = db.query("SELECT uuid, productionDate, expirationDate FROM FoodInfo", + emptyArray()) + if (cursor.moveToFirst()) { + do { + val uuid = cursor.getString(cursor.getColumnIndex("uuid")) + val productionDate = cursor.getString(cursor.getColumnIndex("productionDate")) + val expirationDate = cursor.getString(cursor.getColumnIndex("expirationDate")) + + val dateFormatter = SimpleDateFormat("yy-MM-dd", Locale.getDefault()) + val productionDateTimestamp = dateFormatter.parse(productionDate)?.time ?: 0 + val expirationDateTimestamp = if (expirationDate == "--") 0 else dateFormatter.parse(expirationDate)?.time ?: 0 + val updateQuery = "UPDATE FoodInfo SET productionDate = ?, expirationDate = ? " + + "WHERE uuid = ?" + + db.execSQL(updateQuery, arrayOf(productionDateTimestamp, expirationDateTimestamp, + uuid)) + + } while (cursor.moveToNext()) + } + cursor.close() + } + } fun init(application: Application) { @@ -46,6 +74,7 @@ object AppRepo { foodInfoDB = Room.databaseBuilder(app, FoodInfoDatabase::class.java, DB_NAME_FOOD_INFO) .addMigrations(MIGRATION_1_2) + .addMigrations(MIGRATION_2_3) .build() foodInfoDao = foodInfoDB.foodInfoDao() @@ -134,6 +163,14 @@ object AppRepo { return sp.getLong("next_notification_time", -1) } + fun setDateFormat(format: String) { + sp.edit().putString("date_format", format).apply() + } + + fun getDateFormat(): String { + return sp.getString("date_format", "yy-MM-dd") ?: "yy-MM-dd" + } + private fun addInitializedData() { CoroutineScope(Dispatchers.IO).launch { diff --git a/app/src/main/java/lying/fengfeng/foodrecords/ui/FoodRecordsAppViewModel.kt b/app/src/main/java/lying/fengfeng/foodrecords/ui/FoodRecordsAppViewModel.kt index 725ffa1..5d5df0e 100644 --- a/app/src/main/java/lying/fengfeng/foodrecords/ui/FoodRecordsAppViewModel.kt +++ b/app/src/main/java/lying/fengfeng/foodrecords/ui/FoodRecordsAppViewModel.kt @@ -7,6 +7,7 @@ import android.content.pm.PackageManager import android.os.Build import android.widget.Toast import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.core.app.ActivityCompat @@ -31,7 +32,8 @@ class FoodRecordsAppViewModel: ViewModel() { var foodTypeList = mutableStateListOf() var shelfLifeList = mutableStateListOf() var isNotificationEnabled = mutableStateOf(false) - var daysBeforeNotification = mutableStateOf(AppRepo.getDaysBeforeNotification()) + var daysBeforeNotification = mutableIntStateOf(AppRepo.getDaysBeforeNotification()) + var dateFormat = mutableStateOf(AppRepo.getDateFormat()) init { CoroutineScope(Dispatchers.Main).launch { @@ -109,6 +111,11 @@ class FoodRecordsAppViewModel: ViewModel() { AppRepo.removeShelfLifeInfo(shelfLifeInfo) } + fun updateDaysBeforeNotification(days: Int) { + daysBeforeNotification.value = days + AppRepo.setDaysBeforeNotification(days) + } + fun enableNotification(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) @@ -130,9 +137,9 @@ class FoodRecordsAppViewModel: ViewModel() { } } - fun updateDaysBeforeNotification(days: Int) { - daysBeforeNotification.value = days - AppRepo.setDaysBeforeNotification(days) + fun updateDateFormat(dateFormat: String) { + this.dateFormat.value = dateFormat + AppRepo.setDateFormat(dateFormat) } private fun scheduleNotifications(context: Context) { diff --git a/app/src/main/java/lying/fengfeng/foodrecords/ui/components/AppBars.kt b/app/src/main/java/lying/fengfeng/foodrecords/ui/components/AppBars.kt index 3b097e9..5dcf75d 100644 --- a/app/src/main/java/lying/fengfeng/foodrecords/ui/components/AppBars.kt +++ b/app/src/main/java/lying/fengfeng/foodrecords/ui/components/AppBars.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import lying.fengfeng.foodrecords.MainActivity import lying.fengfeng.foodrecords.R +import lying.fengfeng.foodrecords.repository.AppRepo import lying.fengfeng.foodrecords.utils.EffectUtil import java.text.SimpleDateFormat import java.util.Date @@ -186,6 +187,6 @@ fun FoodRecordsBottomBar( } private fun getCurrentDate(): String { - val dateFormat = SimpleDateFormat("yy-MM-dd", Locale.getDefault()) + val dateFormat = SimpleDateFormat(AppRepo.getDateFormat(), Locale.getDefault()) return dateFormat.format(Date()) } \ No newline at end of file diff --git a/app/src/main/java/lying/fengfeng/foodrecords/ui/components/FoodInfoCard.kt b/app/src/main/java/lying/fengfeng/foodrecords/ui/components/FoodInfoCard.kt index c68bd01..9c10876 100644 --- a/app/src/main/java/lying/fengfeng/foodrecords/ui/components/FoodInfoCard.kt +++ b/app/src/main/java/lying/fengfeng/foodrecords/ui/components/FoodInfoCard.kt @@ -287,7 +287,7 @@ fun RemainingDaysWindow( .fillMaxWidth(), contentAlignment = Alignment.Center ) { - val fontSize = 36.sp + val fontSize = 32.sp val (remainingTitleColor, remainingTitleText) = if (remainingDays > 0) { ExpiredGreen to context.getString(R.string.valid_in) diff --git a/app/src/main/java/lying/fengfeng/foodrecords/ui/components/insertionDialog/InsertionDialog.kt b/app/src/main/java/lying/fengfeng/foodrecords/ui/components/insertionDialog/InsertionDialog.kt index 6543b1c..f279177 100644 --- a/app/src/main/java/lying/fengfeng/foodrecords/ui/components/insertionDialog/InsertionDialog.kt +++ b/app/src/main/java/lying/fengfeng/foodrecords/ui/components/insertionDialog/InsertionDialog.kt @@ -48,9 +48,7 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -76,6 +74,7 @@ import com.ujizin.camposer.state.rememberCameraState import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import lying.fengfeng.foodrecords.R +import lying.fengfeng.foodrecords.repository.AppRepo import lying.fengfeng.foodrecords.ui.FoodRecordsAppViewModel import lying.fengfeng.foodrecords.ui.LocalScreenParams import lying.fengfeng.foodrecords.utils.DateUtil.dateWithFormat @@ -132,7 +131,9 @@ fun InsertionDialog() { dismissOnClickOutside = false ), ) { - Column { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { Card( shape = RoundedCornerShape(12.dp), elevation = CardDefaults.cardElevation(4.dp), @@ -285,10 +286,10 @@ fun InsertionDialog() { datePickerState.selectedDateMillis?.also { if (isExpireDate) { expirationDate = - dateWithFormat(it, "YY-MM-dd") + dateWithFormat(it, AppRepo.getDateFormat()) } else { productionDate = - dateWithFormat(it, "YY-MM-dd") + dateWithFormat(it, AppRepo.getDateFormat()) } } }, diff --git a/app/src/main/java/lying/fengfeng/foodrecords/ui/components/insertionDialog/InsertionDialogViewModel.kt b/app/src/main/java/lying/fengfeng/foodrecords/ui/components/insertionDialog/InsertionDialogViewModel.kt index ee63ea5..4d4f003 100644 --- a/app/src/main/java/lying/fengfeng/foodrecords/ui/components/insertionDialog/InsertionDialogViewModel.kt +++ b/app/src/main/java/lying/fengfeng/foodrecords/ui/components/insertionDialog/InsertionDialogViewModel.kt @@ -18,7 +18,7 @@ class InsertionDialogViewModel : ViewModel() { var productionDate: MutableState = mutableStateOf( DateUtil.dateWithFormat( DateUtil.todayMillis(), - "YY-MM-dd" + AppRepo.getDateFormat() ) ) @@ -31,11 +31,11 @@ class InsertionDialogViewModel : ViewModel() { var uuid: MutableState = mutableStateOf("") var tips: MutableState = mutableStateOf("") - init { -// initParams() - } - fun refreshParams() { + productionDate.value = DateUtil.dateWithFormat( + DateUtil.todayMillis(), + AppRepo.getDateFormat() + ) CoroutineScope(Dispatchers.IO).launch { foodTypes = AppRepo.getAllTypeInfo().map { it.type } shelfLifeList = AppRepo.getAllShelfLifeInfo().map { it.life } @@ -62,7 +62,7 @@ class InsertionDialogViewModel : ViewModel() { productionDate = mutableStateOf( DateUtil.dateWithFormat( DateUtil.todayMillis(), - "YY-MM-dd" + AppRepo.getDateFormat() ) ) expirationDate = mutableStateOf("") diff --git a/app/src/main/java/lying/fengfeng/foodrecords/ui/dice/DicePager.kt b/app/src/main/java/lying/fengfeng/foodrecords/ui/dice/DicePager.kt index 942e5a9..a630055 100644 --- a/app/src/main/java/lying/fengfeng/foodrecords/ui/dice/DicePager.kt +++ b/app/src/main/java/lying/fengfeng/foodrecords/ui/dice/DicePager.kt @@ -1,20 +1,13 @@ package lying.fengfeng.foodrecords.ui.dice -import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton +import androidx.compose.foundation.pager.PagerState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -22,21 +15,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import lying.fengfeng.foodrecords.MainActivity import lying.fengfeng.foodrecords.R @@ -45,12 +31,11 @@ import lying.fengfeng.foodrecords.repository.AppRepo import lying.fengfeng.foodrecords.ui.FoodRecordsAppViewModel import lying.fengfeng.foodrecords.ui.LocalActivityContext import lying.fengfeng.foodrecords.ui.components.FoodInfoCard -import lying.fengfeng.foodrecords.utils.EffectUtil -import kotlin.random.Random +@OptIn(ExperimentalFoundationApi::class) @Composable fun DicePager( - + pagerState: PagerState ) { val activityContext = LocalActivityContext.current @@ -69,7 +54,7 @@ fun DicePager( if (isCardListEmpty) { EmptyView() } else { - DiceView(cardDataList = foodInfoList) + DiceView(cardDataList = foodInfoList, pagerState = pagerState) } } @@ -85,18 +70,13 @@ fun EmptyView() { @OptIn(ExperimentalFoundationApi::class) @Composable -fun DiceView(cardDataList: List) { +fun DiceView(cardDataList: List, pagerState: PagerState) { Column( modifier = Modifier, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly ) { - val context = LocalContext.current - val pagerState = rememberPagerState(pageCount = { - cardDataList.size - }) - Box( modifier = Modifier.fillMaxWidth(0.5f).wrapContentHeight(), ) { @@ -107,83 +87,5 @@ fun DiceView(cardDataList: List) { FoodInfoCard(foodInfo = cardDataList[page]) } } - - val coroutineScope = rememberCoroutineScope() - var isRollButtonEnabled by remember { mutableStateOf(true) } - - IconButton( - onClick = { - coroutineScope.launch { - isRollButtonEnabled = false - val pageList = selectRandomElements(cardDataList, 13) - val smoothList = generateDeceleratedSequence( - 100, - 800, - pageList.size, - 0.75 - ) - for ((pageIndex, pageValue) in pageList.withIndex()) { - delay(smoothList[pageIndex].toLong()) - - val indexOfCard = cardDataList.indexOf(pageValue) - pagerState.scrollToPage( - page = indexOfCard - ) - - EffectUtil.playSoundEffect(context) - EffectUtil.playVibrationEffect(context) - - } - isRollButtonEnabled = true - EffectUtil.playNotification(context) - Toast.makeText(context, context.getString(R.string.pick_it), Toast.LENGTH_SHORT).show() - } - - }, - modifier = Modifier - .padding(16.dp) - .wrapContentSize() - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer), - enabled = isRollButtonEnabled - ) { - Icon(painter = painterResource(id = R.drawable.dice5_svg), null) - } } -} - -fun generateDeceleratedSequence(min: Int, max: Int, count: Int, percent: Double): List { - if (min >= max) throw IllegalArgumentException("Min should be less than Max.") - if (percent < 0.0 || percent > 1.0) throw IllegalArgumentException("Percent must be between 0 and 1.") - if (count <= 0) throw IllegalArgumentException("Count must be positive.") - - val sequence = MutableList(count) { min } - val startDecelerationIndex = (percent * count).toInt() - val decelerationLength = count - startDecelerationIndex - - for (i in 0 until decelerationLength) { - val decelerationFactor = i.toDouble() / (decelerationLength - 1) // 计算减速因子 - sequence[startDecelerationIndex + i] = min + ((max - min) * decelerationFactor).toInt() - } - - return sequence -} - -fun selectRandomElements(givenList: List, count: Int): List { - val selectedList = mutableListOf() - val random = Random(System.currentTimeMillis()) - - if (givenList.size < count) { - selectedList.addAll(givenList) - } - - val remainingCount = count - selectedList.size - for (i in 0 until remainingCount) { - val randomIndex = random.nextInt(givenList.size) - selectedList.add(givenList[randomIndex]) - } - - selectedList.shuffle(random) - - return selectedList } \ No newline at end of file diff --git a/app/src/main/java/lying/fengfeng/foodrecords/ui/dice/RollScreen.kt b/app/src/main/java/lying/fengfeng/foodrecords/ui/dice/RollScreen.kt index c17789a..5c0c7d6 100644 --- a/app/src/main/java/lying/fengfeng/foodrecords/ui/dice/RollScreen.kt +++ b/app/src/main/java/lying/fengfeng/foodrecords/ui/dice/RollScreen.kt @@ -1,21 +1,45 @@ package lying.fengfeng.foodrecords.ui.dice +import android.widget.Toast +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import lying.fengfeng.foodrecords.MainActivity import lying.fengfeng.foodrecords.R +import lying.fengfeng.foodrecords.entities.FoodInfo +import lying.fengfeng.foodrecords.ui.FoodRecordsAppViewModel +import lying.fengfeng.foodrecords.ui.LocalActivityContext +import lying.fengfeng.foodrecords.utils.EffectUtil +import kotlin.random.Random +@OptIn(ExperimentalFoundationApi::class) @Composable fun RollScreen() { @@ -23,11 +47,23 @@ fun RollScreen() { Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceEvenly + verticalArrangement = Arrangement.SpaceBetween ) { + + val coroutineScope = rememberCoroutineScope() + val activityContext = LocalActivityContext.current + val appViewModel: FoodRecordsAppViewModel = + viewModel(viewModelStoreOwner = (activityContext as MainActivity)) + val foodInfoList: List = remember { appViewModel.foodInfoList } + val pagerState = rememberPagerState(pageCount = { + foodInfoList.size + }) + var isRollButtonEnabled by remember { mutableStateOf(foodInfoList.isNotEmpty()) } + Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.padding(32.dp) ) { Text( text = context.getString(R.string.roll_title_primary), @@ -35,11 +71,9 @@ fun RollScreen() { fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary, lineHeight = 48.sp, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + modifier = Modifier.padding(vertical = 16.dp) ) - - Spacer(modifier = Modifier.padding(6.dp)) - Text( text = context.getString(R.string.roll_title_secondary), fontSize = 36.sp, @@ -50,6 +84,87 @@ fun RollScreen() { ) } - DicePager() + Box { + DicePager(pagerState) + } + + + IconButton( + onClick = { + if (foodInfoList.size == 1) { + Toast.makeText(context, context.getString(R.string.only_one), Toast.LENGTH_SHORT).show() + return@IconButton + } + coroutineScope.launch { + isRollButtonEnabled = false + val pageList = selectRandomElements(foodInfoList, 13) + val smoothList = generateDeceleratedSequence( + 100, + 800, + pageList.size, + 0.75 + ) + for ((pageIndex, pageValue) in pageList.withIndex()) { + delay(smoothList[pageIndex].toLong()) + + val indexOfCard = foodInfoList.indexOf(pageValue) + pagerState.scrollToPage( + page = indexOfCard + ) + + EffectUtil.playSoundEffect(context) + EffectUtil.playVibrationEffect(context) + + } + isRollButtonEnabled = true + EffectUtil.playNotification(context) + Toast.makeText(context, context.getString(R.string.pick_it), Toast.LENGTH_SHORT) + .show() + } + }, + modifier = Modifier + .padding(32.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer), + enabled = isRollButtonEnabled + ) { + Icon(painter = painterResource(id = R.drawable.dice5_svg), null) + } } +} + +fun selectRandomElements(givenList: List, count: Int): List { + val selectedList = mutableListOf() + val random = Random(System.currentTimeMillis()) + + if (givenList.size < count) { + selectedList.addAll(givenList) + } + + val remainingCount = count - selectedList.size + for (i in 0 until remainingCount) { + val randomIndex = random.nextInt(givenList.size) + selectedList.add(givenList[randomIndex]) + } + + selectedList.shuffle(random) + + return selectedList +} + +fun generateDeceleratedSequence(min: Int, max: Int, count: Int, percent: Double): List { + if (min >= max) throw IllegalArgumentException("Min should be less than Max.") + if (percent < 0.0 || percent > 1.0) throw IllegalArgumentException("Percent must be between 0 and 1.") + if (count <= 0) throw IllegalArgumentException("Count must be positive.") + + val sequence = MutableList(count) { min } + val startDecelerationIndex = (percent * count).toInt() + val decelerationLength = count - startDecelerationIndex + + for (i in 0 until decelerationLength) { + val decelerationFactor = i.toDouble() / (decelerationLength - 1) // 计算减速因子 + sequence[startDecelerationIndex + i] = min + ((max - min) * decelerationFactor).toInt() + } + + return sequence } \ No newline at end of file diff --git a/app/src/main/java/lying/fengfeng/foodrecords/ui/settings/SettingsScreen.kt b/app/src/main/java/lying/fengfeng/foodrecords/ui/settings/SettingsScreen.kt index de46ddd..fd0dd5f 100644 --- a/app/src/main/java/lying/fengfeng/foodrecords/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/lying/fengfeng/foodrecords/ui/settings/SettingsScreen.kt @@ -18,10 +18,12 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.staggeredgrid.LazyHorizontalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells @@ -34,6 +36,7 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Mail import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.Numbers import androidx.compose.material.icons.filled.Remove import androidx.compose.material.icons.outlined.Remove import androidx.compose.material3.AlertDialog @@ -77,6 +80,7 @@ import lying.fengfeng.foodrecords.MainActivity import lying.fengfeng.foodrecords.R import lying.fengfeng.foodrecords.entities.FoodTypeInfo import lying.fengfeng.foodrecords.entities.ShelfLifeInfo +import lying.fengfeng.foodrecords.repository.AppRepo import lying.fengfeng.foodrecords.ui.FoodRecordsAppViewModel import lying.fengfeng.foodrecords.ui.LocalActivityContext @@ -102,6 +106,7 @@ fun SettingsScreen( var foodTypeOptionExpanded by remember { mutableStateOf(false) } var shelfOptionExpanded by remember { mutableStateOf(false) } + var dateFormatOptionExpanded by remember { mutableStateOf(false) } var notificationOptionExpanded by remember { mutableStateOf(false) } var infoExpanded by remember { mutableStateOf(false) } var wechatInfoExpanded by remember { mutableStateOf(false) } @@ -161,6 +166,7 @@ fun SettingsScreen( notificationOptionExpanded = false infoExpanded = false wechatInfoExpanded = false + dateFormatOptionExpanded = false } ) } @@ -282,6 +288,7 @@ fun SettingsScreen( notificationOptionExpanded = false infoExpanded = false wechatInfoExpanded = false + dateFormatOptionExpanded = false } ) } @@ -357,12 +364,124 @@ fun SettingsScreen( } } + Column( + modifier = Modifier + .wrapContentSize() + .padding(vertical = 8.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(12.dp) + ) { + Icon( + imageVector = Icons.Filled.Numbers, + contentDescription = null, + modifier = Modifier.size(iconSize) + ) + Text( + text = stringResource(id = R.string.date_format_option), + fontSize = titleFontSize, + modifier = Modifier.padding(start = 8.dp) + ) + + Spacer(modifier = Modifier.weight(1f)) + + Icon( + imageVector = Icons.Filled.ArrowDropDown, + contentDescription = null, + modifier = Modifier + .size(iconSize) + .rotate(if (dateFormatOptionExpanded) 180f else 0f) + .clickable { + dateFormatOptionExpanded = !dateFormatOptionExpanded + foodTypeOptionExpanded = false + shelfOptionExpanded = false + notificationOptionExpanded = false + infoExpanded = false + wechatInfoExpanded = false + } + ) + } + + AnimatedVisibility(visible = dateFormatOptionExpanded) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + var currentDateFormat by remember { appViewModel.dateFormat } + + LazyHorizontalStaggeredGrid( + rows = StaggeredGridCells.Fixed(1), + modifier = Modifier.heightIn(max = 48.dp), + contentPadding = PaddingValues(horizontal = 8.dp) + ) { + item { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = currentDateFormat == "yy-MM-dd", + onCheckedChange = { + currentDateFormat = "yy-MM-dd" + appViewModel.updateDateFormat(currentDateFormat) + } + ) + Text( + text = "YY-MM-DD", + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(8.dp) + ) + } + } + + item { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = currentDateFormat == "dd-MM-yy", + onCheckedChange = { + currentDateFormat = "dd-MM-yy" + appViewModel.updateDateFormat(currentDateFormat) + } + ) + Text( + text = "DD-MM-YY", + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(8.dp) + ) + } + } + + item { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = currentDateFormat == "MM-dd-yy", + onCheckedChange = { + currentDateFormat = "MM-dd-yy" + appViewModel.updateDateFormat(currentDateFormat) + } + ) + Text( + text = "MM-DD-YY", + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(8.dp) + ) + } + } + } + } + } + } + Column( modifier = Modifier .wrapContentSize() .padding(vertical = 8.dp), ) { Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(12.dp) ) { Icon( @@ -391,6 +510,7 @@ fun SettingsScreen( notificationOptionExpanded = !notificationOptionExpanded infoExpanded = false wechatInfoExpanded = false + dateFormatOptionExpanded = false } ) } @@ -454,6 +574,7 @@ fun SettingsScreen( .padding(vertical = 8.dp), ) { Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(12.dp) ) { Icon( @@ -482,6 +603,7 @@ fun SettingsScreen( notificationOptionExpanded = false infoExpanded = !infoExpanded wechatInfoExpanded = false + dateFormatOptionExpanded = false } ) } @@ -718,7 +840,8 @@ fun NumberPickerWithButtons( } onNumberChange(number) }, - modifier = Modifier.size(40.dp) + modifier = Modifier + .size(40.dp) .clip(CircleShape) .background(MaterialTheme.colorScheme.primaryContainer) ) { @@ -741,7 +864,8 @@ fun NumberPickerWithButtons( onNumberChange(number) } }, - modifier = Modifier.size(40.dp) + modifier = Modifier + .size(40.dp) .clip(CircleShape) .background(MaterialTheme.colorScheme.primaryContainer) ) { @@ -835,7 +959,7 @@ fun GitHubButton() { } } -fun isAppInstalled(packageManager: PackageManager, packageName: String): Boolean { +private fun isAppInstalled(packageManager: PackageManager, packageName: String): Boolean { return try { packageManager.getPackageInfo(packageName, 0) true diff --git a/app/src/main/java/lying/fengfeng/foodrecords/utils/DateUtil.kt b/app/src/main/java/lying/fengfeng/foodrecords/utils/DateUtil.kt index 4ed4786..4af341e 100644 --- a/app/src/main/java/lying/fengfeng/foodrecords/utils/DateUtil.kt +++ b/app/src/main/java/lying/fengfeng/foodrecords/utils/DateUtil.kt @@ -1,6 +1,7 @@ package lying.fengfeng.foodrecords.utils import lying.fengfeng.foodrecords.entities.FoodInfo +import lying.fengfeng.foodrecords.repository.AppRepo import java.text.ParseException import java.text.SimpleDateFormat import java.time.LocalDate @@ -18,7 +19,7 @@ object DateUtil { } fun dateTimeStamp(dateString: String): Long { - val dateFormatter = SimpleDateFormat("yy-MM-dd", Locale.getDefault()) + val dateFormatter = SimpleDateFormat(AppRepo.getDateFormat(), Locale.getDefault()) return dateFormatter.parse(dateString)?.time ?: 0 } @@ -31,20 +32,14 @@ object DateUtil { } fun getRemainingDays(productionDate: String, shelfLife: String, expirationDate: String): Int { - val dateFormatter = SimpleDateFormat("yy-MM-dd", Locale.getDefault()) - return try { - dateFormatter.parse(expirationDate) - val expirationTimeMillis = dateFormatter.parse(expirationDate).time + if (expirationDate == "0") { + val productionTimeMillis = productionDate.toLong() + val expirationTimeMillis = productionTimeMillis + shelfLife.toLong() * (24 * 60 * 60 * 1000) + return ((expirationTimeMillis - System.currentTimeMillis()) / (24 * 60 * 60 * 1000)).toInt() + } else { + val expirationTimeMillis = expirationDate.toLong() val remainingMillis = expirationTimeMillis - System.currentTimeMillis() - (remainingMillis / (24 * 60 * 60 * 1000)).toInt() - } catch (e: ParseException) { - try { - val productionTimeMillis = dateFormatter.parse(productionDate).time - val expirationTimeMillis = productionTimeMillis + shelfLife.toLong() * (24 * 60 * 60 * 1000) - ((expirationTimeMillis - System.currentTimeMillis()) / (24 * 60 * 60 * 1000)).toInt() - } catch (e: ParseException) { - 0 - } + return (remainingMillis / (24 * 60 * 60 * 1000)).toInt() } } @@ -52,28 +47,23 @@ object DateUtil { val productionDate = foodInfo.productionDate val shelfLife = foodInfo.shelfLife val expirationDate = foodInfo.expirationDate - val dateFormatter = SimpleDateFormat("yy-MM-dd", Locale.getDefault()) - return try { - dateFormatter.parse(expirationDate) - expirationDate - } catch (e: ParseException) { - try { - val productionTimeMillis = dateFormatter.parse(productionDate).time - val expirationTimeMillis = productionTimeMillis + shelfLife.toInt() * 24 * 60 * 60 * 1000 - dateFormatter.format(expirationTimeMillis) - } catch (e: ParseException) { - "--" - } + val dateFormatter = SimpleDateFormat(AppRepo.getDateFormat(), Locale.getDefault()) + if (expirationDate == "0") { + val productionTimeMillis = productionDate.toLong() + val expirationTimeMillis = productionTimeMillis + shelfLife.toInt() * 24 * 60 * 60 * 1000 + return dateFormatter.format(expirationTimeMillis) + } else { + return dateFormatter.format(expirationDate.toLong()) } } fun validateDateFormat(date: String): String { - val dateFormatter = SimpleDateFormat("yy-MM-dd", Locale.getDefault()) + val dateFormatter = SimpleDateFormat(AppRepo.getDateFormat(), Locale.getDefault()) return try { - dateFormatter.parse(date) - date + val parsedDate = dateFormatter.parse(date) + parsedDate?.time.toString() } catch (e: ParseException) { - "--" + "0" } } diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6060c2e..c052e9f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -45,4 +45,6 @@ exporter des données Importer des données Veuillez entrer le nom + Paramètres de format de date + Un seul, mon ami :) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2e03446..9c889cc 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -46,4 +46,6 @@ 导出数据 导入数据 请输入名称 + 日期格式设置 + 就这一个,不用挑了:) \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 999c271..03430dc 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -46,4 +46,6 @@ 匯出數據 導入數據 請輸入名稱 + 日期格式設定 + 就這一個,不用挑了:) \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index e92f6d6..3c5f9e8 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -46,4 +46,6 @@ 匯出數據 導入數據 請輸入名稱 + 日期格式設定 + 就這一個,不用挑了:) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e4d8d09..99f52a8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,13 +2,13 @@ Fridgey Day(s) Add new food! - Production Date - Expiration Date + PD + ED Type Shelf Life Name Expired - Valid in + Valid Delete OK Cancel @@ -45,4 +45,6 @@ Export data Import data Please enter a name + Date Format Settings + Only one, my friend :) \ No newline at end of file diff --git a/metadata/en-US/changelogs/7.txt b/metadata/en-US/changelogs/7.txt new file mode 100644 index 0000000..ecc7ab6 --- /dev/null +++ b/metadata/en-US/changelogs/7.txt @@ -0,0 +1,2 @@ +Added the function of modifying the date format +Optimized the view of the dice page \ No newline at end of file diff --git a/metadata/fr/changelogs/7.txt b/metadata/fr/changelogs/7.txt new file mode 100644 index 0000000..fe0966c --- /dev/null +++ b/metadata/fr/changelogs/7.txt @@ -0,0 +1,2 @@ +Ajout de la possibilité de modifier le format de la date +Optimisation de la vue de la page des dés \ No newline at end of file diff --git a/metadata/zh-CN/changelogs/7.txt b/metadata/zh-CN/changelogs/7.txt new file mode 100644 index 0000000..350ad52 --- /dev/null +++ b/metadata/zh-CN/changelogs/7.txt @@ -0,0 +1,2 @@ +新增了修改日期格式的功能 +优化了骰子页面的视图 \ No newline at end of file diff --git a/metadata/zh-HK/changelogs/7.txt b/metadata/zh-HK/changelogs/7.txt new file mode 100644 index 0000000..c00f778 --- /dev/null +++ b/metadata/zh-HK/changelogs/7.txt @@ -0,0 +1,2 @@ +新增了修改日期格式的功能 +優化了骰子頁面的視圖 \ No newline at end of file diff --git a/metadata/zh-TW/changelogs/7.txt b/metadata/zh-TW/changelogs/7.txt new file mode 100644 index 0000000..c00f778 --- /dev/null +++ b/metadata/zh-TW/changelogs/7.txt @@ -0,0 +1,2 @@ +新增了修改日期格式的功能 +優化了骰子頁面的視圖 \ No newline at end of file