Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added import/export to html functionality #54

Merged
merged 6 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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/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,25 @@
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 {
object ImportExportFileParserFactory {
fun getInstance(fileType: ImportExportFileType): ImportExportFileParser {
return when (fileType) {
ImportExportFileType.JSON -> JsonImportExportFileParser()
ImportExportFileType.HTML -> HtmlImportExportFileParser()
}
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can create the factory pattern but as a single function in the view model for now

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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add new line before method declaration

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