From ca3f6e26f48732698d9f347ad24b7a63c7ad852e Mon Sep 17 00:00:00 2001 From: Xwite <1797350009@qq.com> Date: Tue, 7 Mar 2023 13:51:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96webFile=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 7 + .../io/legado/app/data/entities/Server.kt | 6 +- .../io/legado/app/help/book/BookExtensions.kt | 38 ++--- .../io/legado/app/lib/webdav/Authorization.kt | 2 +- .../io/legado/app/model/webBook/BookInfo.kt | 3 +- .../association/ImportOnLineBookFileDialog.kt | 131 ------------------ .../ImportOnLineBookFileViewModel.kt | 78 ----------- .../app/ui/book/info/BookInfoActivity.kt | 71 +++++++--- .../app/ui/book/info/BookInfoViewModel.kt | 79 +++++++++-- 9 files changed, 150 insertions(+), 265 deletions(-) delete mode 100644 app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileDialog.kt delete mode 100644 app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileViewModel.kt diff --git a/app/build.gradle b/app/build.gradle index 2965ba11c..626d5c1e2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,6 +96,7 @@ android { } flavorDimensions.add("mode") + productFlavors { app { dimension "mode" @@ -107,6 +108,7 @@ android { manifestPlaceholders.put("APP_CHANNEL_VALUE", "google") } } + compileOptions { // Flag to enable support for the new language APIs coreLibraryDesugaringEnabled true @@ -115,6 +117,11 @@ android { targetCompatibility JavaVersion.VERSION_11 } + packagingOptions { + // fix found with path 'META-INF/INDEX.LIST' from inputs + exclude 'META-INF/INDEX.LIST' + } + sourceSets { // Adds exported schema location as test app assets. androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) diff --git a/app/src/main/java/io/legado/app/data/entities/Server.kt b/app/src/main/java/io/legado/app/data/entities/Server.kt index 4c47e135e..2faa6b2c9 100644 --- a/app/src/main/java/io/legado/app/data/entities/Server.kt +++ b/app/src/main/java/io/legado/app/data/entities/Server.kt @@ -49,9 +49,9 @@ data class Server( @Parcelize data class WebDavConfig( - var url: String? = null, - var username: String? = null, - var password: String? = null, + var url: String, + var username: String, + var password: String ) : Parcelable } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt index 65cdef262..94041e500 100644 --- a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt +++ b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt @@ -18,49 +18,39 @@ import java.util.concurrent.ConcurrentHashMap val Book.isAudio: Boolean - get() { - return type and BookType.audio > 0 - } + get() = isType(BookType.audio) val Book.isImage: Boolean - get() { - return type and BookType.image > 0 - } + get() = isType(BookType.image) val Book.isLocal: Boolean get() { if (type == 0) { return origin == BookType.localTag || origin.startsWith(BookType.webDavTag) } - return type and BookType.local > 0 + return isType(BookType.local) } val Book.isLocalTxt: Boolean - get() { - return isLocal && originName.endsWith(".txt", true) - } + get() = isLocal && originName.endsWith(".txt", true) val Book.isEpub: Boolean - get() { - return isLocal && originName.endsWith(".epub", true) - } + get() = isLocal && originName.endsWith(".epub", true) val Book.isUmd: Boolean - get() { - return isLocal && originName.endsWith(".umd", true) - } + get() = isLocal && originName.endsWith(".umd", true) + val Book.isPdf: Boolean - get() { - return isLocal && originName.endsWith(".pdf", true) - } + get() = isLocal && originName.endsWith(".pdf", true) val Book.isOnLineTxt: Boolean - get() { - return !isLocal && type and BookType.text > 0 - } + get() = !isLocal && isType(BookType.text) + +val Book.isWebFile: Boolean + get() = isType(BookType.webFile) val Book.isUpError: Boolean - get() = type and BookType.updateError > 0 + get() = isType(BookType.updateError) fun Book.contains(word: String?): Boolean { if (word.isNullOrEmpty()) { @@ -172,6 +162,8 @@ fun Book.clearType() { type = 0 } +fun Book.isType(@BookType.Type bookType: Int): Boolean = type and bookType > 0 + fun Book.upType() { if (type < 8) { type = when (type) { diff --git a/app/src/main/java/io/legado/app/lib/webdav/Authorization.kt b/app/src/main/java/io/legado/app/lib/webdav/Authorization.kt index 1b44d5f78..b89126ba3 100644 --- a/app/src/main/java/io/legado/app/lib/webdav/Authorization.kt +++ b/app/src/main/java/io/legado/app/lib/webdav/Authorization.kt @@ -24,7 +24,7 @@ data class Authorization( constructor(serverID: Long?): this("","") { serverID ?: throw NoStackTraceException("Unexpected server ID") - appDb.serverDao.get(serverID)?.getWebDavConfig().run { + appDb.serverDao.get(serverID)?.getWebDavConfig()?.run { data = Credentials.basic(username, password, charset) } ?: throw WebDavException("Unexpected WebDav Authorization") } diff --git a/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt b/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt index 1052d6a18..58b52f259 100644 --- a/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt +++ b/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt @@ -7,6 +7,7 @@ import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookSource import io.legado.app.exception.NoStackTraceException import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.isWebFile import io.legado.app.model.Debug import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.utils.DebugLog @@ -137,7 +138,7 @@ object BookInfo { Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") DebugLog.e("获取封面出错", e) } - if (book.type and BookType.webFile == 0) { + if (book.isWebFile) { coroutineContext.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取目录链接") book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true) diff --git a/app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileDialog.kt deleted file mode 100644 index 3b3562f64..000000000 --- a/app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileDialog.kt +++ /dev/null @@ -1,131 +0,0 @@ -package io.legado.app.ui.association - -import android.content.Context -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.viewModels -import androidx.recyclerview.widget.LinearLayoutManager -import io.legado.app.R -import io.legado.app.base.BaseDialogFragment -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.RecyclerAdapter -import io.legado.app.databinding.DialogRecyclerViewBinding -import io.legado.app.databinding.ItemBookFileImportBinding -import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.theme.primaryColor -import io.legado.app.ui.widget.dialog.WaitDialog -import io.legado.app.utils.openFileUri -import io.legado.app.utils.setLayout -import io.legado.app.utils.viewbindingdelegate.viewBinding -import io.legado.app.utils.visible - - -/** - * 导入在线书籍文件弹出窗口 - */ -class ImportOnLineBookFileDialog : BaseDialogFragment(R.layout.dialog_recycler_view) { - - - private val binding by viewBinding(DialogRecyclerViewBinding::bind) - private val viewModel by viewModels() - private val adapter by lazy { BookFileAdapter(requireContext()) } - - override fun onStart() { - super.onStart() - setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - } - - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - val bookUrl = arguments?.getString("bookUrl") - viewModel.initData(bookUrl) - binding.toolBar.setBackgroundColor(primaryColor) - binding.toolBar.setTitle(R.string.download_and_import_file) - binding.rotateLoading.visible() - binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) - binding.recyclerView.adapter = adapter - viewModel.errorLiveData.observe(this) { - binding.rotateLoading.gone() - binding.tvMsg.apply { - text = it - visible() - } - } - viewModel.successLiveData.observe(this) { - binding.rotateLoading.gone() - if (it > 0) { - adapter.setItems(viewModel.allBookFiles) - } - } - viewModel.savedFileUriData.observe(this) { - requireContext().openFileUri(it, "*/*") - } - } - - private fun importFileAndUpdate(url: String, fileName: String) { - val waitDialog = WaitDialog(requireContext()) - waitDialog.show() - viewModel.importOnLineBookFile(url, fileName) { - waitDialog.dismiss() - dismissAllowingStateLoss() - } - } - - private fun downloadFile(url: String, fileName: String) { - val waitDialog = WaitDialog(requireContext()) - waitDialog.show() - viewModel.downloadUrl(url, fileName) { - waitDialog.dismiss() - dismissAllowingStateLoss() - } -} - - inner class BookFileAdapter(context: Context) : - RecyclerAdapter -, ItemBookFileImportBinding>(context) { - - override fun getViewBinding(parent: ViewGroup): ItemBookFileImportBinding { - return ItemBookFileImportBinding.inflate(inflater, parent, false) - } - - override fun convert( - holder: ItemViewHolder, - binding: ItemBookFileImportBinding, - item: Triple, - payloads: MutableList - ) { - binding.apply { - cbFileName.text = item.second - } - } - - override fun registerListener( - holder: ItemViewHolder, - binding: ItemBookFileImportBinding - ) { - binding.apply { - cbFileName.setOnClickListener { - val selectFile = viewModel.allBookFiles[holder.layoutPosition] - if (selectFile.third) { - importFileAndUpdate(selectFile.first, selectFile.second) - } else { - alert( - title = getString(R.string.draw), - message = getString(R.string.file_not_supported, selectFile.second) - ) { - yesButton { - importFileAndUpdate(selectFile.first, selectFile.second) - } - neutralButton(R.string.open_fun) { - downloadFile(selectFile.first, selectFile.second) - } - noButton() - } - } - } - } - } - - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileViewModel.kt b/app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileViewModel.kt deleted file mode 100644 index 667c9042c..000000000 --- a/app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileViewModel.kt +++ /dev/null @@ -1,78 +0,0 @@ -package io.legado.app.ui.association - -import android.app.Application -import android.net.Uri -import androidx.lifecycle.MutableLiveData -import io.legado.app.base.BaseViewModel -import io.legado.app.constant.AppPattern -import io.legado.app.constant.EventBus -import io.legado.app.data.appDb -import io.legado.app.data.entities.BookSource -import io.legado.app.exception.NoStackTraceException -import io.legado.app.model.analyzeRule.AnalyzeRule -import io.legado.app.model.analyzeRule.AnalyzeUrl -import io.legado.app.model.localBook.LocalBook -import io.legado.app.utils.postEvent -import io.legado.app.utils.toastOnUi - -class ImportOnLineBookFileViewModel(app: Application) : BaseViewModel(app) { - - val allBookFiles = arrayListOf>() - val errorLiveData = MutableLiveData() - val successLiveData = MutableLiveData() - val savedFileUriData = MutableLiveData() - var bookSource: BookSource? = null - - fun initData(bookUrl: String?) { - execute { - bookUrl ?: throw NoStackTraceException("书籍详情页链接为空") - val book = appDb.searchBookDao.getSearchBook(bookUrl)?.toBook() - ?: throw NoStackTraceException("book is null") - bookSource = appDb.bookSourceDao.getBookSource(book.origin) - ?: throw NoStackTraceException("bookSource is null") - val ruleDownloadUrls = bookSource?.getBookInfoRule()?.downloadUrls - val content = AnalyzeUrl(bookUrl, source = bookSource).getStrResponse().body - val analyzeRule = AnalyzeRule(book, bookSource) - analyzeRule.setContent(content).setBaseUrl(bookUrl) - val fileName = "${book.name} 作者:${book.author}" - analyzeRule.getStringList(ruleDownloadUrls, isUrl = true)?.let { - it.forEach { url -> - val mFileName = "${fileName}.${LocalBook.parseFileSuffix(url)}" - val isSupportedFile = AppPattern.bookFileRegex.matches(mFileName) - allBookFiles.add(Triple(url, mFileName, isSupportedFile)) - } - } ?: throw NoStackTraceException("下载链接规则解析为空") - }.onSuccess { - successLiveData.postValue(allBookFiles.size) - }.onError { - errorLiveData.postValue(it.localizedMessage ?: "") - context.toastOnUi("获取书籍下载链接失败\n${it.localizedMessage}") - } - - } - - fun downloadUrl(url: String, fileName: String, success: () -> Unit) { - execute { - LocalBook.saveBookFile(url, fileName, bookSource).let { - savedFileUriData.postValue(it) - } - }.onSuccess { - success.invoke() - }.onError { - context.toastOnUi("下载书籍文件失败\n${it.localizedMessage}") - } - } - - fun importOnLineBookFile(url: String, fileName: String, success: () -> Unit) { - execute { - LocalBook.importFileOnLine(url, fileName, bookSource).let { - postEvent(EventBus.FILE_SOURCE_DOWNLOAD_DONE, it.bookUrl) - } - }.onSuccess { - success.invoke() - }.onError { - context.toastOnUi("下载书籍文件失败\n${it.localizedMessage}") - } - } - -} diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt index fd8b4e97a..c1067e463 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt @@ -21,10 +21,7 @@ import io.legado.app.databinding.ActivityBookInfoBinding import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.exception.NoStackTraceException import io.legado.app.help.AppWebDav -import io.legado.app.help.book.isAudio -import io.legado.app.help.book.isLocal -import io.legado.app.help.book.isLocalTxt -import io.legado.app.help.book.getRemoteUrl +import io.legado.app.help.book.* import io.legado.app.help.config.AppConfig import io.legado.app.lib.dialogs.alert import io.legado.app.lib.theme.backgroundColor @@ -33,7 +30,6 @@ import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.model.BookCover import io.legado.app.model.remote.RemoteBookWebDav import io.legado.app.ui.about.AppLogDialog -import io.legado.app.ui.association.ImportOnLineBookFileDialog import io.legado.app.ui.book.audio.AudioPlayActivity import io.legado.app.ui.book.changecover.ChangeCoverDialog import io.legado.app.ui.book.changesource.ChangeBookSourceDialog @@ -231,7 +227,6 @@ class BookInfoActivity : waitDialog.setText("上传中.....") waitDialog.show() try { - bookWebDav ?.upload(book) ?: throw NoStackTraceException("未配置webDav") @@ -253,6 +248,7 @@ class BookInfoActivity : tvOrigin.text = getString(R.string.origin_show, book.originName) tvLasted.text = getString(R.string.lasted_show, book.latestChapterTitle) tvIntro.text = book.getDisplayIntro() + tvToc.visible(!book.isWebFile) upTvBookshelf() val kinds = book.getKindList() if (kinds.isEmpty()) { @@ -278,11 +274,10 @@ class BookInfoActivity : binding.tvToc.text = getString(R.string.toc_s, getString(R.string.loading)) } chapterList.isNullOrEmpty() -> { - binding.tvToc.text = - if (viewModel.isImportBookOnLine) getString(R.string.click_read_button_load) else getString( - R.string.toc_s, - getString(R.string.error_load_toc) - ) + binding.tvToc.text = getString( + R.string.toc_s, + getString(R.string.error_load_toc) + ) } else -> { viewModel.bookData.value?.let { @@ -333,9 +328,9 @@ class BookInfoActivity : } tvRead.setOnClickListener { viewModel.bookData.value?.let { book -> - if (viewModel.isImportBookOnLine) { - showDialogFragment { - putString("bookUrl", book.bookUrl) + if (book.isWebFile) { + showWebFileDownloadAlert { + readBook(book) } } else { readBook(book) @@ -343,11 +338,21 @@ class BookInfoActivity : } ?: toastOnUi("Book is null") } tvShelf.setOnClickListener { - if (viewModel.inBookshelf) { - deleteBook() - } else { - viewModel.addToBookshelf { - upTvBookshelf() + viewModel.bookData.value?.let { book -> + if (viewModel.inBookshelf) { + deleteBook() + } else { + if (book.isWebFile) { + showWebFileDownloadAlert { + viewModel.addToBookshelf { + upTvBookshelf() + } + } + } else { + viewModel.addToBookshelf { + upTvBookshelf() + } + } } } } @@ -493,6 +498,34 @@ class BookInfoActivity : } } + private fun showWebFileDownloadAlert(onClick: (() -> Unit)?) { + viewModel.webFileData ?: return + alert(titleResource = R.string.download_and_import_file) { + val webFileData = viewModel.webFileData + webFileData?.let { + items(it) { _, webFile, _ -> + if (webFile.isSupported) { + viewModel.importOrDownloadWebFile(webFile) { + onClick?.invoke() + } + } else { + alert( + title = getString(R.string.draw), + message = getString(R.string.file_not_supported, webFile.name) + ) { + neutralButton(R.string.open_fun) { + viewModel.importOrDownloadWebFile(webFile) { uri -> + openFileUri(uri!!, "*/*") + } + } + noButton() + } + } + } + } + } + } + private fun readBook(book: Book) { if (!viewModel.inBookshelf) { viewModel.saveBook(book) { diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt index 1ef3978bb..a71da7e73 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.book.info +import android.net.Uri import android.app.Application import android.content.Intent import androidx.lifecycle.MutableLiveData @@ -7,6 +8,7 @@ import androidx.lifecycle.viewModelScope import io.legado.app.R import io.legado.app.base.BaseViewModel import io.legado.app.constant.AppLog +import io.legado.app.constant.AppPattern import io.legado.app.constant.BookSourceType import io.legado.app.constant.BookType import io.legado.app.constant.EventBus @@ -19,6 +21,8 @@ import io.legado.app.help.AppWebDav import io.legado.app.help.book.* import io.legado.app.help.coroutine.Coroutine import io.legado.app.lib.webdav.ObjectNotFoundException +import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.BookCover import io.legado.app.model.ReadBook import io.legado.app.model.localBook.LocalBook @@ -32,11 +36,10 @@ import kotlinx.coroutines.Dispatchers.IO class BookInfoViewModel(application: Application) : BaseViewModel(application) { val bookData = MutableLiveData() val chapterListData = MutableLiveData>() + val webFileData: MutableList? = null var inBookshelf = false var bookSource: BookSource? = null private var changeSourceCoroutine: Coroutine<*>? = null - val isImportBookOnLine: Boolean - get() = bookSource?.bookSourceType == BookSourceType.file fun initData(intent: Intent) { execute { @@ -82,8 +85,6 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { appDb.bookSourceDao.getBookSource(book.origin) if (book.tocUrl.isEmpty() && !book.isLocal) { loadBookInfo(book) - } else if (isImportBookOnLine) { - chapterListData.postValue(emptyList()) } else { val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl) if (chapterList.isNotEmpty()) { @@ -156,13 +157,14 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { inBookshelf = true } bookData.postValue(book) - if (isImportBookOnLine) { - appDb.searchBookDao.update(book.toSearchBook()) - } if (inBookshelf) { appDb.bookDao.update(book) } - loadChapter(it, scope) + if (it.isWebFile) { + loadWebFile(book, bookSource, scope) + } else { + loadChapter(it, scope) + } }.onError { AppLog.put("获取数据信息失败\n${it.localizedMessage}", it) context.toastOnUi(R.string.error_get_book_info) @@ -187,7 +189,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { appDb.bookChapterDao.insert(*it.toTypedArray()) chapterListData.postValue(it) } - } else if (isImportBookOnLine) { + } else if (book.isWebFile) { chapterListData.postValue(emptyList()) } else { bookSource?.let { bookSource -> @@ -224,6 +226,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { } } + fun loadGroup(groupId: Long, success: ((groupNames: String?) -> Unit)) { execute { appDb.bookGroupDao.getGroupNames(groupId).joinToString(",") @@ -232,6 +235,54 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { } } + private fun loadWebFile( + book: Book, + bookSource: BookSource, + scope: CoroutineScope = viewModelScope + ) { + execute(scope) { + webFileData?.clear() + val fileName = "${book.name} 作者:${book.author}" + if (book.downloadUrls.isNullOrEmpty()) { + val ruleDownloadUrls = bookSource.getBookInfoRule().downloadUrls + val content = AnalyzeUrl(book.bookUrl, source = bookSource).getStrResponse().body + val analyzeRule = AnalyzeRule(book, bookSource) + analyzeRule.setContent(content).setBaseUrl(book.bookUrl) + analyzeRule.getStringList(ruleDownloadUrls, isUrl = true)?.let { + parseDownloadUrls(it, fileName) + } ?: throw NoStackTraceException("Unexpected ruleDownloadUrls") + } else { + parseDownloadUrls(book.downloadUrls, fileName) + } + }.onError { + context.toastOnUi("LoadWebFileError\n${it.localizedMessage}") + } + } + + private fun parseDownloadUrls(downloadUrls: List?, fileName: String) { + val urls = downloadUrls + urls?.forEach { url -> + val mFileName = "${fileName}.${LocalBook.parseFileSuffix(url)}" + val isSupportedFile = AppPattern.bookFileRegex.matches(mFileName) + webFileData?.add(WebFile(url, mFileName, isSupportedFile)) + } + } + + fun importOrDownloadWebFile(webFile: WebFile, success: ((Uri?) -> Unit)?) { + bookSource ?: return + execute { + if (webFile.isSupported) { + LocalBook.importFileOnLine(webFile.url, webFile.name, bookSource) + } else { + LocalBook.saveBookFile(webFile.url, webFile.name, bookSource) + } + }.onSuccess { + success?.invoke(it as? Uri) + }.onError { + context.toastOnUi("ImportWebFileError\n${it.localizedMessage}") + } + } + fun changeTo(source: BookSource, book: Book, toc: List) { changeSourceCoroutine?.cancel() changeSourceCoroutine = execute { @@ -356,4 +407,14 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { } } + data class WebFile( + val url: String, + val name: String, + val isSupported: Boolean + ) { + override fun toString(): String { + return name + } + } + }