diff --git a/app/build.gradle b/app/build.gradle index d00357b..117bb49 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" } \ No newline at end of file diff --git a/app/src/main/java/com/amrdeveloper/linkhub/data/ImportExportFileType.kt b/app/src/main/java/com/amrdeveloper/linkhub/data/ImportExportFileType.kt new file mode 100644 index 0000000..39fe126 --- /dev/null +++ b/app/src/main/java/com/amrdeveloper/linkhub/data/ImportExportFileType.kt @@ -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") +} \ No newline at end of file diff --git a/app/src/main/java/com/amrdeveloper/linkhub/data/parser/HtmlImportExportFileParser.kt b/app/src/main/java/com/amrdeveloper/linkhub/data/parser/HtmlImportExportFileParser.kt new file mode 100644 index 0000000..5b08de1 --- /dev/null +++ b/app/src/main/java/com/amrdeveloper/linkhub/data/parser/HtmlImportExportFileParser.kt @@ -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 { + 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() + 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() + 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 { + val foldersResult = folderRepository.getFolderList() + if (foldersResult.isSuccess) { + val folders = foldersResult.getOrDefault(listOf()) + val htmlString = buildString { + appendLine("\n") + appendLine("\n") + appendLine("Bookmarks\n") + appendLine("

Bookmarks

\n") + appendLine("

\n") + + folders.forEach { + val linksGetResult = linkRepository.getSortedFolderLinkList(it.id) + appendLine("

${it.name}

\n") + if (linksGetResult.isSuccess) { + val links = linksGetResult.getOrDefault(listOf()) + appendLine("

\n") + links.forEach { link -> + appendLine("

${link.title}\n") + } + appendLine("

\n") + } + + } + + val bookmarks: List = linkRepository.getSortedFolderLinkList(-1).getOrDefault( + listOf() + ) + if (bookmarks.isNotEmpty()) { + bookmarks.forEach { link -> + appendLine("

${link.title}\n") + } + } + appendLine("

\n") + + } + return Result.success(htmlString) + } + return Result.failure(Throwable()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/amrdeveloper/linkhub/data/parser/ImportExportFileParser.kt b/app/src/main/java/com/amrdeveloper/linkhub/data/parser/ImportExportFileParser.kt new file mode 100644 index 0000000..6af4dfc --- /dev/null +++ b/app/src/main/java/com/amrdeveloper/linkhub/data/parser/ImportExportFileParser.kt @@ -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 + suspend fun exportData( + folderRepository: FolderRepository, + linkRepository: LinkRepository, + uiPreferences: UiPreferences + ): Result + fun getFileType(): ImportExportFileType +} \ No newline at end of file diff --git a/app/src/main/java/com/amrdeveloper/linkhub/data/parser/JsonImportExportFileParser.kt b/app/src/main/java/com/amrdeveloper/linkhub/data/parser/JsonImportExportFileParser.kt new file mode 100644 index 0000000..2fca7ce --- /dev/null +++ b/app/src/main/java/com/amrdeveloper/linkhub/data/parser/JsonImportExportFileParser.kt @@ -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 { + 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{ + 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()); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/amrdeveloper/linkhub/data/source/FolderDataSource.kt b/app/src/main/java/com/amrdeveloper/linkhub/data/source/FolderDataSource.kt index 8468cb3..a2beba9 100644 --- a/app/src/main/java/com/amrdeveloper/linkhub/data/source/FolderDataSource.kt +++ b/app/src/main/java/com/amrdeveloper/linkhub/data/source/FolderDataSource.kt @@ -10,6 +10,8 @@ interface FolderDataSource { suspend fun getFolderById(id: Int): Result + suspend fun getFolderByName(name: String): Result + suspend fun getFolderList(): Result> suspend fun getSortedFolderList(): Result> diff --git a/app/src/main/java/com/amrdeveloper/linkhub/data/source/FolderRepository.kt b/app/src/main/java/com/amrdeveloper/linkhub/data/source/FolderRepository.kt index 58f0bb6..4193223 100644 --- a/app/src/main/java/com/amrdeveloper/linkhub/data/source/FolderRepository.kt +++ b/app/src/main/java/com/amrdeveloper/linkhub/data/source/FolderRepository.kt @@ -15,6 +15,9 @@ class FolderRepository(private val dataSource: FolderDataSource) { suspend fun getFolderById(folderId : Int) : Result { return dataSource.getFolderById(folderId) } + suspend fun getFolderByName(name : String) : Result { + return dataSource.getFolderByName(name) + } suspend fun getFolderList(): Result> { return dataSource.getFolderList() diff --git a/app/src/main/java/com/amrdeveloper/linkhub/data/source/local/FolderDao.kt b/app/src/main/java/com/amrdeveloper/linkhub/data/source/local/FolderDao.kt index 2ba8401..3281b33 100644 --- a/app/src/main/java/com/amrdeveloper/linkhub/data/source/local/FolderDao.kt +++ b/app/src/main/java/com/amrdeveloper/linkhub/data/source/local/FolderDao.kt @@ -11,6 +11,9 @@ interface FolderDao : BaseDao { @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 diff --git a/app/src/main/java/com/amrdeveloper/linkhub/data/source/local/FolderLocalDataSource.kt b/app/src/main/java/com/amrdeveloper/linkhub/data/source/local/FolderLocalDataSource.kt index 8261597..94b73a5 100644 --- a/app/src/main/java/com/amrdeveloper/linkhub/data/source/local/FolderLocalDataSource.kt +++ b/app/src/main/java/com/amrdeveloper/linkhub/data/source/local/FolderLocalDataSource.kt @@ -35,6 +35,14 @@ class FolderLocalDataSource internal constructor( } } + override suspend fun getFolderByName(name : String): Result = withContext(ioDispatcher) { + return@withContext try { + Result.success(folderDao.getFolderByName(name)) + } catch (e: Exception) { + Result.failure(e) + } + } + override suspend fun getFolderList(): Result> = withContext(ioDispatcher) { return@withContext try { Result.success(folderDao.getFolderList()) diff --git a/app/src/main/java/com/amrdeveloper/linkhub/ui/importexport/ImportExportFragment.kt b/app/src/main/java/com/amrdeveloper/linkhub/ui/importexport/ImportExportFragment.kt index 6812a6a..c632e5e 100644 --- a/app/src/main/java/com/amrdeveloper/linkhub/ui/importexport/ImportExportFragment.kt +++ b/app/src/main/java/com/amrdeveloper/linkhub/ui/importexport/ImportExportFragment.kt @@ -2,6 +2,8 @@ package com.amrdeveloper.linkhub.ui.importexport import android.Manifest import android.app.Activity +import android.app.AlertDialog +import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.os.Build @@ -14,6 +16,7 @@ import androidx.core.content.ContextCompat.checkSelfPermission import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.amrdeveloper.linkhub.R +import com.amrdeveloper.linkhub.data.ImportExportFileType import com.amrdeveloper.linkhub.databinding.FragmentImportExportBinding import com.amrdeveloper.linkhub.util.getFileName import com.amrdeveloper.linkhub.util.getFileText @@ -23,6 +26,7 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class ImportExportFragment : Fragment() { + private var importExportFileType: ImportExportFileType? = null private var _binding: FragmentImportExportBinding? = null private val binding get() = _binding!! @@ -42,12 +46,31 @@ class ImportExportFragment : Fragment() { private fun setupListeners() { binding.importAction.setOnClickListener { - importDataFile() + launchFileTypePickerDialog(requireContext()) { fileType -> + importExportFileType = fileType + importDataFile(fileType) + } } binding.exportAction.setOnClickListener { - exportDataFile() + launchFileTypePickerDialog(requireContext()) { fileType -> + importExportFileType = fileType + exportDataFile(fileType) + } + } + } + + private fun launchFileTypePickerDialog(context: Context, onFileTypeSelected: (ImportExportFileType)->Unit) { + val fileTypes = ImportExportFileType.entries.map { it.fileTypeName } + val builder = AlertDialog.Builder(context) + builder.setTitle(context.getString(R.string.import_export_choose_file_type)) + builder.setItems(fileTypes.toTypedArray()) { dialog, which -> + val selectedFileType = ImportExportFileType.entries[which] + onFileTypeSelected(selectedFileType) + dialog.dismiss() } + val dialog = builder.create() + dialog.show() } private fun setupObservers() { @@ -56,38 +79,39 @@ class ImportExportFragment : Fragment() { } } - private fun importDataFile() { - importFileFromDeviceWithPermission() + private fun importDataFile(fileType: ImportExportFileType) { + importFileFromDeviceWithPermission(fileType) } - private fun exportDataFile() { - exportFileFromDeviceWthPermission() + private fun exportDataFile(fileType: ImportExportFileType) { + exportFileFromDeviceWthPermission(fileType) } - private fun importFileFromDeviceWithPermission() { + private fun importFileFromDeviceWithPermission(fileType: ImportExportFileType) { // From Android 33 no need for READ_EXTERNAL_STORAGE permission for non media files if (Build.VERSION_CODES.TIRAMISU > Build.VERSION.SDK_INT && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val readPermissionState = checkSelfPermission(requireContext(), Manifest.permission.READ_EXTERNAL_STORAGE) - if (readPermissionState == PackageManager.PERMISSION_GRANTED) launchFileChooserIntent() + if (readPermissionState == PackageManager.PERMISSION_GRANTED) launchFileChooserIntent(fileType) else permissionLauncher.launch(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)) } else { - launchFileChooserIntent() + launchFileChooserIntent(fileType) } } - private fun exportFileFromDeviceWthPermission() { + private fun exportFileFromDeviceWthPermission(fileType: ImportExportFileType) { if (Build.VERSION_CODES.R > Build.VERSION.SDK_INT && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val readPermissionState = checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) - if (readPermissionState == PackageManager.PERMISSION_GRANTED) importExportViewModel.exportDataFile(requireContext()) + if (readPermissionState == PackageManager.PERMISSION_GRANTED) + importExportViewModel.exportDataFile(requireContext(), fileType) else permissionLauncher.launch(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) } else { - importExportViewModel.exportDataFile(requireContext()) + importExportViewModel.exportDataFile(requireContext(), fileType) } } - private fun launchFileChooserIntent() { + private fun launchFileChooserIntent(fileType: ImportExportFileType) { val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.type = "application/json" + intent.type = fileType.mimeType intent.addCategory(Intent.CATEGORY_OPENABLE) val chooserIntent = Intent.createChooser(intent, "Select a File to import") loadFileActivityResult.launch(chooserIntent) @@ -95,11 +119,13 @@ class ImportExportFragment : Fragment() { private val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> - if(result[Manifest.permission.WRITE_EXTERNAL_STORAGE] == true) { - importExportViewModel.exportDataFile(requireContext()) - } - else if(result[Manifest.permission.READ_EXTERNAL_STORAGE] == true) { - launchFileChooserIntent() + importExportFileType?.let { fileType-> + if(result[Manifest.permission.WRITE_EXTERNAL_STORAGE] == true) { + importExportViewModel.exportDataFile(requireContext(), fileType) + } + else if(result[Manifest.permission.READ_EXTERNAL_STORAGE] == true) { + launchFileChooserIntent(fileType) + } } } @@ -110,15 +136,17 @@ class ImportExportFragment : Fragment() { if(resultIntent != null) { val fileUri = resultIntent.data if(fileUri != null) { - val contentResolver = requireActivity().contentResolver - val fileName = contentResolver.getFileName(fileUri) - val extension = fileName.substring(fileName.lastIndexOf('.') + 1) - if (extension != "json") { - activity?.showSnackBar(R.string.message_invalid_extension) - return@registerForActivityResult + importExportFileType?.let { fileType-> + val contentResolver = requireActivity().contentResolver + val fileName = contentResolver.getFileName(fileUri) + val extension = fileName.substring(fileName.lastIndexOf('.') + 1) + if (".$extension" != fileType.extension) { + activity?.showSnackBar(R.string.message_invalid_extension) + return@registerForActivityResult + } + val fileContent = contentResolver.getFileText(fileUri) + importExportViewModel.importDataFile(fileContent, fileType) } - val fileContent = contentResolver.getFileText(fileUri) - importExportViewModel.importDataFile(fileContent) } } } diff --git a/app/src/main/java/com/amrdeveloper/linkhub/ui/importexport/ImportExportViewModel.kt b/app/src/main/java/com/amrdeveloper/linkhub/ui/importexport/ImportExportViewModel.kt index d8e449a..823f528 100644 --- a/app/src/main/java/com/amrdeveloper/linkhub/ui/importexport/ImportExportViewModel.kt +++ b/app/src/main/java/com/amrdeveloper/linkhub/ui/importexport/ImportExportViewModel.kt @@ -9,13 +9,11 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.amrdeveloper.linkhub.R -import com.amrdeveloper.linkhub.data.DataPackage -import com.amrdeveloper.linkhub.data.FolderColor +import com.amrdeveloper.linkhub.data.ImportExportFileType +import com.amrdeveloper.linkhub.data.parser.ImportExportFileParser 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 import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.io.File @@ -31,75 +29,66 @@ class ImportExportViewModel @Inject constructor ( private val _stateMessages = MutableLiveData() val stateMessages = _stateMessages - fun importDataFile(data : String) { + fun importDataFile(data : String, fileType: ImportExportFileType) { viewModelScope.launch { - 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) - - // Import show click count flag if it available - val lastShowClickCountConfig = uiPreferences.isClickCounterEnabled() - uiPreferences.setEnableClickCounter(dataPackage.showClickCounter ?: lastShowClickCountConfig) - - // Import enabling auto saving - val lastAutoSavingEnabled = uiPreferences.isAutoSavingEnabled() - uiPreferences.setEnableAutoSave(dataPackage.enableAutoSaving ?: lastAutoSavingEnabled) - - // Import theme flag if it available - val lastThemeOption = uiPreferences.getThemeType() - uiPreferences.setThemeType(dataPackage.theme ?: lastThemeOption) - + val parser = ImportExportFileParser.getDataParser(fileType) + val dataPackageResult = parser.importData(data, folderRepository, linkRepository) + //dataPackage is null in case of non-configuration import + if(dataPackageResult.isSuccess) { + dataPackageResult.getOrNull()?.let { + // Import show click count flag if it available + val lastShowClickCountConfig = uiPreferences.isClickCounterEnabled() + uiPreferences.setEnableClickCounter( + it.showClickCounter ?: lastShowClickCountConfig + ) + // Import enabling auto saving + val lastAutoSavingEnabled = uiPreferences.isAutoSavingEnabled() + uiPreferences.setEnableAutoSave( + it.enableAutoSaving ?: lastAutoSavingEnabled + ) + // Import theme flag if it available + val lastThemeOption = uiPreferences.getThemeType() + uiPreferences.setThemeType(it.theme ?: lastThemeOption) + } _stateMessages.value = R.string.message_data_imported - } catch (e : JsonSyntaxException) { + } else { _stateMessages.value = R.string.message_invalid_data_format } } } - fun exportDataFile(context: Context) { + fun exportDataFile(context: Context, fileType: ImportExportFileType) { viewModelScope.launch { - 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) - val jsonDataPackage = Gson().toJson(dataPackage) - createdExportedFile(context, jsonDataPackage) + val parser = ImportExportFileParser.getDataParser(fileType) + val exportResult = parser.exportData(folderRepository, linkRepository, uiPreferences) + if (exportResult.isSuccess) { + createdExportedFile(context, fileType, exportResult.getOrDefault("")) } else { _stateMessages.value = R.string.message_invalid_export } } } - private fun createdExportedFile(context: Context, data : String) { - val fileName = System.currentTimeMillis().toString() + ".json" - + private fun createdExportedFile(context: Context, fileType: ImportExportFileType, data : String) { + val fileName = System.currentTimeMillis().toString() + fileType.extension if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val resolver = context.contentResolver val values = ContentValues() values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) - values.put(MediaStore.MediaColumns.MIME_TYPE, "application/json") + values.put( + MediaStore.MediaColumns.MIME_TYPE, + fileType.mimeType + ) values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) val outputStream = uri?.let { resolver.openOutputStream(it) } outputStream?.write(data.toByteArray()) } else { - val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + val downloadDir = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) val dataFile = File(downloadDir, fileName) dataFile.writeText(data) } - _stateMessages.value = R.string.message_data_exported } diff --git a/app/src/main/java/com/amrdeveloper/linkhub/ui/setting/SettingFragment.kt b/app/src/main/java/com/amrdeveloper/linkhub/ui/setting/SettingFragment.kt index ccba653..82abc01 100644 --- a/app/src/main/java/com/amrdeveloper/linkhub/ui/setting/SettingFragment.kt +++ b/app/src/main/java/com/amrdeveloper/linkhub/ui/setting/SettingFragment.kt @@ -11,7 +11,14 @@ import com.amrdeveloper.linkhub.BuildConfig import com.amrdeveloper.linkhub.R import com.amrdeveloper.linkhub.data.Theme import com.amrdeveloper.linkhub.databinding.FragmentSettingBinding -import com.amrdeveloper.linkhub.util.* +import com.amrdeveloper.linkhub.util.PLAY_STORE_URL +import com.amrdeveloper.linkhub.util.REPOSITORY_CONTRIBUTORS_URL +import com.amrdeveloper.linkhub.util.REPOSITORY_ISSUES_URL +import com.amrdeveloper.linkhub.util.REPOSITORY_SPONSORSHIP_URL +import com.amrdeveloper.linkhub.util.REPOSITORY_URL +import com.amrdeveloper.linkhub.util.UiPreferences +import com.amrdeveloper.linkhub.util.openLinkIntent +import com.amrdeveloper.linkhub.util.shareTextIntent import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 813c1a7..b3815ec 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -165,7 +165,12 @@ android:id="@+id/importExportFragment" android:name="com.amrdeveloper.linkhub.ui.importexport.ImportExportFragment" android:label="fragment_import_export" - tools:layout="@layout/fragment_import_export" /> + tools:layout="@layout/fragment_import_export" > + + Next Folder Color Previous Folder Color Import and Export + Choose a file type Password Enable password diff --git a/build.gradle b/build.gradle index 7bdf30d..a2e087c 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,7 @@ ext { archCoreTestVersion = '2.1.0' coroutinesTestVersion = '1.2.1' espressoVersion = '3.5.1' + jsoupVersion = '1.13.1' } tasks.register('clean', Delete) {