diff --git a/app/src/main/java/me/grey/picquery/common/AppModules.kt b/app/src/main/java/me/grey/picquery/common/AppModules.kt index 7b6d95b..a0367a9 100644 --- a/app/src/main/java/me/grey/picquery/common/AppModules.kt +++ b/app/src/main/java/me/grey/picquery/common/AppModules.kt @@ -42,9 +42,9 @@ private val dataModules = module { AppDatabase::class.java, "app-db" ).build() } - + single { get().embeddingDao() } single { AlbumRepository(androidContext().contentResolver, database = get()) } - single { EmbeddingRepository(database = get()) } + single { EmbeddingRepository(dataSource = get()) } single { PhotoRepository(androidContext()) } single { PreferenceRepository() } } diff --git a/app/src/main/java/me/grey/picquery/common/AssetUtil.kt b/app/src/main/java/me/grey/picquery/common/AssetUtil.kt index e79ee0f..ef43eab 100644 --- a/app/src/main/java/me/grey/picquery/common/AssetUtil.kt +++ b/app/src/main/java/me/grey/picquery/common/AssetUtil.kt @@ -3,9 +3,7 @@ package me.grey.picquery.common import android.content.Context import android.util.Log import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.io.* @@ -37,7 +35,12 @@ object AssetUtil { } } - private suspend fun copyAssets(context: Context, sourceAsset: String, targetFolder: File, assets: Array) { + private suspend fun copyAssets( + context: Context, + sourceAsset: String, + targetFolder: File, + assets: Array + ) { val assetManager = context.assets for (itemInFolder in assets) { val currentAssetPath = "$sourceAsset/$itemInFolder" @@ -54,25 +57,24 @@ object AssetUtil { } @Throws(IOException::class) - suspend fun copyAssetFile(context: Context, sourceAsset: String, target: File) { + fun copyAssetFile(context: Context, sourceAsset: String, target: File) { if (target.exists() && target.length() > 0) { return } - coroutineScope { - val inputStream: InputStream = context.assets.open(sourceAsset) - val outputStream: OutputStream = FileOutputStream(target) + val inputStream: InputStream = context.assets.open(sourceAsset) + val outputStream: OutputStream = FileOutputStream(target) - inputStream.use { inputs -> - outputStream.use { os -> - val buffer = ByteArray(4 * 1024) - var read: Int - while (inputs.read(buffer).also { read = it } != -1) { - os.write(buffer, 0, read) - } - os.flush() + inputStream.use { inputs -> + outputStream.use { os -> + val buffer = ByteArray(4 * 1024) + var read: Int + while (inputs.read(buffer).also { read = it } != -1) { + os.write(buffer, 0, read) } + os.flush() } + } } @@ -98,7 +100,7 @@ object AssetUtil { } return try { - runBlocking { copyAssetFile(context, assetName, file) } + copyAssetFile(context, assetName, file) file.absolutePath } catch (_: Exception) { "" @@ -127,7 +129,7 @@ object AssetUtil { } return try { - runBlocking { copyAssetFile(context, assetName, file) } + copyAssetFile(context, assetName, file) file } catch (_: Exception) { null diff --git a/app/src/main/java/me/grey/picquery/common/UiUtil.kt b/app/src/main/java/me/grey/picquery/common/UiUtil.kt index 9ef6d2a..6dad27d 100644 --- a/app/src/main/java/me/grey/picquery/common/UiUtil.kt +++ b/app/src/main/java/me/grey/picquery/common/UiUtil.kt @@ -6,6 +6,8 @@ import androidx.compose.animation.ExitTransition import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf @@ -45,6 +47,9 @@ object Animation { val navigateInAnimation = fadeIn(tween(DEFAULT_NAVIGATION_ANIMATION_DURATION)) val navigateUpAnimation = fadeOut(tween(DEFAULT_NAVIGATION_ANIMATION_DURATION)) + val popInAnimation = slideInHorizontally { width -> width } + val popUpAnimation = slideOutHorizontally { width -> -width } + fun enterAnimation(durationMillis: Int): EnterTransition = fadeIn(tween(durationMillis)) diff --git a/app/src/main/java/me/grey/picquery/data/data_source/AlbumRepository.kt b/app/src/main/java/me/grey/picquery/data/data_source/AlbumRepository.kt index 0a5e37a..7aee96f 100644 --- a/app/src/main/java/me/grey/picquery/data/data_source/AlbumRepository.kt +++ b/app/src/main/java/me/grey/picquery/data/data_source/AlbumRepository.kt @@ -32,8 +32,6 @@ class AlbumRepository( private val albumCollection: Uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) - // 查询相册 - // 本质上其实是还是遍历所有的图片,但是手动将他们归类和统计数量 fun getAllAlbums(): List { val queryAlbums = contentResolver.query( albumCollection, diff --git a/app/src/main/java/me/grey/picquery/data/data_source/EmbeddingRepository.kt b/app/src/main/java/me/grey/picquery/data/data_source/EmbeddingRepository.kt index 808699d..dee72fb 100644 --- a/app/src/main/java/me/grey/picquery/data/data_source/EmbeddingRepository.kt +++ b/app/src/main/java/me/grey/picquery/data/data_source/EmbeddingRepository.kt @@ -1,28 +1,27 @@ package me.grey.picquery.data.data_source import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.flow -import me.grey.picquery.data.AppDatabase +import me.grey.picquery.data.dao.EmbeddingDao import me.grey.picquery.data.model.Album import me.grey.picquery.data.model.Embedding import java.util.concurrent.LinkedBlockingDeque class EmbeddingRepository( - private val database: AppDatabase + private val dataSource: EmbeddingDao ) { companion object { private const val TAG = "EmbeddingRepo" } fun getAll(): List { - return database.embeddingDao().getAll() + return dataSource.getAll() } fun getAllEmbeddingsPaginated(batchSize: Int): Flow> = flow { var offset = 0 while (true) { - val embeddings = database.embeddingDao().getEmbeddingsPaginated(batchSize, offset) + val embeddings = dataSource.getEmbeddingsPaginated(batchSize, offset) if (embeddings.isEmpty()) { emit(emptyList()) break @@ -33,21 +32,21 @@ class EmbeddingRepository( } fun getTotalCount(): Long { - return database.embeddingDao().getTotalCount() + return dataSource.getTotalCount() } fun getByAlbumId(albumId: Long): List { - return database.embeddingDao().getAllByAlbumId(albumId) + return dataSource.getAllByAlbumId(albumId) } fun getByAlbumList(albumList: List): List { - return database.embeddingDao().getByAlbumIdList(albumList.map { it.id }) + return dataSource.getByAlbumIdList(albumList.map { it.id }) } fun getEmbeddingsByAlbumIdsPaginated(albumIds: List, batchSize: Int): Flow> = flow { var offset = 0 while (true) { - val embeddings = database.embeddingDao().getByAlbumIdList(albumIds, batchSize, offset) + val embeddings = dataSource.getByAlbumIdList(albumIds, batchSize, offset) if (embeddings.isEmpty()) { emit(emptyList()) break @@ -58,7 +57,7 @@ class EmbeddingRepository( } fun update(emb: Embedding) { - return database.embeddingDao().upsertAll(listOf(emb)) + return dataSource.upsertAll(listOf(emb)) } fun updateList(e: Embedding) { @@ -66,7 +65,7 @@ class EmbeddingRepository( if (cacheLinkedBlockingDeque.size >= 300) { val toUpdate = cacheLinkedBlockingDeque.toList() cacheLinkedBlockingDeque.clear() - return database.embeddingDao().upsertAll(toUpdate) + return dataSource.upsertAll(toUpdate) } } @@ -74,12 +73,12 @@ class EmbeddingRepository( if (cacheLinkedBlockingDeque.isNotEmpty()) { val toUpdate = cacheLinkedBlockingDeque.toList() cacheLinkedBlockingDeque.clear() - return database.embeddingDao().upsertAll(toUpdate) + return dataSource.upsertAll(toUpdate) } } private val cacheLinkedBlockingDeque = LinkedBlockingDeque() fun updateAll(list: List) { - return database.embeddingDao().upsertAll(list) + return dataSource.upsertAll(list) } } \ No newline at end of file diff --git a/app/src/main/java/me/grey/picquery/feature/mobileclip2/ImageEncoderMobileCLIPv2.kt b/app/src/main/java/me/grey/picquery/feature/mobileclip2/ImageEncoderMobileCLIPv2.kt index 2e1db6e..f0433fc 100644 --- a/app/src/main/java/me/grey/picquery/feature/mobileclip2/ImageEncoderMobileCLIPv2.kt +++ b/app/src/main/java/me/grey/picquery/feature/mobileclip2/ImageEncoderMobileCLIPv2.kt @@ -3,6 +3,9 @@ package me.grey.picquery.feature.mobileclip2 import android.content.Context import android.graphics.Bitmap import android.util.Log +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import me.grey.picquery.PicQueryApplication import me.grey.picquery.common.AssetUtil import me.grey.picquery.feature.base.ImageEncoder import org.tensorflow.lite.DataType @@ -29,6 +32,7 @@ class ImageEncoderMobileCLIPv2(context: Context, private val preprocessor: Prepr } init { + val modelFile = AssetUtil.assetFile(context, MODEL_PATH) ?: throw FileNotFoundException("Model: $MODEL_PATH not exist.") diff --git a/app/src/main/java/me/grey/picquery/ui/AppNavHost.kt b/app/src/main/java/me/grey/picquery/ui/AppNavHost.kt index fef6fd5..5aa860e 100644 --- a/app/src/main/java/me/grey/picquery/ui/AppNavHost.kt +++ b/app/src/main/java/me/grey/picquery/ui/AppNavHost.kt @@ -1,16 +1,16 @@ package me.grey.picquery.ui import android.net.Uri +import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation.NavHostController -import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument import me.grey.picquery.common.Animation.navigateInAnimation import me.grey.picquery.common.Animation.navigateUpAnimation +import me.grey.picquery.common.Animation.popInAnimation import me.grey.picquery.common.Routes import me.grey.picquery.ui.display.DisplayScreen import me.grey.picquery.ui.home.HomeScreen @@ -26,7 +26,6 @@ fun AppNavHost( NavHost( navController, startDestination = startDestination, - // TODO: Animation when switching screens enterTransition = { navigateInAnimation }, exitTransition = { navigateUpAnimation }, ) { @@ -49,15 +48,15 @@ fun AppNavHost( initialQuery = queryText, onNavigateBack = { navController.popBackStack() }, onClickPhoto = { _, index -> + Log.d("AppNavHost", "onClickPhoto: $index") navController.navigate("${Routes.Display.name}/${index}") }, ) } composable( "${Routes.Display.name}/{index}", - arguments = listOf(navArgument("index") { type = NavType.IntType }) ) { - val initialIndex: Int = it.arguments?.getInt("index") ?: 0 + val initialIndex: Int = it.arguments?.getString("index")?.toInt() ?: 0 DisplayScreen( initialPage = initialIndex, onNavigateBack = { @@ -65,7 +64,12 @@ fun AppNavHost( }, ) } - composable(Routes.Setting.name) { + composable( + Routes.Setting.name, + enterTransition = { popInAnimation }, + popEnterTransition = { popInAnimation }, + exitTransition = { navigateUpAnimation }, + ) { SettingScreen( onNavigateBack = { navController.popBackStack() }, ) diff --git a/app/src/main/java/me/grey/picquery/ui/display/DisplayScreen.kt b/app/src/main/java/me/grey/picquery/ui/display/DisplayScreen.kt index 4a7e17f..5af7c36 100644 --- a/app/src/main/java/me/grey/picquery/ui/display/DisplayScreen.kt +++ b/app/src/main/java/me/grey/picquery/ui/display/DisplayScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -44,7 +45,6 @@ import androidx.compose.ui.unit.dp import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import me.grey.picquery.R -import me.grey.picquery.common.InitializeEffect import me.grey.picquery.data.model.Photo import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.zoomable @@ -64,10 +64,20 @@ fun DisplayScreen( initialPageOffsetFraction = 0f, pageCount = { photoList.size } ) - InitializeEffect { - displayViewModel.loadPhotos(initialPage) - pagerState.scrollToPage(initialPage) + + LaunchedEffect(initialPage) { + displayViewModel.loadPhotos() + if (photoList.isNotEmpty()) { + pagerState.scrollToPage(initialPage) + } } + + LaunchedEffect(photoList) { + if (photoList.isNotEmpty()) { + pagerState.scrollToPage(initialPage) + } + } + Scaffold( topBar = { if (pagerState.currentPage + 1 <= photoList.size) { @@ -146,7 +156,7 @@ private fun TopPhotoInfoBar(currentPhoto: Photo) { } -// TODO 标明出处 + @OptIn( ExperimentalFoundationApi::class, ExperimentalGlideComposeApi::class, ) diff --git a/app/src/main/java/me/grey/picquery/ui/display/DisplayViewModel.kt b/app/src/main/java/me/grey/picquery/ui/display/DisplayViewModel.kt index d93a3fc..1f5e30e 100644 --- a/app/src/main/java/me/grey/picquery/ui/display/DisplayViewModel.kt +++ b/app/src/main/java/me/grey/picquery/ui/display/DisplayViewModel.kt @@ -1,6 +1,5 @@ package me.grey.picquery.ui.display -import androidx.compose.runtime.mutableIntStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow @@ -17,10 +16,8 @@ class DisplayViewModel( private val _photoList = MutableStateFlow>(mutableListOf()) val photoList:StateFlow> = _photoList - private val currentIndex = mutableIntStateOf(0) - fun loadPhotos(initialPage: Int) { - currentIndex.intValue = initialPage + fun loadPhotos() { viewModelScope.launch { val ids = imageSearcher.searchResultIds val list = reorderList(photoRepository.getPhotoListByIds(ids),ids) diff --git a/app/src/main/java/me/grey/picquery/ui/search/SearchResultList.kt b/app/src/main/java/me/grey/picquery/ui/search/SearchResultList.kt index 0a8383f..de882b8 100644 --- a/app/src/main/java/me/grey/picquery/ui/search/SearchResultList.kt +++ b/app/src/main/java/me/grey/picquery/ui/search/SearchResultList.kt @@ -40,9 +40,6 @@ fun SearchResultGrid( state: SearchState, onClickPhoto: (Photo, Int) -> Unit, ) { - fun onClickPhotoResult(index: Int) { - onClickPhoto(resultList[index], index) - } when (state) { SearchState.NO_INDEX -> UnReadyText() @@ -63,7 +60,7 @@ fun SearchResultGrid( Box(padding) { PhotoResultRecommend( photo = resultList[0], - onItemClick = { onClickPhotoResult(0) }, + onItemClick = { onClickPhoto(resultList[0], 0) }, ) } } @@ -79,7 +76,7 @@ fun SearchResultGrid( resultList[index + 1], onItemClick = { Log.e("SearchResultGrid", "click: $index") - onClickPhotoResult(index + 1) + onClickPhoto(resultList[index+1], index+1) }, ) }