Implement feed groups manual sorting

Now, the user can sort its groups to his liking even after he created
them.

Also updated the database diagram to reflect the table's new column.
This commit is contained in:
Mauricio Colli 2020-02-25 23:01:23 -03:00
parent 50714c3006
commit d1d5f6821f
No known key found for this signature in database
GPG Key ID: F200BFD6F29DDD85
20 changed files with 473 additions and 19 deletions

View File

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "83d5d68663102d5fa28d63caaffb396d",
"identityHash": "9f825b1ee281480bedd38b971feac327",
"entities": [
{
"tableName": "subscriptions",
@ -555,7 +555,7 @@
},
{
"tableName": "feed_group",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "uid",
@ -574,6 +574,12 @@
"columnName": "icon_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sortOrder",
"columnName": "sort_order",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
@ -582,7 +588,16 @@
],
"autoGenerate": true
},
"indices": [],
"indices": [
{
"name": "index_feed_group_sort_order",
"unique": false,
"columnNames": [
"sort_order"
],
"createSql": "CREATE INDEX `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
}
],
"foreignKeys": []
},
{
@ -686,7 +701,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '83d5d68663102d5fa28d63caaffb396d')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9f825b1ee281480bedd38b971feac327')"
]
}
}

View File

@ -91,7 +91,8 @@ public class Migrations {
// Tables for feed feature
database.execSQL("CREATE TABLE IF NOT EXISTS feed (stream_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, PRIMARY KEY(stream_id, subscription_id), FOREIGN KEY(stream_id) REFERENCES streams(uid) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
database.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)");
database.execSQL("CREATE TABLE IF NOT EXISTS feed_group (uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, icon_id INTEGER NOT NULL)");
database.execSQL("CREATE TABLE IF NOT EXISTS feed_group (uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)");
database.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)");
database.execSQL("CREATE TABLE IF NOT EXISTS feed_group_subscription_join (group_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, PRIMARY KEY(group_id, subscription_id), FOREIGN KEY(group_id) REFERENCES feed_group(uid) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
database.execSQL("CREATE INDEX index_feed_group_subscription_join_subscription_id ON feed_group_subscription_join (subscription_id)");
database.execSQL("CREATE TABLE IF NOT EXISTS feed_last_updated (subscription_id INTEGER NOT NULL, last_updated INTEGER, PRIMARY KEY(subscription_id), FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");

View File

@ -9,14 +9,18 @@ import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity
@Dao
abstract class FeedGroupDAO {
@Query("SELECT * FROM feed_group")
@Query("SELECT * FROM feed_group ORDER BY sort_order ASC")
abstract fun getAll(): Flowable<List<FeedGroupEntity>>
@Query("SELECT * FROM feed_group WHERE uid = :groupId")
abstract fun getGroup(groupId: Long): Maybe<FeedGroupEntity>
@Insert(onConflict = OnConflictStrategy.ABORT)
abstract fun insert(feedEntity: FeedGroupEntity): Long
@Transaction
open fun insert(feedGroupEntity: FeedGroupEntity): Long {
val nextSortOrder = nextSortOrder()
feedGroupEntity.sortOrder = nextSortOrder
return insertInternal(feedGroupEntity)
}
@Update(onConflict = OnConflictStrategy.IGNORE)
abstract fun update(feedGroupEntity: FeedGroupEntity): Int
@ -41,4 +45,18 @@ abstract class FeedGroupDAO {
deleteSubscriptionsFromGroup(groupId)
insertSubscriptionsToGroup(subscriptionIds.map { FeedGroupSubscriptionEntity(groupId, it) })
}
@Transaction
open fun updateOrder(orderMap: Map<Long, Long>) {
orderMap.forEach { (groupId, sortOrder) -> updateOrder(groupId, sortOrder) }
}
@Query("UPDATE feed_group SET sort_order = :sortOrder WHERE uid = :groupId")
abstract fun updateOrder(groupId: Long, sortOrder: Long): Int
@Query("SELECT IFNULL(MAX(sort_order) + 1, 0) FROM feed_group")
protected abstract fun nextSortOrder(): Long
@Insert(onConflict = OnConflictStrategy.ABORT)
protected abstract fun insertInternal(feedGroupEntity: FeedGroupEntity): Long
}

View File

@ -2,11 +2,16 @@ package org.schabi.newpipe.database.feed.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.FEED_GROUP_TABLE
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.SORT_ORDER
import org.schabi.newpipe.local.subscription.FeedGroupIcon
@Entity(tableName = FEED_GROUP_TABLE)
@Entity(
tableName = FEED_GROUP_TABLE,
indices = [Index(SORT_ORDER)]
)
data class FeedGroupEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = ID)
@ -16,7 +21,10 @@ data class FeedGroupEntity(
var name: String,
@ColumnInfo(name = ICON)
var icon: FeedGroupIcon
var icon: FeedGroupIcon,
@ColumnInfo(name = SORT_ORDER)
var sortOrder: Long = -1
) {
companion object {
const val FEED_GROUP_TABLE = "feed_group"
@ -24,6 +32,7 @@ data class FeedGroupEntity(
const val ID = "uid"
const val NAME = "name"
const val ICON = "icon_id"
const val SORT_ORDER = "sort_order"
const val GROUP_ALL_ID = -1L
}

View File

@ -147,6 +147,15 @@ class FeedDatabaseManager(context: Context) {
.observeOn(AndroidSchedulers.mainThread())
}
fun updateGroupsOrder(groupIdList: List<Long>): Completable {
var index = 0L
val orderMap = groupIdList.associateBy({ it }, { index++ })
return Completable.fromCallable { feedGroupTable.updateOrder(orderMap) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<Date>> {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedTable.oldestSubscriptionUpdateFromAll()

View File

@ -25,8 +25,9 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.fragments.BaseStateFragment
import org.schabi.newpipe.local.subscription.SubscriptionViewModel.*
import org.schabi.newpipe.local.subscription.SubscriptionViewModel.SubscriptionState
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialog
import org.schabi.newpipe.local.subscription.item.*
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.EXPORT_COMPLETE_ACTION
@ -34,11 +35,8 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.*
import org.schabi.newpipe.report.UserAction
import org.schabi.newpipe.util.*
import org.schabi.newpipe.util.AnimationUtils.animateView
import org.schabi.newpipe.util.FilePickerActivityHelper
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.ShareUtils
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
@ -54,6 +52,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private val feedGroupsSection = Section()
private var feedGroupsCarousel: FeedGroupCarouselItem? = null
private lateinit var importExportItem: FeedImportExportItem
private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem
private val subscriptionsSection = Section()
@State @JvmField var itemsListState: Parcelable? = null
@ -164,6 +163,10 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.absolutePath), REQUEST_EXPORT_CODE)
}
private fun openReorderDialog() {
FeedGroupReorderDialog().show(requireFragmentManager(), null)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (data != null && data.data != null && resultCode == Activity.RESULT_OK) {
@ -210,7 +213,12 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
}
feedGroupsCarousel = FeedGroupCarouselItem(requireContext(), carouselAdapter)
add(Section(HeaderItem(getString(R.string.feed_groups_header_title)), listOf(feedGroupsCarousel)))
feedGroupsSortMenuItem = HeaderWithMenuItem(
getString(R.string.feed_groups_header_title),
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_sort),
menuItemOnClickListener = ::openReorderDialog
)
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
groupAdapter.add(this)
}
@ -333,6 +341,12 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
feedGroupsCarousel?.onRestoreInstanceState(feedGroupsListState)
feedGroupsListState = null
}
if (groups.size < 2) {
items_list.post { feedGroupsSortMenuItem.notifyChanged(HeaderWithMenuItem.PAYLOAD_HIDE_MENU_ITEM) }
} else {
items_list.post { feedGroupsSortMenuItem.notifyChanged(HeaderWithMenuItem.PAYLOAD_SHOW_MENU_ITEM) }
}
}
///////////////////////////////////////////////////////////////////////////

View File

@ -39,6 +39,7 @@ class FeedGroupDialog : DialogFragment() {
private lateinit var viewModel: FeedGroupDialogViewModel
private var groupId: Long = NO_GROUP_SELECTED
private var groupIcon: FeedGroupIcon? = null
private var groupSortOrder: Long = -1
sealed class ScreenState : Serializable {
object InitialScreen : ScreenState()
@ -145,7 +146,7 @@ class FeedGroupDialog : DialogFragment() {
when (groupId) {
NO_GROUP_SELECTED -> viewModel.createGroup(name, icon, selectedSubscriptions)
else -> viewModel.updateGroup(name, icon, selectedSubscriptions)
else -> viewModel.updateGroup(name, icon, selectedSubscriptions, groupSortOrder)
}
} else {
showInitialScreen()
@ -167,6 +168,7 @@ class FeedGroupDialog : DialogFragment() {
val icon = feedGroupEntity?.icon ?: FeedGroupIcon.ALL
val name = feedGroupEntity?.name ?: ""
groupIcon = feedGroupEntity?.icon
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
icon_preview.setImageResource((if (selectedIcon == null) icon else selectedIcon!!).getDrawableRes(requireContext()))

View File

@ -58,9 +58,9 @@ class FeedGroupDialogViewModel(applicationContext: Context, val groupId: Long =
.subscribe { successLiveData.postValue(FeedDialogEvent.SuccessEvent) })
}
fun updateGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set<Long>) {
fun updateGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set<Long>, sortOrder: Long) {
disposables.add(feedDatabaseManager.updateSubscriptionsForGroup(groupId, selectedSubscriptions.toList())
.andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon)))
.andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder)))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { successLiveData.postValue(FeedDialogEvent.SuccessEvent) })

View File

@ -0,0 +1,103 @@
package org.schabi.newpipe.local.subscription.dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.TouchCallback
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import icepick.Icepick
import icepick.State
import kotlinx.android.synthetic.main.dialog_feed_group_reorder.*
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
import org.schabi.newpipe.util.ThemeHelper
import java.util.*
import kotlin.collections.ArrayList
class FeedGroupReorderDialog : DialogFragment() {
private lateinit var viewModel: FeedGroupReorderDialogViewModel
@State @JvmField var groupOrderedIdList = ArrayList<Long>()
private val groupAdapter = GroupAdapter<GroupieViewHolder>()
private val itemTouchHelper = ItemTouchHelper(getItemTouchCallback())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Icepick.restoreInstanceState(this, savedInstanceState)
setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_feed_group_reorder, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this).get(FeedGroupReorderDialogViewModel::class.java)
viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups))
viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer {
when (it) {
is SuccessEvent -> dismiss()
}
})
feed_groups_list.layoutManager = LinearLayoutManager(requireContext())
feed_groups_list.adapter = groupAdapter
itemTouchHelper.attachToRecyclerView(feed_groups_list)
confirm_button.setOnClickListener {
viewModel.updateOrder(groupOrderedIdList)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
Icepick.saveInstanceState(this, outState)
}
private fun handleGroups(list: List<FeedGroupEntity>) {
val groupList: List<FeedGroupEntity>
if (groupOrderedIdList.isEmpty()) {
groupList = list
groupOrderedIdList.addAll(groupList.map { it.uid })
} else {
groupList = list.sortedBy { groupOrderedIdList.indexOf(it.uid) }
}
groupAdapter.update(groupList.map { FeedGroupReorderItem(it, itemTouchHelper) })
}
private fun getItemTouchCallback(): SimpleCallback {
return object : TouchCallback() {
override fun onMove(recyclerView: RecyclerView, source: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder): Boolean {
val sourceIndex = source.adapterPosition
val targetIndex = target.adapterPosition
groupAdapter.notifyItemMoved(sourceIndex, targetIndex)
Collections.swap(groupOrderedIdList, sourceIndex, targetIndex)
return true
}
override fun isLongPressDragEnabled(): Boolean = false
override fun isItemViewSwipeEnabled(): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) {}
}
}
}

View File

@ -0,0 +1,41 @@
package org.schabi.newpipe.local.subscription.dialog
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.local.feed.FeedDatabaseManager
class FeedGroupReorderDialogViewModel(application: Application) : AndroidViewModel(application) {
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application)
val groupsLiveData = MutableLiveData<List<FeedGroupEntity>>()
val dialogEventLiveData = MutableLiveData<DialogEvent>()
private val disposables = CompositeDisposable()
private var groupsDisposable = feedDatabaseManager.groups()
.limit(1)
.subscribeOn(Schedulers.io())
.subscribe(groupsLiveData::postValue)
override fun onCleared() {
super.onCleared()
groupsDisposable.dispose()
disposables.dispose()
}
fun updateOrder(groupIdList: List<Long>) {
disposables.add(feedDatabaseManager.updateGroupsOrder(groupIdList)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { dialogEventLiveData.postValue(DialogEvent.SuccessEvent) })
}
sealed class DialogEvent {
object SuccessEvent : DialogEvent()
}
}

View File

@ -0,0 +1,48 @@
package org.schabi.newpipe.local.subscription.item
import android.view.MotionEvent
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
import androidx.recyclerview.widget.ItemTouchHelper.UP
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import kotlinx.android.synthetic.main.feed_group_reorder_item.*
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.local.subscription.FeedGroupIcon
data class FeedGroupReorderItem(
val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
val name: String,
val icon: FeedGroupIcon,
val dragCallback: ItemTouchHelper
) : Item() {
constructor (feedGroupEntity: FeedGroupEntity, dragCallback: ItemTouchHelper)
: this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon, dragCallback)
override fun getId(): Long {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> super.getId()
else -> groupId
}
}
override fun getLayout(): Int = R.layout.feed_group_reorder_item
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.group_name.text = name
viewHolder.group_icon.setImageResource(icon.getDrawableRes(viewHolder.containerView.context))
viewHolder.handle.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
dragCallback.startDrag(viewHolder)
return@setOnTouchListener true
}
false
}
}
override fun getDragDirs(): Int {
return UP or DOWN
}
}

View File

@ -0,0 +1,48 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View.*
import androidx.annotation.DrawableRes
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import kotlinx.android.synthetic.main.header_with_menu_item.*
import org.schabi.newpipe.R
class HeaderWithMenuItem(
val title: String,
@DrawableRes val itemIcon: Int = 0,
private val onClickListener: (() -> Unit)? = null,
private val menuItemOnClickListener: (() -> Unit)? = null
) : Item() {
companion object {
const val PAYLOAD_SHOW_MENU_ITEM = 1
const val PAYLOAD_HIDE_MENU_ITEM = 2
}
override fun getLayout(): Int = R.layout.header_with_menu_item
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.contains(PAYLOAD_SHOW_MENU_ITEM)) {
viewHolder.header_menu_item.visibility = VISIBLE
return
} else if (payloads.contains(PAYLOAD_HIDE_MENU_ITEM)) {
viewHolder.header_menu_item.visibility = GONE
return
}
super.bind(viewHolder, position, payloads)
}
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.header_title.text = title
viewHolder.header_menu_item.setImageResource(itemIcon)
val listener: OnClickListener? =
onClickListener?.let { OnClickListener { onClickListener.invoke() } }
viewHolder.root.setOnClickListener(listener)
val menuItemListener: OnClickListener? =
menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } }
viewHolder.header_menu_item.setOnClickListener(menuItemListener)
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
</vector>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/feed_groups_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipToPadding="false"
android:padding="6dp"
android:scrollbars="vertical"
tools:itemCount="100"
tools:listitem="@layout/feed_group_reorder_item" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/separator_color" />
<Button
android:id="@+id/confirm_button"
style="?buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/finish" />
</LinearLayout>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layoutCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="3dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="3dp"
android:minHeight="?listPreferredItemHeightSmall"
android:orientation="horizontal"
app:cardBackgroundColor="?attr/card_item_background_color"
app:cardCornerRadius="5dp"
app:cardElevation="2dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/group_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginLeft="16dp"
tools:ignore="ContentDescription,RtlHardcoded"
tools:src="?attr/ic_hot" />
<TextView
android:id="@+id/group_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="16dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:layout_toLeftOf="@+id/handle"
android:layout_toRightOf="@+id/group_icon"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="?textAppearanceListItem"
tools:ignore="RtlHardcoded"
tools:text="Lorem ipsum dolor sit amet" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:paddingLeft="16dp"
android:paddingTop="12dp"
android:paddingRight="16dp"
android:paddingBottom="12dp"
android:src="?attr/drag_handle"
tools:ignore="ContentDescription,RtlHardcoded" />
</RelativeLayout>
</androidx.cardview.widget.CardView>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingTop="12dp"
android:paddingRight="16dp"
android:paddingBottom="12dp">
<TextView
android:id="@+id/header_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:gravity="start|center_vertical"
android:maxLines="2"
android:minHeight="24dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold"
tools:text="Header" />
<ImageButton
android:id="@+id/header_menu_item"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
tools:src="?attr/ic_bookmark" />
</LinearLayout>

View File

@ -79,6 +79,7 @@
<attr name="ic_sunny" format="reference"/>
<attr name="ic_telescope" format="reference"/>
<attr name="ic_megaphone" format="reference"/>
<attr name="ic_sort" format="reference"/>
<attr name="progress_horizontal_drawable" format="reference"/>
<!-- Can't refer to colors directly in drawable's xml-->

View File

@ -94,6 +94,7 @@
<item name="ic_sunny">@drawable/ic_sunny_black_24dp</item>
<item name="ic_telescope">@drawable/ic_telescope_black_24dp</item>
<item name="ic_megaphone">@drawable/ic_megaphone_black_24dp</item>
<item name="ic_sort">@drawable/ic_sort_black_24dp</item>
<item name="separator_color">@color/light_separator_color</item>
<item name="contrast_background_color">@color/light_contrast_background_color</item>
@ -198,6 +199,7 @@
<item name="ic_sunny">@drawable/ic_sunny_white_24dp</item>
<item name="ic_telescope">@drawable/ic_telescope_white_24dp</item>
<item name="ic_megaphone">@drawable/ic_megaphone_white_24dp</item>
<item name="ic_sort">@drawable/ic_sort_white_24dp</item>
<item name="separator_color">@color/dark_separator_color</item>
<item name="contrast_background_color">@color/dark_contrast_background_color</item>

Binary file not shown.