Skip to content

Commit

Permalink
Merge pull request #54 from GalievBulat/html-import-export
Browse files Browse the repository at this point in the history
Added import/export to html functionality
  • Loading branch information
AmrDeveloper authored Mar 27, 2024
2 parents 19536fb + f882898 commit ae2c071
Show file tree
Hide file tree
Showing 15 changed files with 329 additions and 76 deletions.
3 changes: 3 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,7 @@ dependencies {
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.archCoreTestVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$rootProject.coroutinesTestVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$rootProject.espressoVersion"

// Html parsing lib
implementation "org.jsoup:jsoup:$rootProject.jsoupVersion"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.amrdeveloper.linkhub.data

enum class ImportExportFileType(val mimeType: String, val extension: String, val fileTypeName: String) {
JSON( "application/json",".json", "Json"),
HTML("text/html", ".html", "HTML")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.amrdeveloper.linkhub.data.parser

import com.amrdeveloper.linkhub.data.DataPackage
import com.amrdeveloper.linkhub.data.Folder
import com.amrdeveloper.linkhub.data.FolderColor
import com.amrdeveloper.linkhub.data.ImportExportFileType
import com.amrdeveloper.linkhub.data.Link
import com.amrdeveloper.linkhub.data.source.FolderRepository
import com.amrdeveloper.linkhub.data.source.LinkRepository
import com.amrdeveloper.linkhub.util.UiPreferences
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.select.Selector

class HtmlImportExportFileParser: ImportExportFileParser {
override fun getFileType(): ImportExportFileType = ImportExportFileType.HTML

override suspend fun importData(data: String, folderRepository: FolderRepository, linkRepository: LinkRepository): Result<DataPackage?> {
try {
val doc: Document = Jsoup.parse(data)
val folders = doc.select("h3")
for (i in folders.indices) {
val folder = Folder(folders[i].text()).apply {
folderColor = FolderColor.BLUE
}
//the default case - no folder id
var folderId = -1
val getFolderRes = folderRepository.getFolderByName(folder.name)
//a case when a folder does already exists
if (getFolderRes.isSuccess && getFolderRes.getOrNull() != null) {
val existingFolder = getFolderRes.getOrNull()!!
folderId = existingFolder.id
} else {
//a case when a folder does not exists
val addFolderRes = folderRepository.insertFolder(folder)
if (addFolderRes.isSuccess) {
folderId = addFolderRes.getOrDefault(-1).toInt()
}
}

val folderLinks = mutableListOf<Link>()
val nextDL = folders[i].nextElementSibling()
val links = nextDL.select("a")
for (j in links.indices) {
val link = links[j]
val title = link.text()
val url = link.attr("href")
//subtitle = title = link name
folderLinks.add(Link(title, title, url, folderId = folderId))
}
linkRepository.insertLinks(folderLinks)
}
// If there are bookmarks without a folder, add then individually
val rootDL = doc.select("dl").firstOrNull()
val folderLinks = mutableListOf<Link>()
if (rootDL != null) {
val individualBookmarks = rootDL.select("> dt > a")
if (individualBookmarks.isNotEmpty()) {
for (bookmarkElement in individualBookmarks) {
val title = bookmarkElement.text()
val url = bookmarkElement.attr("href")
folderLinks.add(Link(title, title, url))
}
}
}
linkRepository.insertLinks(folderLinks)
return Result.success(null)
} catch (e: Selector.SelectorParseException){
return Result.failure(e)
}
}
override suspend fun exportData(
folderRepository: FolderRepository,
linkRepository: LinkRepository,
uiPreferences: UiPreferences
): Result<String> {
val foldersResult = folderRepository.getFolderList()
if (foldersResult.isSuccess) {
val folders = foldersResult.getOrDefault(listOf())
val htmlString = buildString {
appendLine("<!DOCTYPE NETSCAPE-Bookmark-file-1>\n")
appendLine("<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n")
appendLine("<TITLE>Bookmarks</TITLE>\n")
appendLine("<H1>Bookmarks</H1>\n")
appendLine("<DL><p>\n")

folders.forEach {
val linksGetResult = linkRepository.getSortedFolderLinkList(it.id)
appendLine("<DT><H3>${it.name}</H3>\n")
if (linksGetResult.isSuccess) {
val links = linksGetResult.getOrDefault(listOf())
appendLine("<DL><p>\n")
links.forEach { link ->
appendLine("<DT><A HREF=\"${link.url}\">${link.title}</A>\n")
}
appendLine("</DL><p>\n")
}

}

val bookmarks: List<Link> = linkRepository.getSortedFolderLinkList(-1).getOrDefault(
listOf()
)
if (bookmarks.isNotEmpty()) {
bookmarks.forEach { link ->
appendLine("<DT><A HREF=\"${link.url}\">${link.title}</A>\n")
}
}
appendLine("</DL><p>\n")

}
return Result.success(htmlString)
}
return Result.failure(Throwable())
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.amrdeveloper.linkhub.data.parser

import com.amrdeveloper.linkhub.data.DataPackage
import com.amrdeveloper.linkhub.data.ImportExportFileType
import com.amrdeveloper.linkhub.data.source.FolderRepository
import com.amrdeveloper.linkhub.data.source.LinkRepository
import com.amrdeveloper.linkhub.util.UiPreferences

interface ImportExportFileParser {

companion object {
fun getDataParser(fileType: ImportExportFileType): ImportExportFileParser {
return when (fileType) {
ImportExportFileType.JSON -> JsonImportExportFileParser()
ImportExportFileType.HTML -> HtmlImportExportFileParser()
}
}
}
suspend fun importData(data: String, folderRepository: FolderRepository, linkRepository: LinkRepository): Result<DataPackage?>
suspend fun exportData(
folderRepository: FolderRepository,
linkRepository: LinkRepository,
uiPreferences: UiPreferences
): Result<String>
fun getFileType(): ImportExportFileType
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.amrdeveloper.linkhub.data.parser

import com.amrdeveloper.linkhub.data.DataPackage
import com.amrdeveloper.linkhub.data.FolderColor
import com.amrdeveloper.linkhub.data.ImportExportFileType
import com.amrdeveloper.linkhub.data.source.FolderRepository
import com.amrdeveloper.linkhub.data.source.LinkRepository
import com.amrdeveloper.linkhub.util.UiPreferences
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException

class JsonImportExportFileParser: ImportExportFileParser {
override fun getFileType(): ImportExportFileType = ImportExportFileType.JSON
override suspend fun importData(
data: String,
folderRepository: FolderRepository,
linkRepository: LinkRepository
): Result<DataPackage?> {
try{
val dataPackage = Gson().fromJson(data, DataPackage::class.java)

val folders = dataPackage.folders
// This code should be removed after found why it not serialized on some devices (see Issue #23)
// folderColor field is declared as non nullable type but in this case GSON will break the null safty feature
folders.forEach { if (it.folderColor == null) it.folderColor = FolderColor.BLUE }
folderRepository.insertFolders(folders)

linkRepository.insertLinks(dataPackage.links)
return Result.success(dataPackage)
} catch (e : JsonSyntaxException) {
return Result.failure(e)
}
}
override suspend fun exportData(
folderRepository: FolderRepository,
linkRepository: LinkRepository,
uiPreferences: UiPreferences
): Result<String>{
val foldersResult = folderRepository.getFolderList()
val linksResult = linkRepository.getLinkList()
if (foldersResult.isSuccess && linksResult.isSuccess) {
val folders = foldersResult.getOrDefault(listOf())
val links = linksResult.getOrDefault(listOf())
val showClickCounter = uiPreferences.isClickCounterEnabled()
val autoSaving = uiPreferences.isAutoSavingEnabled()
val lastTheme = uiPreferences.getThemeType()
val dataPackage = DataPackage(folders, links, showClickCounter, autoSaving, lastTheme)
return Result.success(Gson().toJson(dataPackage))
} else {
return Result.failure(Throwable());
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ interface FolderDataSource {

suspend fun getFolderById(id: Int): Result<Folder>

suspend fun getFolderByName(name: String): Result<Folder>

suspend fun getFolderList(): Result<List<Folder>>

suspend fun getSortedFolderList(): Result<List<Folder>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class FolderRepository(private val dataSource: FolderDataSource) {
suspend fun getFolderById(folderId : Int) : Result<Folder> {
return dataSource.getFolderById(folderId)
}
suspend fun getFolderByName(name : String) : Result<Folder> {
return dataSource.getFolderByName(name)
}

suspend fun getFolderList(): Result<List<Folder>> {
return dataSource.getFolderList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ interface FolderDao : BaseDao<Folder> {
@Query("SELECT * FROM folder WHERE id = :id LIMIT 1")
suspend fun getFolderById(id : Int) : Folder

@Query("SELECT * FROM folder WHERE name = :name LIMIT 1")
suspend fun getFolderByName(name : String) : Folder

@Query("SELECT * FROM folder")
suspend fun getFolderList(): List<Folder>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ class FolderLocalDataSource internal constructor(
}
}

override suspend fun getFolderByName(name : String): Result<Folder> = withContext(ioDispatcher) {
return@withContext try {
Result.success(folderDao.getFolderByName(name))
} catch (e: Exception) {
Result.failure(e)
}
}

override suspend fun getFolderList(): Result<List<Folder>> = withContext(ioDispatcher) {
return@withContext try {
Result.success(folderDao.getFolderList())
Expand Down
Loading

0 comments on commit ae2c071

Please sign in to comment.