添加规则订阅功能

pull/517/head
gedoor 5 years ago
parent d3b7bf7b2e
commit 7dd28530f7
  1. 4
      app/src/main/AndroidManifest.xml
  2. 25
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  3. 27
      app/src/main/java/io/legado/app/data/dao/RuleSubDao.kt
  4. 27
      app/src/main/java/io/legado/app/data/dao/SourceSubDao.kt
  5. 10
      app/src/main/java/io/legado/app/data/entities/RuleSub.kt
  6. 2
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  7. 4
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  8. 6
      app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt
  9. 72
      app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubActivity.kt
  10. 20
      app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubAdapter.kt
  11. 4
      app/src/main/res/layout/activity_rule_sub.xml
  12. 4
      app/src/main/res/values-zh-rHK/strings.xml
  13. 4
      app/src/main/res/values-zh-rTW/strings.xml
  14. 4
      app/src/main/res/values-zh/strings.xml
  15. 4
      app/src/main/res/values/strings.xml

@ -171,9 +171,9 @@
<activity <activity
android:name=".ui.qrcode.QrCodeActivity" android:name=".ui.qrcode.QrCodeActivity"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
<!-- 订阅 --> <!-- 规则订阅 -->
<activity <activity
android:name=".ui.rss.subscription.SourceSubActivity" android:name=".ui.rss.subscription.RuleSubActivity"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
<!-- 书源编辑 --> <!-- 书源编辑 -->
<activity <activity

@ -18,8 +18,8 @@ import java.util.*
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class, ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class, RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class, RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class,
SourceSub::class], RuleSub::class],
version = 25, version = 26,
exportSchema = true exportSchema = true
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -40,7 +40,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun readRecordDao(): ReadRecordDao abstract fun readRecordDao(): ReadRecordDao
abstract fun httpTTSDao(): HttpTTSDao abstract fun httpTTSDao(): HttpTTSDao
abstract fun cacheDao(): CacheDao abstract fun cacheDao(): CacheDao
abstract fun sourceSubDao(): SourceSubDao abstract fun ruleSubDao(): RuleSubDao
companion object { companion object {
@ -53,7 +53,7 @@ abstract class AppDatabase : RoomDatabase() {
migration_10_11, migration_11_12, migration_12_13, migration_13_14, migration_10_11, migration_11_12, migration_12_13, migration_13_14,
migration_14_15, migration_15_17, migration_17_18, migration_18_19, migration_14_15, migration_15_17, migration_17_18, migration_18_19,
migration_19_20, migration_20_21, migration_21_22, migration_22_23, migration_19_20, migration_20_21, migration_21_22, migration_22_23,
migration_23_24, migration_24_25 migration_23_24, migration_24_25, migration_25_26
) )
.allowMainThreadQueries() .allowMainThreadQueries()
.addCallback(dbCallback) .addCallback(dbCallback)
@ -215,6 +215,23 @@ abstract class AppDatabase : RoomDatabase() {
) )
} }
} }
private val migration_25_26 = object : Migration(25, 26) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `ruleSubs` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL,
`customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))
"""
)
database.execSQL(
"""
insert into `ruleSubs` select *, 0, 0 from `sourceSubs`
"""
)
database.execSQL("DROP TABLE `sourceSubs`")
}
}
} }
} }

@ -0,0 +1,27 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import io.legado.app.data.entities.RuleSub
@Dao
interface RuleSubDao {
@get:Query("select * from ruleSubs order by customOrder")
val all: List<RuleSub>
@Query("select * from ruleSubs order by customOrder")
fun observeAll(): LiveData<List<RuleSub>>
@get:Query("select customOrder from ruleSubs order by customOrder limit 0,1")
val maxOrder: Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg ruleSub: RuleSub)
@Delete
fun delete(vararg ruleSub: RuleSub)
@Update
fun update(vararg ruleSub: RuleSub)
}

@ -1,27 +0,0 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import io.legado.app.data.entities.SourceSub
@Dao
interface SourceSubDao {
@get:Query("select * from sourceSubs order by customOrder")
val all: List<SourceSub>
@Query("select * from sourceSubs order by customOrder")
fun observeAll(): LiveData<List<SourceSub>>
@get:Query("select customOrder from sourceSubs order by customOrder limit 0,1")
val maxOrder: Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg sourceSub: SourceSub)
@Delete
fun delete(vararg sourceSub: SourceSub)
@Update
fun update(vararg sourceSub: SourceSub)
}

@ -3,14 +3,16 @@ package io.legado.app.data.entities
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
@Entity(tableName = "sourceSubs") @Entity(tableName = "ruleSubs")
data class SourceSub( data class RuleSub(
@PrimaryKey @PrimaryKey
val id: Long = System.currentTimeMillis(), val id: Long = System.currentTimeMillis(),
var name: String = "", var name: String = "",
var url: String = "", var url: String = "",
var type: Int = 0, var type: Int = 0,
var customOrder: Int = 0 var customOrder: Int = 0,
var autoUpdate: Boolean = false,
var update: Long = System.currentTimeMillis()
) { ) {
fun setType(type: Type) { fun setType(type: Type) {
@ -18,6 +20,6 @@ data class SourceSub(
} }
enum class Type { enum class Type {
BookSource, RssSource BookSource, RssSource, ReplaceRule
} }
} }

@ -67,7 +67,7 @@ object Backup {
writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath) writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath)
writeListToJson(App.db.readRecordDao().all, "readRecord.json", backupPath) writeListToJson(App.db.readRecordDao().all, "readRecord.json", backupPath)
writeListToJson(App.db.searchKeywordDao().all, "searchHistory.json", backupPath) writeListToJson(App.db.searchKeywordDao().all, "searchHistory.json", backupPath)
writeListToJson(App.db.sourceSubDao().all, "sourceSub.json", backupPath) writeListToJson(App.db.ruleSubDao().all, "sourceSub.json", backupPath)
writeListToJson(App.db.txtTocRule().all, DefaultData.txtTocRuleFileName, backupPath) writeListToJson(App.db.txtTocRule().all, DefaultData.txtTocRuleFileName, backupPath)
writeListToJson(App.db.httpTTSDao().all, DefaultData.httpTtsFileName, backupPath) writeListToJson(App.db.httpTTSDao().all, DefaultData.httpTtsFileName, backupPath)
GSON.toJson(ReadBookConfig.configList).let { GSON.toJson(ReadBookConfig.configList).let {

@ -134,8 +134,8 @@ object Restore {
fileToListT<SearchKeyword>(path, "searchHistory.json")?.let { fileToListT<SearchKeyword>(path, "searchHistory.json")?.let {
App.db.searchKeywordDao().insert(*it.toTypedArray()) App.db.searchKeywordDao().insert(*it.toTypedArray())
} }
fileToListT<SourceSub>(path, "sourceSub.json")?.let { fileToListT<RuleSub>(path, "sourceSub.json")?.let {
App.db.sourceSubDao().insert(*it.toTypedArray()) App.db.ruleSubDao().insert(*it.toTypedArray())
} }
fileToListT<TxtTocRule>(path, DefaultData.txtTocRuleFileName)?.let { fileToListT<TxtTocRule>(path, DefaultData.txtTocRuleFileName)?.let {
App.db.txtTocRule().insert(*it.toTypedArray()) App.db.txtTocRule().insert(*it.toTypedArray())

@ -16,7 +16,7 @@ import io.legado.app.ui.rss.favorites.RssFavoritesActivity
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
import io.legado.app.ui.rss.source.manage.RssSourceActivity import io.legado.app.ui.rss.source.manage.RssSourceActivity
import io.legado.app.ui.rss.source.manage.RssSourceViewModel import io.legado.app.ui.rss.source.manage.RssSourceViewModel
import io.legado.app.ui.rss.subscription.SourceSubActivity import io.legado.app.ui.rss.subscription.RuleSubActivity
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import io.legado.app.utils.startActivity import io.legado.app.utils.startActivity
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
@ -56,10 +56,10 @@ class RssFragment : VMBaseFragment<RssSourceViewModel>(R.layout.fragment_rss),
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
adapter.addHeaderView { adapter.addHeaderView {
ItemRssBinding.inflate(layoutInflater, it, false).apply { ItemRssBinding.inflate(layoutInflater, it, false).apply {
tvName.setText(R.string.source_subscription) tvName.setText(R.string.rule_subscription)
ivIcon.setImageResource(R.mipmap.ic_launcher) ivIcon.setImageResource(R.mipmap.ic_launcher)
root.onClick { root.onClick {
startActivity<SourceSubActivity>() startActivity<RuleSubActivity>()
} }
} }
} }

@ -9,8 +9,8 @@ import androidx.recyclerview.widget.ItemTouchHelper
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseActivity import io.legado.app.base.BaseActivity
import io.legado.app.data.entities.SourceSub import io.legado.app.data.entities.RuleSub
import io.legado.app.databinding.ActivitySourceSubBinding import io.legado.app.databinding.ActivityRuleSubBinding
import io.legado.app.databinding.DialogSourceSubEditBinding import io.legado.app.databinding.DialogSourceSubEditBinding
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.ui.association.ImportBookSourceActivity import io.legado.app.ui.association.ImportBookSourceActivity
@ -20,14 +20,14 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivity
class SourceSubActivity : BaseActivity<ActivitySourceSubBinding>(), class RuleSubActivity : BaseActivity<ActivityRuleSubBinding>(),
SourceSubAdapter.Callback { RuleSubAdapter.Callback {
private lateinit var adapter: SourceSubAdapter private lateinit var adapter: RuleSubAdapter
private var liveData: LiveData<List<SourceSub>>? = null private var liveData: LiveData<List<RuleSub>>? = null
override fun getViewBinding(): ActivitySourceSubBinding { override fun getViewBinding(): ActivityRuleSubBinding {
return ActivitySourceSubBinding.inflate(layoutInflater) return ActivityRuleSubBinding.inflate(layoutInflater)
} }
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
@ -43,15 +43,15 @@ class SourceSubActivity : BaseActivity<ActivitySourceSubBinding>(),
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_add -> { R.id.menu_add -> {
val order = App.db.sourceSubDao().maxOrder + 1 val order = App.db.ruleSubDao().maxOrder + 1
editSubscription(SourceSub(customOrder = order)) editSubscription(RuleSub(customOrder = order))
} }
} }
return super.onCompatOptionsItemSelected(item) return super.onCompatOptionsItemSelected(item)
} }
private fun initView() { private fun initView() {
adapter = SourceSubAdapter(this, this) adapter = RuleSubAdapter(this, this)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
val itemTouchCallback = ItemTouchCallback(adapter) val itemTouchCallback = ItemTouchCallback(adapter)
itemTouchCallback.isCanDrag = true itemTouchCallback.isCanDrag = true
@ -60,69 +60,69 @@ class SourceSubActivity : BaseActivity<ActivitySourceSubBinding>(),
private fun initData() { private fun initData() {
liveData?.removeObservers(this) liveData?.removeObservers(this)
liveData = App.db.sourceSubDao().observeAll() liveData = App.db.ruleSubDao().observeAll()
liveData?.observe(this) { liveData?.observe(this) {
binding.tvEmptyMsg.isGone = it.isNotEmpty() binding.tvEmptyMsg.isGone = it.isNotEmpty()
adapter.setItems(it) adapter.setItems(it)
} }
} }
override fun openSubscription(sourceSub: SourceSub) { override fun openSubscription(ruleSub: RuleSub) {
when (sourceSub.type) { when (ruleSub.type) {
SourceSub.Type.RssSource.ordinal -> { RuleSub.Type.RssSource.ordinal -> {
startActivity<ImportRssSourceActivity>("source" to sourceSub.url) startActivity<ImportRssSourceActivity>("source" to ruleSub.url)
} }
else -> { else -> {
startActivity<ImportBookSourceActivity>("source" to sourceSub.url) startActivity<ImportBookSourceActivity>("source" to ruleSub.url)
} }
} }
} }
override fun editSubscription(sourceSub: SourceSub) { override fun editSubscription(ruleSub: RuleSub) {
alert(R.string.source_subscription) { alert(R.string.rule_subscription) {
val alertBinding = DialogSourceSubEditBinding.inflate(layoutInflater).apply { val alertBinding = DialogSourceSubEditBinding.inflate(layoutInflater).apply {
when (sourceSub.type) { when (ruleSub.type) {
SourceSub.Type.RssSource.ordinal -> rgType.check(R.id.rb_rss_source) RuleSub.Type.RssSource.ordinal -> rgType.check(R.id.rb_rss_source)
else -> rgType.check(R.id.rb_book_source) else -> rgType.check(R.id.rb_book_source)
} }
etName.setText(sourceSub.name) etName.setText(ruleSub.name)
etUrl.setText(sourceSub.url) etUrl.setText(ruleSub.url)
} }
customView = alertBinding.root customView = alertBinding.root
okButton { okButton {
when (alertBinding.rgType.checkedRadioButtonId) { when (alertBinding.rgType.checkedRadioButtonId) {
R.id.rb_rss_source -> sourceSub.setType(SourceSub.Type.RssSource) R.id.rb_rss_source -> ruleSub.setType(RuleSub.Type.RssSource)
else -> sourceSub.setType(SourceSub.Type.BookSource) else -> ruleSub.setType(RuleSub.Type.BookSource)
} }
sourceSub.name = alertBinding.etName.text?.toString() ?: "" ruleSub.name = alertBinding.etName.text?.toString() ?: ""
sourceSub.url = alertBinding.etUrl.text?.toString() ?: "" ruleSub.url = alertBinding.etUrl.text?.toString() ?: ""
launch(IO) { launch(IO) {
App.db.sourceSubDao().insert(sourceSub) App.db.ruleSubDao().insert(ruleSub)
} }
} }
cancelButton() cancelButton()
}.show() }.show()
} }
override fun delSubscription(sourceSub: SourceSub) { override fun delSubscription(ruleSub: RuleSub) {
launch(IO) { launch(IO) {
App.db.sourceSubDao().delete(sourceSub) App.db.ruleSubDao().delete(ruleSub)
} }
} }
override fun updateSourceSub(vararg sourceSub: SourceSub) { override fun updateSourceSub(vararg ruleSub: RuleSub) {
launch(IO) { launch(IO) {
App.db.sourceSubDao().update(*sourceSub) App.db.ruleSubDao().update(*ruleSub)
} }
} }
override fun upOrder() { override fun upOrder() {
launch(IO) { launch(IO) {
val sourceSubs = App.db.sourceSubDao().all val sourceSubs = App.db.ruleSubDao().all
for ((index: Int, sourceSub: SourceSub) in sourceSubs.withIndex()) { for ((index: Int, ruleSub: RuleSub) in sourceSubs.withIndex()) {
sourceSub.customOrder = index + 1 ruleSub.customOrder = index + 1
} }
App.db.sourceSubDao().update(*sourceSubs.toTypedArray()) App.db.ruleSubDao().update(*sourceSubs.toTypedArray())
} }
} }

@ -8,23 +8,23 @@ import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.SourceSub import io.legado.app.data.entities.RuleSub
import io.legado.app.databinding.ItemSourceSubBinding import io.legado.app.databinding.ItemSourceSubBinding
import io.legado.app.ui.widget.recycler.ItemTouchCallback import io.legado.app.ui.widget.recycler.ItemTouchCallback
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import java.util.* import java.util.*
class SourceSubAdapter(context: Context, val callBack: Callback) : class RuleSubAdapter(context: Context, val callBack: Callback) :
SimpleRecyclerAdapter<SourceSub, ItemSourceSubBinding>(context), SimpleRecyclerAdapter<RuleSub, ItemSourceSubBinding>(context),
ItemTouchCallback.Callback { ItemTouchCallback.Callback {
override fun convert( override fun convert(
holder: ItemViewHolder, holder: ItemViewHolder,
binding: ItemSourceSubBinding, binding: ItemSourceSubBinding,
item: SourceSub, item: RuleSub,
payloads: MutableList<Any> payloads: MutableList<Any>
) { ) {
binding.tvType.text = SourceSub.Type.values()[item.type].name binding.tvType.text = RuleSub.Type.values()[item.type].name
binding.tvName.text = item.name binding.tvName.text = item.name
binding.tvUrl.text = item.url binding.tvUrl.text = item.url
} }
@ -77,7 +77,7 @@ class SourceSubAdapter(context: Context, val callBack: Callback) :
return true return true
} }
private val movedItems = hashSetOf<SourceSub>() private val movedItems = hashSetOf<RuleSub>()
override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
if (movedItems.isNotEmpty()) { if (movedItems.isNotEmpty()) {
@ -87,10 +87,10 @@ class SourceSubAdapter(context: Context, val callBack: Callback) :
} }
interface Callback { interface Callback {
fun openSubscription(sourceSub: SourceSub) fun openSubscription(ruleSub: RuleSub)
fun editSubscription(sourceSub: SourceSub) fun editSubscription(ruleSub: RuleSub)
fun delSubscription(sourceSub: SourceSub) fun delSubscription(ruleSub: RuleSub)
fun updateSourceSub(vararg sourceSub: SourceSub) fun updateSourceSub(vararg ruleSub: RuleSub)
fun upOrder() fun upOrder()
} }

@ -10,7 +10,7 @@
android:id="@+id/title_bar" android:id="@+id/title_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:title="@string/source_subscription" /> app:title="@string/rule_subscription" />
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -31,7 +31,7 @@
android:layout_gravity="center" android:layout_gravity="center"
android:gravity="center" android:gravity="center"
android:visibility="gone" android:visibility="gone"
android:text="@string/source_sub_empty_msg" android:text="@string/rule_sub_empty_msg"
app:layout_constraintTop_toBottomOf="@+id/title_bar" app:layout_constraintTop_toBottomOf="@+id/title_bar"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"

@ -787,7 +787,7 @@
<string name="body_title">正文标题</string> <string name="body_title">正文标题</string>
<string name="show_hide">显示/隐藏</string> <string name="show_hide">显示/隐藏</string>
<string name="header_footer">页眉<![CDATA[&]]>页脚</string> <string name="header_footer">页眉<![CDATA[&]]>页脚</string>
<string name="source_subscription">订阅</string> <string name="rule_subscription">规则订阅</string>
<string name="source_sub_empty_msg">添加大佬们提供的源导入地址,添加后点击可导入源</string> <string name="rule_sub_empty_msg">添加大佬们提供的源导入地址,添加后点击可导入源</string>
</resources> </resources>

@ -788,7 +788,7 @@
<string name="body_title">正文标题</string> <string name="body_title">正文标题</string>
<string name="show_hide">显示/隐藏</string> <string name="show_hide">显示/隐藏</string>
<string name="header_footer">页眉<![CDATA[&]]>页脚</string> <string name="header_footer">页眉<![CDATA[&]]>页脚</string>
<string name="source_subscription">订阅</string> <string name="rule_subscription">规则订阅</string>
<string name="source_sub_empty_msg">添加大佬们提供的源导入地址,添加后点击可导入源</string> <string name="rule_sub_empty_msg">添加大佬们提供的源导入地址,添加后点击可导入源</string>
</resources> </resources>

@ -791,7 +791,7 @@
<string name="body_title">正文标题</string> <string name="body_title">正文标题</string>
<string name="show_hide">显示/隐藏</string> <string name="show_hide">显示/隐藏</string>
<string name="header_footer">页眉<![CDATA[&]]>页脚</string> <string name="header_footer">页眉<![CDATA[&]]>页脚</string>
<string name="source_subscription">订阅</string> <string name="rule_subscription">规则订阅</string>
<string name="source_sub_empty_msg">添加大佬们提供的源导入地址,添加后点击可导入源</string> <string name="rule_sub_empty_msg">添加大佬们提供的源导入地址,添加后点击可导入源</string>
</resources> </resources>

@ -794,7 +794,7 @@
<string name="body_title">正文标题</string> <string name="body_title">正文标题</string>
<string name="show_hide">显示/隐藏</string> <string name="show_hide">显示/隐藏</string>
<string name="header_footer">footer <![CDATA[&]]> header</string> <string name="header_footer">footer <![CDATA[&]]> header</string>
<string name="source_subscription">订阅</string> <string name="rule_subscription">规则订阅</string>
<string name="source_sub_empty_msg">添加大佬们提供的源导入地址,添加后点击可导入源</string> <string name="rule_sub_empty_msg">添加大佬们提供的源导入地址,添加后点击可导入源</string>
</resources> </resources>

Loading…
Cancel
Save