Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/src/main/java/com/yogeshpaliyal/deepr/DeeprApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.yogeshpaliyal.deepr.backup.ExportRepository
import com.yogeshpaliyal.deepr.backup.ExportRepositoryImpl
import com.yogeshpaliyal.deepr.backup.ImportRepository
import com.yogeshpaliyal.deepr.backup.ImportRepositoryImpl
import com.yogeshpaliyal.deepr.backup.importer.TextFileImporter
import com.yogeshpaliyal.deepr.data.HtmlParser
import com.yogeshpaliyal.deepr.data.NetworkRepository
import com.yogeshpaliyal.deepr.preference.AppPreferenceDataStore
Expand Down Expand Up @@ -72,6 +73,8 @@ class DeeprApplication : Application() {

single<ImportRepository> { ImportRepositoryImpl(androidContext(), get()) }

single { TextFileImporter(androidContext(), get()) }

single<SyncRepository> { SyncRepositoryImpl(androidContext(), get(), get()) }

single<AutoBackupWorker> { AutoBackupWorker(androidContext(), get(), get()) }
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import com.yogeshpaliyal.deepr.ui.screens.AboutUs
import com.yogeshpaliyal.deepr.ui.screens.AboutUsScreen
import com.yogeshpaliyal.deepr.ui.screens.BackupScreen
import com.yogeshpaliyal.deepr.ui.screens.BackupScreenContent
import com.yogeshpaliyal.deepr.ui.screens.ImportPreviewScreen
import com.yogeshpaliyal.deepr.ui.screens.ImportPreviewScreenContent
import com.yogeshpaliyal.deepr.ui.screens.LocalNetworkServer
import com.yogeshpaliyal.deepr.ui.screens.LocalNetworkServerScreen
import com.yogeshpaliyal.deepr.ui.screens.RestoreScreen
Expand Down Expand Up @@ -181,6 +183,11 @@ fun Dashboard(
RestoreScreenContent(backStack)
}

is ImportPreviewScreen ->
NavEntry(key) {
ImportPreviewScreenContent(key.uri, backStack)
}

else -> NavEntry(Unit) { Text("Unknown route") }
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.yogeshpaliyal.deepr.backup.importer.BookmarkImporter
import com.yogeshpaliyal.deepr.backup.importer.ChromeBookmarkImporter
import com.yogeshpaliyal.deepr.backup.importer.CsvBookmarkImporter
import com.yogeshpaliyal.deepr.backup.importer.MozillaBookmarkImporter
import com.yogeshpaliyal.deepr.backup.importer.TextFileImporter
import com.yogeshpaliyal.deepr.util.RequestResult

class ImportRepositoryImpl(
Expand All @@ -16,6 +17,7 @@ class ImportRepositoryImpl(
private val csvImporter = CsvBookmarkImporter(context, deeprQueries)
private val chromeImporter = ChromeBookmarkImporter(context, deeprQueries)
private val mozillaImporter = MozillaBookmarkImporter(context, deeprQueries)
private val textFileImporter = TextFileImporter(context, deeprQueries)

override suspend fun importFromCsv(uri: Uri): RequestResult<ImportResult> = csvImporter.import(uri)

Expand All @@ -24,6 +26,7 @@ class ImportRepositoryImpl(
csvImporter,
chromeImporter,
mozillaImporter,
textFileImporter,
)

override suspend fun importBookmarks(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.yogeshpaliyal.deepr.backup

import androidx.annotation.Keep

/**
* Represents a link candidate for import with selection state.
*/
@Keep
data class LinkImportCandidate(
val link: String,
val isValid: Boolean,
val isDuplicate: Boolean = false,
val isSelected: Boolean = true, // By default all are selected
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package com.yogeshpaliyal.deepr.backup.importer

import android.content.Context
import android.net.Uri
import com.yogeshpaliyal.deepr.DeeprQueries
import com.yogeshpaliyal.deepr.backup.ImportResult
import com.yogeshpaliyal.deepr.backup.LinkImportCandidate
import com.yogeshpaliyal.deepr.util.RequestResult
import com.yogeshpaliyal.deepr.util.isValidDeeplink
import java.io.IOException

/**
* Importer for text files containing links.
* Supports both comma-separated and newline-separated links.
*/
class TextFileImporter(
private val context: Context,
private val deeprQueries: DeeprQueries,
) : BookmarkImporter {
override suspend fun import(uri: Uri): RequestResult<ImportResult> {
var updatedCount = 0
var skippedCount = 0

try {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
inputStream.reader().use { reader ->
val content = reader.readText()
val links = extractLinks(content)

links.forEach { link ->
val trimmedLink = link.trim()
if (trimmedLink.isNotBlank() && isValidDeeplink(trimmedLink)) {
val existing = deeprQueries.getDeeprByLink(trimmedLink).executeAsOneOrNull()
if (existing == null) {
updatedCount++
deeprQueries.insertDeepr(
link = trimmedLink,
name = "",
openedCount = 0L,
notes = "",
thumbnail = "",
)
} else {
skippedCount++
}
} else {
skippedCount++
}
}
}
}

return RequestResult.Success(ImportResult(updatedCount, skippedCount))
} catch (e: IOException) {
return RequestResult.Error("Error reading file: ${e.message}")
} catch (e: Exception) {
return RequestResult.Error("An unexpected error occurred: ${e.message}")
}
}

/**
* Parse the text file and extract link candidates for preview.
*/
suspend fun parseForPreview(uri: Uri): RequestResult<List<LinkImportCandidate>> {
try {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
inputStream.reader().use { reader ->
val content = reader.readText()
val links = extractLinks(content)

val candidates = links.mapNotNull { link ->
val trimmedLink = link.trim()
if (trimmedLink.isNotBlank()) {
val isValid = isValidDeeplink(trimmedLink)
val isDuplicate = if (isValid) {
deeprQueries.getDeeprByLink(trimmedLink).executeAsOneOrNull() != null
} else {
false
}
LinkImportCandidate(
link = trimmedLink,
isValid = isValid,
isDuplicate = isDuplicate,
isSelected = isValid && !isDuplicate, // Only select valid, non-duplicate links by default
)
} else {
null
}
}

return RequestResult.Success(candidates)
}
}

return RequestResult.Error("Unable to open file")
} catch (e: IOException) {
return RequestResult.Error("Error reading file: ${e.message}")
} catch (e: Exception) {
return RequestResult.Error("An unexpected error occurred: ${e.message}")
}
}

/**
* Import selected links from the preview.
*/
suspend fun importSelected(links: List<String>): RequestResult<ImportResult> {
var updatedCount = 0
var skippedCount = 0

try {
links.forEach { link ->
val trimmedLink = link.trim()
if (trimmedLink.isNotBlank() && isValidDeeplink(trimmedLink)) {
val existing = deeprQueries.getDeeprByLink(trimmedLink).executeAsOneOrNull()
if (existing == null) {
updatedCount++
deeprQueries.insertDeepr(
link = trimmedLink,
name = "",
openedCount = 0L,
notes = "",
thumbnail = "",
)
} else {
skippedCount++
}
} else {
skippedCount++
}
}

return RequestResult.Success(ImportResult(updatedCount, skippedCount))
} catch (e: Exception) {
return RequestResult.Error("An unexpected error occurred: ${e.message}")
}
}

override fun getDisplayName(): String = "Text File"

override fun getSupportedMimeTypes(): Array<String> =
arrayOf(
"text/plain",
"text/*",
)

/**
* Extract links from text content.
* Supports both comma-separated and newline-separated links.
*/
private fun extractLinks(content: String): List<String> {
val links = mutableListOf<String>()

// Split by commas to check if comma-separated format is used
val commaSeparated = content.split(",").map { it.trim() }

// Check if comma separation produces valid results
// We consider it comma-separated if:
// 1. We have multiple items after split
// 2. Most items look like they could be links (contain :// or .)
val isCommaSeparated =
commaSeparated.size > 1 &&
commaSeparated.count { it.contains("://") || (it.contains(".") && !it.contains("\n")) } >= commaSeparated.size * 0.5

if (isCommaSeparated) {
links.addAll(commaSeparated)
} else {
// Otherwise, split by newlines
links.addAll(content.split("\n").map { it.trim() })
}

return links.filter { it.isNotBlank() }
}
}
Loading
Loading