diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0ff1234..579c2e0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,7 +17,7 @@ android { minSdk = 28 targetSdk = 36 versionCode = 1 - versionName = "Alpha 0.6.3" + versionName = "Alpha 0.6.3.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -56,9 +56,6 @@ protobuf { artifact = "com.google.protobuf:protoc:4.27.0" } - // Generates the java Protobuf-lite code for the Protobufs in this project. See - // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation - // for more information. generateProtoTasks { all().forEach { task -> task.builtins { diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt b/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt index c8f358b..2a12005 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/MainActivity.kt @@ -1,7 +1,6 @@ package com.github.nacabaro.vbhelper import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -68,13 +67,10 @@ class MainActivity : ComponentActivity() { ) } } - - Log.i("MainActivity", "Activity onCreated") } override fun onPause() { super.onPause() - Log.i("MainActivity", "onPause") for(activityListener in onActivityLifecycleListeners) { activityListener.value.onPause() } @@ -82,7 +78,6 @@ class MainActivity : ComponentActivity() { override fun onResume() { super.onResume() - Log.i("MainActivity", "Resume") for(activityListener in onActivityLifecycleListeners) { activityListener.value.onResume() } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/logger/AppLogger.kt b/app/src/main/java/com/github/nacabaro/vbhelper/logger/AppLogger.kt new file mode 100644 index 0000000..c345f6d --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/logger/AppLogger.kt @@ -0,0 +1,25 @@ +import android.util.Log + +object AppLogger { + private val logs = StringBuilder() + + fun d(tag: String, message: String) { + Log.d(tag, message) + append("DEBUG", tag, message) + } + + fun e(tag: String, message: String, tr: Throwable? = null) { + Log.e(tag, message, tr) + append("ERROR", tag, message + (tr?.stackTraceToString() ?: "")) + } + + private fun append(level: String, tag: String, message: String) { + logs.append("${System.currentTimeMillis()} [$level][$tag] $message\n") + } + + fun getLogs(): String = logs.toString() + + fun clear() { + logs.clear() + } +} diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt index 7d207b2..7c28e29 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/AppNavigation.kt @@ -36,6 +36,7 @@ import com.github.nacabaro.vbhelper.screens.adventureScreen.AdventureScreenContr import com.github.nacabaro.vbhelper.screens.cardScreen.CardAdventureScreen import com.github.nacabaro.vbhelper.screens.cardScreen.CardScreenControllerImpl import com.github.nacabaro.vbhelper.screens.settingsScreen.CreditsScreen +import com.github.nacabaro.vbhelper.screens.settingsScreen.LoggerScreen import com.github.nacabaro.vbhelper.screens.spriteViewer.SpriteViewerControllerImpl import com.github.nacabaro.vbhelper.screens.storageScreen.StorageScreenControllerImpl import com.github.nacabaro.vbhelper.source.StorageRepository @@ -182,6 +183,11 @@ fun AppNavigation( ) } } + composable(NavigationItems.Logger.route) { + LoggerScreen( + navController = navController + ) + } } } } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/NavigationItems.kt b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/NavigationItems.kt index 77b9b91..6b37486 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/navigation/NavigationItems.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/navigation/NavigationItems.kt @@ -98,4 +98,10 @@ sealed class NavigationItems( R.drawable.baseline_data_24, R.string.nav_credits ) + + object Logger : NavigationItems( + "Logger", + R.drawable.baseline_bug_report, + R.string.nav_logs + ) } \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardsScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardsScreen.kt index 7908c4c..5c37f75 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardsScreen.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/cardScreen/CardsScreen.kt @@ -1,6 +1,5 @@ package com.github.nacabaro.vbhelper.screens.cardScreen -import android.util.Log import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -98,8 +97,8 @@ fun CardsScreen( selectedCard.value = null }, onRename = { newName -> - Log.d("CardsScreen", "New name: $newName") - Log.d("CardsScreen", "Card: ${selectedCard.value.toString()}") + AppLogger.d("CardsScreen", "New name: $newName") + AppLogger.d("CardsScreen", "Card: ${selectedCard.value.toString()}") cardScreenController .renameCard( cardId = selectedCard.value!!.cardId, diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt index 487ab2d..b00dbfb 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/ScanScreenControllerImpl.kt @@ -6,7 +6,6 @@ import android.nfc.Tag import android.nfc.tech.NfcA import android.os.Bundle import android.provider.Settings -import android.util.Log import android.widget.Toast import androidx.activity.ComponentActivity import androidx.lifecycle.lifecycleScope @@ -127,18 +126,18 @@ class ScanScreenControllerImpl( handleTag(secrets) { tagCommunicator -> try { if (nfcCharacter is VBNfcCharacter) { - Log.d("SendCharacter", "VBNfcCharacter") + AppLogger.d("SendCharacter", "VBNfcCharacter") val castNfcCharacter: VBNfcCharacter = nfcCharacter tagCommunicator.sendCharacter(castNfcCharacter) } else if (nfcCharacter is BENfcCharacter) { - Log.d("SendCharacter", "BENfcCharacter") + AppLogger.d("SendCharacter", "BENfcCharacter") val castNfcCharacter: BENfcCharacter = nfcCharacter tagCommunicator.sendCharacter(castNfcCharacter) } onComplete.invoke() componentActivity.getString(R.string.scan_sent_character_success) } catch (e: Throwable) { - Log.e("TAG", e.stackTraceToString()) + AppLogger.e("TAG", e.stackTraceToString()) componentActivity.getString(R.string.scan_error_generic) } } @@ -178,7 +177,7 @@ class ScanScreenControllerImpl( ) val character = nfcGenerator.characterToNfc(characterId) - Log.d("CharacterType", character.toString()) + AppLogger.d("CharacterType", character.toString()) return character } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/ToNfcConverter.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/ToNfcConverter.kt index 143fef8..b4a8f0b 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/ToNfcConverter.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/scanScreen/converters/ToNfcConverter.kt @@ -2,7 +2,6 @@ package com.github.nacabaro.vbhelper.screens.scanScreen.converters import android.icu.util.Calendar import android.icu.util.TimeZone -import android.util.Log import androidx.activity.ComponentActivity import com.github.cfogrady.vbnfc.be.BENfcCharacter import com.github.cfogrady.vbnfc.be.FirmwareVersion @@ -161,7 +160,7 @@ class ToNfcConverter( } nfcVitalsHistory.map { - Log.d("NFC", it.toString()) + AppLogger.d("NFC", it.toString()) } return nfcVitalsHistory @@ -247,7 +246,7 @@ class ToNfcConverter( val calendar = android.icu.util.GregorianCalendar(TimeZone.getTimeZone("UTC")) calendar.time = date - Log.d( + AppLogger.d( "TransformationHistory", "Year: ${calendar.get(Calendar.YEAR)}, " + "Month: ${calendar.get(Calendar.MONTH) + 1}, " + diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/LoggerScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/LoggerScreen.kt new file mode 100644 index 0000000..b8fa3cf --- /dev/null +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/LoggerScreen.kt @@ -0,0 +1,44 @@ +package com.github.nacabaro.vbhelper.screens.settingsScreen + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavController +import com.github.nacabaro.vbhelper.R +import com.github.nacabaro.vbhelper.components.TopBanner + +@Composable +fun LoggerScreen( + navController: NavController +) { + Scaffold ( + topBar = { + TopBanner( + text = stringResource(R.string.logs_title), + onBackClick = { + navController.popBackStack() + } + ) + }, + modifier = Modifier + .fillMaxSize() + ) { contentPadding -> + Column ( + modifier = Modifier + .padding(top = contentPadding.calculateTopPadding()) + ) { + Text( + text = AppLogger.getLogs(), + modifier = Modifier + .verticalScroll(rememberScrollState()) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreen.kt index 6a71765..70cc6e8 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreen.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/SettingsScreen.kt @@ -1,7 +1,6 @@ package com.github.nacabaro.vbhelper.screens.settingsScreen import android.content.Intent -import android.net.Uri import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -24,6 +23,7 @@ import androidx.navigation.NavController import com.github.nacabaro.vbhelper.components.TopBanner import com.github.nacabaro.vbhelper.navigation.NavigationItems import com.github.nacabaro.vbhelper.R +import androidx.core.net.toUri @Composable @@ -80,10 +80,16 @@ fun SettingsScreen( ) { val browserIntent = Intent( Intent.ACTION_VIEW, - Uri.parse("https://github.com/nacabaro/vbhelper/") + "https://github.com/nacabaro/vbhelper/".toUri() ) context.startActivity(browserIntent) } + SettingsEntry( + title = stringResource(R.string.settings_logs_title), + description = stringResource(R.string.settings_logs_desc) + ) { + navController.navigate(NavigationItems.Logger.route) + } SettingsSection(title = stringResource(R.string.settings_section_data)) SettingsEntry( diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/CardImportController.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/CardImportController.kt index 1626405..c6527a4 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/CardImportController.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/CardImportController.kt @@ -1,6 +1,5 @@ package com.github.nacabaro.vbhelper.screens.settingsScreen.controllers -import android.util.Log import com.github.cfogrady.vb.dim.card.BemCard import com.github.cfogrady.vb.dim.card.DimCard import com.github.cfogrady.vb.dim.card.DimReader @@ -154,7 +153,7 @@ class CardImportController( cardId: Long, card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *> ) { - Log.d("importAdventureMissions", "Importing adventure missions") + AppLogger.d("importAdventureMissions", "Importing adventure missions") if (card is BemCard) { card.adventureLevels.levels.forEach { database @@ -190,13 +189,13 @@ class CardImportController( cardId: Long, card: com.github.cfogrady.vb.dim.card.Card<*, *, *, *, *, *> ) { - Log.d("importCardFusions", "Importing card fusions") + AppLogger.d("importCardFusions", "Importing card fusions") if (card is DimCard) { card .attributeFusions .entries .forEach { - Log.d("importCardFusions", "Importing fusion: ${it.attribute1Fusion}") + AppLogger.d("importCardFusions", "Importing fusion: ${it.attribute1Fusion}") if (it.attribute1Fusion != 65535 && it.characterIndex != 65535) { database .cardFusionsDao() diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/DatabaseManagementController.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/DatabaseManagementController.kt index f3fb931..0cb3962 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/DatabaseManagementController.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/settingsScreen/controllers/DatabaseManagementController.kt @@ -2,7 +2,6 @@ package com.github.nacabaro.vbhelper.screens.settingsScreen.controllers import android.net.Uri import android.provider.OpenableColumns -import android.util.Log import android.widget.Toast import androidx.activity.ComponentActivity import androidx.lifecycle.lifecycleScope @@ -41,7 +40,7 @@ class DatabaseManagementController( componentActivity.finishAffinity() } } catch (e: Exception) { - Log.e("ScanScreenController", "Error exporting database $e") + AppLogger.e("ScanScreenController", "Error exporting database $e") componentActivity.runOnUiThread { Toast.makeText(componentActivity, "Error exporting database: ${e.message}", Toast.LENGTH_LONG).show() } @@ -84,7 +83,7 @@ class DatabaseManagementController( componentActivity.finishAffinity() } } catch (e: Exception) { - Log.e("ScanScreenController", "Error importing database $e") + AppLogger.e("ScanScreenController", "Error importing database $e") componentActivity.runOnUiThread { Toast.makeText(componentActivity, "Error importing database: ${e.message}", Toast.LENGTH_LONG).show() } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/spriteViewer/SpriteViewer.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/spriteViewer/SpriteViewer.kt index e111ad7..a684fd9 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/spriteViewer/SpriteViewer.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/spriteViewer/SpriteViewer.kt @@ -1,7 +1,6 @@ package com.github.nacabaro.vbhelper.screens.spriteViewer import android.graphics.Bitmap -import android.util.Log import androidx.compose.foundation.Image import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -13,7 +12,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.unit.dp @@ -27,8 +25,6 @@ fun SpriteViewer( ) { val spriteList = remember { mutableStateListOf() } - Log.d("SpriteViewer", "spriteList: $spriteList") - LaunchedEffect(spriteViewerController) { val sprites = spriteViewerController.getAllSprites() val bitmapData = spriteViewerController.convertToBitmap(sprites) diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/spriteViewer/SpriteViewerControllerImpl.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/spriteViewer/SpriteViewerControllerImpl.kt index 1413f6b..3937f04 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/spriteViewer/SpriteViewerControllerImpl.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/spriteViewer/SpriteViewerControllerImpl.kt @@ -1,13 +1,11 @@ package com.github.nacabaro.vbhelper.screens.spriteViewer import android.graphics.Bitmap -import android.util.Log import androidx.activity.ComponentActivity -import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.asImageBitmap import com.github.nacabaro.vbhelper.di.VBHelper import com.github.nacabaro.vbhelper.domain.characters.Sprite import java.nio.ByteBuffer +import androidx.core.graphics.createBitmap class SpriteViewerControllerImpl( private val context: ComponentActivity @@ -24,41 +22,40 @@ class SpriteViewerControllerImpl( val bitmapList = mutableListOf() for (sprite in sprites) { - Log.d("SpriteViewer", "sprite: $sprite") - bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { + bitmapList.add(createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteIdle1)) }) - bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { + bitmapList.add(createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteIdle2)) }) - bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { + bitmapList.add(createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteWalk1)) }) - bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { + bitmapList.add(createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteWalk2)) }) - bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { + bitmapList.add(createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteRun1)) }) - bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { + bitmapList.add(createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteRun2)) }) - bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { + bitmapList.add(createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteTrain1)) }) - bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { + bitmapList.add(createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteTrain2)) }) - bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { + bitmapList.add(createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteHappy)) }) - bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { + bitmapList.add(createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteSleep)) }) - bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { + bitmapList.add(createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteAttack)) }) - bitmapList.add(Bitmap.createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { + bitmapList.add(createBitmap(sprite.width, sprite.height, Bitmap.Config.RGB_565).apply { copyPixelsFromBuffer(ByteBuffer.wrap(sprite.spriteDodge)) }) } diff --git a/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageScreen.kt b/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageScreen.kt index e33e6dd..f5f82fd 100644 --- a/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageScreen.kt +++ b/app/src/main/java/com/github/nacabaro/vbhelper/screens/storageScreen/StorageScreen.kt @@ -1,6 +1,5 @@ package com.github.nacabaro.vbhelper.screens.storageScreen -import android.util.Log import android.widget.Toast import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.scrollable @@ -149,7 +148,7 @@ fun StorageScreen( .deleteCharacter( characterId = selectedCharacter!!, onCompletion = { - Log.d("StorageScreen", "Character deleted") + AppLogger.d("StorageScreen", "Character deleted") } ) selectedCharacter = null diff --git a/app/src/main/res/drawable/baseline_bug_report.xml b/app/src/main/res/drawable/baseline_bug_report.xml new file mode 100644 index 0000000..3884bc3 --- /dev/null +++ b/app/src/main/res/drawable/baseline_bug_report.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b5c51fd..b0656f7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -225,5 +225,9 @@ Are you sure you want to delete this special mission with this progress? Dismiss Remove + Logs + Log viewer + Check logs generated within the app + Application logs \ No newline at end of file