diff --git a/.gitignore b/.gitignore index a2c219b..79adf2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Project exclude paths .gradle/ .idea/ +.DS_Store /build /local.properties /satodime-android \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 2a04edc..50b4482 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -99,4 +99,11 @@ dependencies { androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version" + + //Material 3 compose + implementation 'androidx.compose.material3:material3:1.2.1' + + //Compose coil image + implementation "io.coil-kt:coil-gif:2.0.0-rc02" + implementation 'io.coil-kt:coil-compose:2.0.0-rc02' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 127b842..a84c01c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + (null) + } + + Box( + modifier = Modifier.fillMaxSize() + ) { + Navigation() + if (prevStatus == ConnectionChecker.InternetStatus.Lost && status == ConnectionChecker.InternetStatus.Available) { + SatoToast( + title = R.string.networkConnected, + text = R.string.networkConnectedMessage, + icon = R.drawable.contactless_24px, + iconColor = SatoGreen + ) + } + if (status == ConnectionChecker.InternetStatus.Lost || + status == ConnectionChecker.InternetStatus.Unavailable + ) { + SatoToast( + title = R.string.networkError, + text = R.string.networkErrorMessage, + icon = R.drawable.error_cross + ) + prevStatus = status + } + } } } } diff --git a/app/src/main/java/org/satochip/satodimeapp/ui/VaultsView.kt b/app/src/main/java/org/satochip/satodimeapp/ui/VaultsView.kt index 527092f..45b219f 100644 --- a/app/src/main/java/org/satochip/satodimeapp/ui/VaultsView.kt +++ b/app/src/main/java/org/satochip/satodimeapp/ui/VaultsView.kt @@ -49,6 +49,7 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Loop import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -92,10 +93,14 @@ import org.satochip.satodimeapp.ui.components.NfcDialog import org.satochip.satodimeapp.ui.components.NftDialog import org.satochip.satodimeapp.ui.components.RedGradientBackground import org.satochip.satodimeapp.ui.components.VaultCard +import org.satochip.satodimeapp.ui.components.shared.SatoButton +import org.satochip.satodimeapp.ui.components.vaults.VaultDrawerScreen +import org.satochip.satodimeapp.ui.components.vaults.VaultsBottomDrawer import org.satochip.satodimeapp.ui.theme.DarkRed import org.satochip.satodimeapp.ui.theme.LightBlue import org.satochip.satodimeapp.ui.theme.LightDarkBlue import org.satochip.satodimeapp.ui.theme.LightGreen +import org.satochip.satodimeapp.ui.theme.SatoGreen import org.satochip.satodimeapp.ui.theme.SatodimeTheme import org.satochip.satodimeapp.util.SatodimeScreen import org.satochip.satodimeapp.util.formatBalance @@ -112,6 +117,11 @@ fun VaultsView(navController: NavController, sharedViewModel: SharedViewModel) { val showNfcDialog = remember{ mutableStateOf(false) } // for NfcDialog val showNoCardScannedDialog = remember { mutableStateOf(false)}// for NoCardScannedDialog + // NfcDialog + if (showNfcDialog.value){ + NfcDialog(openDialogCustom = showNfcDialog, resultCodeLive = sharedViewModel.resultCodeLive, isConnected = sharedViewModel.isCardConnected) + } + // val showOwnershipDialog = remember{ mutableStateOf(true) } // for OwnershipDialog // val showAuthenticityDialog = remember{ mutableStateOf(true) } // for AuthenticityDialog @@ -373,12 +383,6 @@ fun VaultsView(navController: NavController, sharedViewModel: SharedViewModel) { uriHandler.openUri("https://satochip.io/satodime-ownership-explained/") },) } - - // NfcDialog - if (showNfcDialog.value){ - NfcDialog(openDialogCustom = showNfcDialog, resultCodeLive = sharedViewModel.resultCodeLive, isConnected = sharedViewModel.isCardConnected) - } - } /// LIST VIEW @@ -770,7 +774,7 @@ fun VaultsViewTokenRow(asset: Asset) { contentDescription = "link to explorer", modifier = Modifier .width(30.dp) - .clickable{ + .clickable { uriHandler.openUri(asset.explorerLink ?: "") }, tint = MaterialTheme.colors.secondary, //Color.LightGray, @@ -847,7 +851,7 @@ fun VaultsViewNftRow(asset: Asset) { contentDescription = "link to NFT explorer", modifier = Modifier .width(30.dp) - .clickable{ + .clickable { uriHandler.openUri(asset.nftExplorerLink ?: asset.explorerLink ?: "") }, //.requiredWidth(30.dp) diff --git a/app/src/main/java/org/satochip/satodimeapp/ui/components/NfcDialog.kt b/app/src/main/java/org/satochip/satodimeapp/ui/components/NfcDialog.kt index a2b6949..582a050 100644 --- a/app/src/main/java/org/satochip/satodimeapp/ui/components/NfcDialog.kt +++ b/app/src/main/java/org/satochip/satodimeapp/ui/components/NfcDialog.kt @@ -30,6 +30,8 @@ import kotlinx.coroutines.delay import org.satochip.satodimeapp.R import org.satochip.satodimeapp.data.NfcResultCode import org.satochip.satodimeapp.services.SatoLog +import org.satochip.satodimeapp.ui.components.vaults.VaultDrawerScreen +import org.satochip.satodimeapp.ui.components.vaults.VaultsBottomDrawer import org.satochip.satodimeapp.ui.theme.LightBlue import org.satochip.satodimeapp.ui.theme.Orange import kotlin.time.Duration.Companion.seconds @@ -39,24 +41,46 @@ private const val TAG = "NfcDialog" @Composable fun NfcDialog(openDialogCustom: MutableState, resultCodeLive: NfcResultCode, isConnected: Boolean) { - Dialog(onDismissRequest = { - openDialogCustom.value = false - // todo: disable NFC scan? - }) { - NfcDialogUI(openDialogCustom = openDialogCustom, resultCodeLive = resultCodeLive, isConnected = isConnected) + VaultsBottomDrawer( + showSheet = openDialogCustom + ) { - // auto-close alertDialog when action is done LaunchedEffect(resultCodeLive) { SatoLog.d(TAG, "LaunchedEffect START ${resultCodeLive}") while (resultCodeLive == NfcResultCode.Busy || resultCodeLive == NfcResultCode.None) { SatoLog.d(TAG, "LaunchedEffect in while delay 2s ${resultCodeLive}") delay(2.seconds) } - SatoLog.d(TAG, "LaunchedEffect after while delay 3s ${resultCodeLive}") - delay(3.seconds) - openDialogCustom.value = false + SatoLog.d(TAG, "LaunchedEffect after while delay ${resultCodeLive}") } + if (resultCodeLive == NfcResultCode.Busy){ + VaultDrawerScreen( + closeSheet = { + openDialogCustom.value = !openDialogCustom.value + }, + closeDrawerButton = true, + title = R.string.readyToScan, + image = R.drawable.phone_icon, + message = R.string.nfcHoldSatodime + ) + } else { + if (resultCodeLive == NfcResultCode.Ok){ + VaultDrawerScreen( + closeSheet = { + openDialogCustom.value = !openDialogCustom.value + }, + closeDrawerButton = true, + title = R.string.readyToScan, + image = R.drawable.icon_check_gif, + message = R.string.nfcHoldSatodime + ) + LaunchedEffect(Unit) { + delay(0.5.seconds) + openDialogCustom.value = false + } + } + } } } diff --git a/app/src/main/java/org/satochip/satodimeapp/ui/components/shared/BottomSheet.kt b/app/src/main/java/org/satochip/satodimeapp/ui/components/shared/BottomSheet.kt new file mode 100644 index 0000000..a148c91 --- /dev/null +++ b/app/src/main/java/org/satochip/satodimeapp/ui/components/shared/BottomSheet.kt @@ -0,0 +1,44 @@ +package org.satochip.satodimeapp.ui.components.shared + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BottomSheet( + showSheet: MutableState, + modifier: Modifier, + content: @Composable () -> Unit, +) { + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = false + ) + if (!showSheet.value) { + return + } else { + BottomSheet( + modifier = modifier, + showSheet = showSheet, + ) { + content() + } + ModalBottomSheet( + modifier = modifier, + containerColor = Color.White, + sheetState = sheetState, + onDismissRequest = { + showSheet.value = !showSheet.value + }, + shape = RoundedCornerShape(10.dp) + ) { + content() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/satochip/satodimeapp/ui/components/shared/GifImage.kt b/app/src/main/java/org/satochip/satodimeapp/ui/components/shared/GifImage.kt new file mode 100644 index 0000000..d4cf02a --- /dev/null +++ b/app/src/main/java/org/satochip/satodimeapp/ui/components/shared/GifImage.kt @@ -0,0 +1,46 @@ +package org.satochip.satodimeapp.ui.components.shared + +import android.os.Build +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import coil.ImageLoader +import coil.compose.rememberAsyncImagePainter +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import coil.request.ImageRequest +import coil.size.Size + +@Composable +fun GifImage( + modifier: Modifier, + colorFilter: ColorFilter? = null, + @DrawableRes image: Int +) { + val context = LocalContext.current + val imageLoader = ImageLoader.Builder(context) + .components { + if (Build.VERSION.SDK_INT >= 28) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + } + .build() + val painter = rememberAsyncImagePainter( + ImageRequest.Builder(context).data(data = image) + .apply(block = { + size(Size.ORIGINAL) + }).build(), imageLoader = imageLoader + ) + Image( + modifier = modifier, + painter = painter, + contentDescription = null, + colorFilter = colorFilter + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/satochip/satodimeapp/ui/components/shared/SatoButton.kt b/app/src/main/java/org/satochip/satodimeapp/ui/components/shared/SatoButton.kt index 7145e66..b9d76a8 100644 --- a/app/src/main/java/org/satochip/satodimeapp/ui/components/shared/SatoButton.kt +++ b/app/src/main/java/org/satochip/satodimeapp/ui/components/shared/SatoButton.kt @@ -15,19 +15,21 @@ import androidx.compose.ui.unit.dp @Composable fun SatoButton( + modifier: Modifier = Modifier, onClick: () -> Unit, text: Int, buttonColor: Color = MaterialTheme.colors.primary, - textColor: Color = MaterialTheme.colors.secondary + textColor: Color = MaterialTheme.colors.secondary, + shape: RoundedCornerShape = RoundedCornerShape(50) ) { Button( onClick = { onClick() }, - modifier = Modifier + modifier = modifier .padding(10.dp) .height(40.dp), - shape = RoundedCornerShape(50), + shape = shape, colors = ButtonDefaults.buttonColors( backgroundColor = buttonColor, ) diff --git a/app/src/main/java/org/satochip/satodimeapp/ui/components/shared/SatoToast.kt b/app/src/main/java/org/satochip/satodimeapp/ui/components/shared/SatoToast.kt new file mode 100644 index 0000000..317ed71 --- /dev/null +++ b/app/src/main/java/org/satochip/satodimeapp/ui/components/shared/SatoToast.kt @@ -0,0 +1,102 @@ +package org.satochip.satodimeapp.ui.components.shared + +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.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay +import org.satochip.satodimeapp.R +import org.satochip.satodimeapp.ui.theme.SatoToastGrey +import org.satochip.satodimeapp.ui.theme.SatoWarningOrange +import kotlin.time.Duration.Companion.seconds + +@Composable +fun SatoToast( + title: Int, + text: Int, + icon: Int, + iconColor: Color = SatoWarningOrange +) { + var showToast by remember { + mutableStateOf(true) + } + if (showToast) { + Box( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .background( + color = SatoToastGrey, + shape = RoundedCornerShape(50) + ), + ) { + Row( + modifier = Modifier.padding(8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + GifImage( + modifier = Modifier + .padding(12.dp) + .size(36.dp), + colorFilter = ColorFilter.tint(iconColor), + image = icon + ) + Column( + modifier = Modifier + .fillMaxWidth() + ) { + Text( + text = stringResource(id = title), + style = TextStyle( + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + ) + Text( + text = stringResource(id = text), + style = TextStyle( + fontSize = 16.sp, + ) + ) + } + } + } + LaunchedEffect(showToast) { + delay(5.seconds) + showToast = !showToast + } + } +} + +@Preview +@Composable +private fun SatoToastPreview() { + SatoToast( + title = R.string.networkError, + text = R.string.networkErrorMessage, + icon = R.drawable.error_cross + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/satochip/satodimeapp/ui/components/vaults/VaultDrawerScreen.kt b/app/src/main/java/org/satochip/satodimeapp/ui/components/vaults/VaultDrawerScreen.kt new file mode 100644 index 0000000..4d934e6 --- /dev/null +++ b/app/src/main/java/org/satochip/satodimeapp/ui/components/vaults/VaultDrawerScreen.kt @@ -0,0 +1,80 @@ +package org.satochip.satodimeapp.ui.components.vaults + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.satochip.satodimeapp.R +import org.satochip.satodimeapp.ui.components.shared.GifImage +import org.satochip.satodimeapp.ui.components.shared.SatoButton +import org.satochip.satodimeapp.ui.theme.SatoLightGrey + +@Composable +fun VaultDrawerScreen( + closeSheet: () -> Unit, + closeDrawerButton: Boolean = false, + title: Int? = null, + message: Int, + image: Int? = null +) { + Column( + modifier = Modifier + .height(350.dp) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + title?.let { + Text( + text = stringResource(it), + style = TextStyle( + color = SatoLightGrey, + fontSize = 26.sp + ) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + image?.let { + GifImage( + modifier = Modifier.size(125.dp), + image = image + ) + + Spacer(modifier = Modifier.height(16.dp)) + } + + Text( + text = stringResource(message), + style = TextStyle( + color = Color.Black, + fontSize = 16.sp + ) + ) + Spacer(modifier = Modifier.height(16.dp)) + if (closeDrawerButton) { + SatoButton( + modifier = Modifier.fillMaxWidth(), + onClick = closeSheet, + text = R.string.cancel, + buttonColor = SatoLightGrey, + textColor = Color.Black, + shape = RoundedCornerShape(20) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/satochip/satodimeapp/ui/components/vaults/VaultsBottomDrawer.kt b/app/src/main/java/org/satochip/satodimeapp/ui/components/vaults/VaultsBottomDrawer.kt new file mode 100644 index 0000000..864189a --- /dev/null +++ b/app/src/main/java/org/satochip/satodimeapp/ui/components/vaults/VaultsBottomDrawer.kt @@ -0,0 +1,26 @@ +package org.satochip.satodimeapp.ui.components.vaults + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.satochip.satodimeapp.ui.components.shared.BottomSheet + +@Composable +fun VaultsBottomDrawer( + showSheet: MutableState, + content: @Composable () -> Unit, + ) { + BottomSheet(showSheet = showSheet, modifier = Modifier) { + Box( + modifier = Modifier.padding(16.dp), + contentAlignment = Alignment.Center + ) { + content() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/satochip/satodimeapp/ui/theme/Color.kt b/app/src/main/java/org/satochip/satodimeapp/ui/theme/Color.kt index f9a407f..816f41a 100644 --- a/app/src/main/java/org/satochip/satodimeapp/ui/theme/Color.kt +++ b/app/src/main/java/org/satochip/satodimeapp/ui/theme/Color.kt @@ -17,4 +17,8 @@ val LightBlue = Color(0xFF65BBE0) val InfoDialogBackgroundColor = Color(0xFF27273C) val MoreInfoButtonColor = Color(0xFF525684) -val SatoGreen = Color(0xFF25B59A) \ No newline at end of file +val SatoGreen = Color(0xFF25B59A) +val SatoLightGrey = Color(0xFFC6C6C6) +val SatoGrey = Color(0xFF8F8E94) +val SatoToastGrey = Color(0xFFCCCCD4) +val SatoWarningOrange = Color(0xFFFE8B00) \ No newline at end of file diff --git a/app/src/main/java/org/satochip/satodimeapp/util/internetconnection/ConnectionChecker.kt b/app/src/main/java/org/satochip/satodimeapp/util/internetconnection/ConnectionChecker.kt new file mode 100644 index 0000000..a01c2c7 --- /dev/null +++ b/app/src/main/java/org/satochip/satodimeapp/util/internetconnection/ConnectionChecker.kt @@ -0,0 +1,60 @@ +package org.satochip.satodimeapp.util.internetconnection + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch + +class ConnectionChecker( + context: Context +) { + + enum class InternetStatus { + Available, Unavailable, Losing, Lost + } + + private val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + fun observe(): Flow { + return callbackFlow { + val callback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + launch { + send(InternetStatus.Available) + } + } + + override fun onLosing(network: Network, maxMsToLive: Int) { + super.onLosing(network, maxMsToLive) + launch { + send(InternetStatus.Losing) + } + } + + override fun onLost(network: Network) { + super.onLost(network) + launch { + send(InternetStatus.Lost) + } + } + + override fun onUnavailable() { + super.onUnavailable() + launch { + send(InternetStatus.Unavailable) + } + } + } + connectivityManager.registerDefaultNetworkCallback(callback) + awaitClose { + connectivityManager.unregisterNetworkCallback(callback) + } + }.distinctUntilChanged() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/error_cross.xml b/app/src/main/res/drawable/error_cross.xml new file mode 100644 index 0000000..f8ca0c6 --- /dev/null +++ b/app/src/main/res/drawable/error_cross.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/icon_check.png b/app/src/main/res/drawable/icon_check.png new file mode 100644 index 0000000..ce29757 Binary files /dev/null and b/app/src/main/res/drawable/icon_check.png differ diff --git a/app/src/main/res/drawable/icon_check_gif.gif b/app/src/main/res/drawable/icon_check_gif.gif new file mode 100644 index 0000000..b99d526 Binary files /dev/null and b/app/src/main/res/drawable/icon_check_gif.gif differ diff --git a/app/src/main/res/drawable/phone_icon.gif b/app/src/main/res/drawable/phone_icon.gif new file mode 100644 index 0000000..304febd Binary files /dev/null and b/app/src/main/res/drawable/phone_icon.gif differ diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3a89e93..bc72c97 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -145,4 +145,8 @@ Certificat sub-CA Certificat root CA Copié dans le presse-papier + Erreur réseau + Échec de la récupération des données.\nVeuillez vérifier votre connexion réseau. + Connecté + Vous êtes de nouveau en ligne. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 99825a0..9036e1b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -152,4 +152,8 @@ To facilitate maintenance, keep same key name whenever possible Sub-CA certificate Root CA certificate Explore + Network Error + Data recovery failed.\nPlease check your network connection. + Connected + You are back online. \ No newline at end of file