mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Fix favorites not loading (#156)
Fixes bug introduced in #150 that prevented the favorites page from loading anything Also remembers the sort for each favorites tab
This commit is contained in:
parent
a0b9ac7673
commit
13c2bb32d2
12 changed files with 110 additions and 50 deletions
|
|
@ -6,6 +6,7 @@ import androidx.room.OnConflictStrategy
|
|||
import androidx.room.Query
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import java.util.UUID
|
||||
|
||||
@Dao
|
||||
|
|
@ -13,12 +14,17 @@ interface LibraryDisplayInfoDao {
|
|||
fun getItem(
|
||||
user: JellyfinUser,
|
||||
itemId: UUID,
|
||||
): LibraryDisplayInfo? = getItem(user.rowId, itemId.toServerString())
|
||||
|
||||
fun getItem(
|
||||
user: JellyfinUser,
|
||||
itemId: String,
|
||||
): LibraryDisplayInfo? = getItem(user.rowId, itemId)
|
||||
|
||||
@Query("SELECT * from LibraryDisplayInfo WHERE userId=:userId AND itemId=:itemId")
|
||||
fun getItem(
|
||||
userId: Int,
|
||||
itemId: UUID,
|
||||
itemId: String,
|
||||
): LibraryDisplayInfo?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.room.ForeignKey
|
|||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import kotlinx.serialization.UseSerializers
|
||||
|
|
@ -31,10 +32,17 @@ import java.util.UUID
|
|||
@Serializable
|
||||
data class LibraryDisplayInfo(
|
||||
val userId: Int,
|
||||
val itemId: UUID,
|
||||
val itemId: String,
|
||||
val sort: ItemSortBy,
|
||||
val direction: SortOrder,
|
||||
) {
|
||||
constructor(
|
||||
userId: Int,
|
||||
itemId: UUID,
|
||||
sort: ItemSortBy,
|
||||
direction: SortOrder,
|
||||
) : this(userId, itemId.toServerString(), sort, direction)
|
||||
|
||||
@Ignore @Transient
|
||||
val sortAndDirection = SortAndDirection(sort, direction)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,12 +37,10 @@ import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
|||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
|
||||
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
|
||||
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
|
||||
import com.github.damontecres.wholphin.ui.detail.CardGrid
|
||||
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||
|
|
@ -61,6 +59,7 @@ import org.jellyfin.sdk.model.api.ImageType
|
|||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -79,8 +78,7 @@ class CollectionFolderViewModel
|
|||
val filter = MutableLiveData<GetItemsFilter>(GetItemsFilter())
|
||||
|
||||
fun init(
|
||||
itemId: UUID?,
|
||||
potential: BaseItem?,
|
||||
itemId: String,
|
||||
initialSortAndDirection: SortAndDirection?,
|
||||
recursive: Boolean,
|
||||
filter: GetItemsFilter,
|
||||
|
|
@ -91,19 +89,21 @@ class CollectionFolderViewModel
|
|||
context.getString(R.string.error_loading_collection, itemId),
|
||||
) + Dispatchers.IO,
|
||||
) {
|
||||
if (itemId != null) {
|
||||
fetchItem(itemId)
|
||||
|
||||
val sortAndDirection =
|
||||
if (initialSortAndDirection == null) {
|
||||
serverRepository.currentUser?.let { user ->
|
||||
libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection
|
||||
} ?: SortAndDirection.DEFAULT
|
||||
} else {
|
||||
SortAndDirection.DEFAULT
|
||||
}
|
||||
loadResults(sortAndDirection, recursive, filter)
|
||||
this@CollectionFolderViewModel.itemId = itemId
|
||||
itemId?.toUUIDOrNull()?.let {
|
||||
fetchItem(it)
|
||||
}
|
||||
|
||||
val sortAndDirection =
|
||||
if (initialSortAndDirection == null) {
|
||||
serverRepository.currentUser?.let { user ->
|
||||
libraryDisplayInfoDao.getItem(user, itemId)?.sortAndDirection
|
||||
} ?: SortAndDirection.DEFAULT
|
||||
} else {
|
||||
SortAndDirection.DEFAULT
|
||||
}
|
||||
|
||||
loadResults(sortAndDirection, recursive, filter)
|
||||
}
|
||||
|
||||
fun onSortChange(
|
||||
|
|
@ -238,11 +238,36 @@ class CollectionFolderViewModel
|
|||
@Composable
|
||||
fun CollectionFolderGrid(
|
||||
preferences: UserPreferences,
|
||||
itemId: UUID?,
|
||||
item: BaseItem?,
|
||||
itemId: UUID,
|
||||
initialFilter: GetItemsFilter,
|
||||
recursive: Boolean,
|
||||
onClickItem: (BaseItem) -> Unit,
|
||||
sortOptions: List<ItemSortBy>,
|
||||
modifier: Modifier = Modifier,
|
||||
initialSortAndDirection: SortAndDirection? = null,
|
||||
showTitle: Boolean = true,
|
||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
) = CollectionFolderGrid(
|
||||
preferences,
|
||||
itemId.toServerString(),
|
||||
initialFilter,
|
||||
recursive,
|
||||
onClickItem,
|
||||
sortOptions,
|
||||
modifier,
|
||||
initialSortAndDirection = initialSortAndDirection,
|
||||
showTitle = showTitle,
|
||||
positionCallback = positionCallback,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun CollectionFolderGrid(
|
||||
preferences: UserPreferences,
|
||||
itemId: String,
|
||||
initialFilter: GetItemsFilter,
|
||||
recursive: Boolean,
|
||||
onClickItem: (BaseItem) -> Unit,
|
||||
sortOptions: List<ItemSortBy>,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: CollectionFolderViewModel = hiltViewModel(),
|
||||
initialSortAndDirection: SortAndDirection? = null,
|
||||
|
|
@ -250,7 +275,7 @@ fun CollectionFolderGrid(
|
|||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
) {
|
||||
OneTimeLaunchedEffect {
|
||||
viewModel.init(itemId, item, initialSortAndDirection, recursive, initialFilter)
|
||||
viewModel.init(itemId, initialSortAndDirection, recursive, initialFilter)
|
||||
}
|
||||
val sortAndDirection by viewModel.sortAndDirection.observeAsState()
|
||||
val filter by viewModel.filter.observeAsState(initialFilter)
|
||||
|
|
@ -276,6 +301,7 @@ fun CollectionFolderGrid(
|
|||
viewModel.onSortChange(it, recursive, filter)
|
||||
},
|
||||
showTitle = showTitle,
|
||||
sortOptions = sortOptions,
|
||||
positionCallback = positionCallback,
|
||||
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
|
||||
)
|
||||
|
|
@ -293,18 +319,12 @@ fun CollectionFolderGridContent(
|
|||
onClickItem: (BaseItem) -> Unit,
|
||||
onSortChange: (SortAndDirection) -> Unit,
|
||||
letterPosition: suspend (Char) -> Int,
|
||||
sortOptions: List<ItemSortBy>,
|
||||
modifier: Modifier = Modifier,
|
||||
showTitle: Boolean = true,
|
||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
) {
|
||||
val title = item?.name ?: item?.data?.collectionType?.name ?: stringResource(R.string.collection)
|
||||
val sortOptions =
|
||||
when (item?.data?.collectionType) {
|
||||
CollectionType.MOVIES -> MovieSortOptions
|
||||
CollectionType.TVSHOWS -> SeriesSortOptions
|
||||
CollectionType.HOMEVIDEOS -> VideoSortOptions
|
||||
else -> listOf(ItemSortBy.SORT_NAME, ItemSortBy.DATE_CREATED, ItemSortBy.RANDOM)
|
||||
}
|
||||
|
||||
var showHeader by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,15 @@ val SeriesSortOptions =
|
|||
ItemSortBy.RANDOM,
|
||||
)
|
||||
|
||||
val EpisodeSortOptions =
|
||||
listOf(
|
||||
ItemSortBy.SORT_NAME,
|
||||
ItemSortBy.DATE_CREATED,
|
||||
ItemSortBy.DATE_PLAYED,
|
||||
ItemSortBy.AIRED_EPISODE_ORDER,
|
||||
ItemSortBy.RANDOM,
|
||||
)
|
||||
|
||||
val VideoSortOptions =
|
||||
listOf(
|
||||
ItemSortBy.SORT_NAME,
|
||||
|
|
@ -56,14 +65,19 @@ val VideoSortOptions =
|
|||
@StringRes
|
||||
fun getStringRes(sort: ItemSortBy): Int =
|
||||
when (sort) {
|
||||
ItemSortBy.SORT_NAME -> R.string.sort_by_name
|
||||
ItemSortBy.SORT_NAME,
|
||||
ItemSortBy.SERIES_SORT_NAME,
|
||||
-> R.string.sort_by_name
|
||||
ItemSortBy.PREMIERE_DATE -> R.string.sort_by_date_released
|
||||
ItemSortBy.DATE_CREATED -> R.string.sort_by_date_added
|
||||
ItemSortBy.DATE_LAST_CONTENT_ADDED -> R.string.sort_by_date_episode_added
|
||||
ItemSortBy.DATE_PLAYED -> R.string.sort_by_date_played
|
||||
ItemSortBy.DATE_PLAYED,
|
||||
ItemSortBy.SERIES_DATE_PLAYED,
|
||||
-> R.string.sort_by_date_played
|
||||
ItemSortBy.RANDOM -> R.string.sort_by_random
|
||||
ItemSortBy.COMMUNITY_RATING -> R.string.community_rating
|
||||
ItemSortBy.CRITIC_RATING -> R.string.critic_rating
|
||||
ItemSortBy.PLAY_COUNT -> R.string.play_count
|
||||
ItemSortBy.AIRED_EPISODE_ORDER -> R.string.aired_episode_order
|
||||
else -> throw IllegalArgumentException("Unsupported sort option: $sort")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
|||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
|
||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||
import java.util.UUID
|
||||
|
||||
|
|
@ -31,10 +32,10 @@ fun CollectionFolderGeneric(
|
|||
preferences = preferences,
|
||||
onClickItem = { preferencesViewModel.navigationManager.navigateTo(it.destination()) },
|
||||
itemId = itemId,
|
||||
item = item,
|
||||
initialFilter = filter,
|
||||
showTitle = showHeader,
|
||||
recursive = recursive,
|
||||
sortOptions = VideoSortOptions,
|
||||
modifier =
|
||||
modifier
|
||||
.padding(start = 16.dp),
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
|||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
|
||||
import com.github.damontecres.wholphin.ui.detail.livetv.DvrSchedule
|
||||
import com.github.damontecres.wholphin.ui.detail.livetv.TvGuideGrid
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
|
|
@ -204,10 +205,10 @@ fun CollectionFolderLiveTv(
|
|||
preferences = preferences,
|
||||
onClickItem = onClickItem,
|
||||
itemId = folders[folderIndex].id,
|
||||
item = null,
|
||||
initialFilter = GetItemsFilter(),
|
||||
showTitle = showHeader,
|
||||
recursive = false,
|
||||
sortOptions = VideoSortOptions,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp),
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
|||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
|
||||
import com.github.damontecres.wholphin.ui.components.RecommendedMovie
|
||||
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
|
||||
import com.github.damontecres.wholphin.ui.data.VideoSortOptions
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||
|
|
@ -160,13 +162,13 @@ fun CollectionFolderMovie(
|
|||
preferences = preferences,
|
||||
onClickItem = onClickItem,
|
||||
itemId = destination.itemId,
|
||||
item = destination.item,
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
sortOptions = MovieSortOptions,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
|
|
@ -182,13 +184,13 @@ fun CollectionFolderMovie(
|
|||
preferences = preferences,
|
||||
onClickItem = onClickItem,
|
||||
itemId = destination.itemId,
|
||||
item = destination.item,
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
includeItemTypes = listOf(BaseItemKind.BOX_SET),
|
||||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
sortOptions = VideoSortOptions,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
|||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.GenreCardGrid
|
||||
import com.github.damontecres.wholphin.ui.components.RecommendedTvShow
|
||||
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||
|
|
@ -158,13 +159,13 @@ fun CollectionFolderTv(
|
|||
CollectionFolderGrid(
|
||||
preferences = preferences,
|
||||
itemId = destination.itemId,
|
||||
item = destination.item,
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
includeItemTypes = listOf(BaseItemKind.SERIES),
|
||||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
sortOptions = SeriesSortOptions,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
|||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.data.EpisodeSortOptions
|
||||
import com.github.damontecres.wholphin.ui.data.MovieSortOptions
|
||||
import com.github.damontecres.wholphin.ui.data.SeriesSortOptions
|
||||
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
|
|
@ -147,8 +150,7 @@ fun FavoritesPage(
|
|||
CollectionFolderGrid(
|
||||
preferences = preferences,
|
||||
onClickItem = onClickItem,
|
||||
itemId = null,
|
||||
item = null,
|
||||
itemId = "${NavDrawerItem.Favorites.id}_movies",
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
favorite = true,
|
||||
|
|
@ -156,6 +158,7 @@ fun FavoritesPage(
|
|||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
sortOptions = MovieSortOptions,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
|
|
@ -170,8 +173,7 @@ fun FavoritesPage(
|
|||
CollectionFolderGrid(
|
||||
preferences = preferences,
|
||||
onClickItem = onClickItem,
|
||||
itemId = null,
|
||||
item = null,
|
||||
itemId = "${NavDrawerItem.Favorites.id}_series",
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
favorite = true,
|
||||
|
|
@ -179,6 +181,7 @@ fun FavoritesPage(
|
|||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
sortOptions = SeriesSortOptions,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
|
|
@ -193,8 +196,7 @@ fun FavoritesPage(
|
|||
CollectionFolderGrid(
|
||||
preferences = preferences,
|
||||
onClickItem = onClickItem,
|
||||
itemId = null,
|
||||
item = null,
|
||||
itemId = "${NavDrawerItem.Favorites.id}_episodes",
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
favorite = true,
|
||||
|
|
@ -202,6 +204,7 @@ fun FavoritesPage(
|
|||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
sortOptions = EpisodeSortOptions,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import kotlinx.coroutines.Deferred
|
||||
|
|
@ -25,11 +26,13 @@ abstract class ItemViewModel(
|
|||
val api: ApiClient,
|
||||
) : ViewModel() {
|
||||
val item = MutableLiveData<BaseItem?>(null)
|
||||
lateinit var itemId: UUID
|
||||
lateinit var itemId: String
|
||||
lateinit var itemUuid: UUID
|
||||
|
||||
suspend fun fetchItem(itemId: UUID): BaseItem =
|
||||
withContext(Dispatchers.IO) {
|
||||
this@ItemViewModel.itemId = itemId
|
||||
this@ItemViewModel.itemId = itemId.toServerString()
|
||||
this@ItemViewModel.itemUuid = itemId
|
||||
val it = api.userLibraryApi.getItem(itemId).content
|
||||
val fetchedItem = BaseItem.from(it, api)
|
||||
return@withContext fetchedItem.let {
|
||||
|
|
|
|||
|
|
@ -129,21 +129,21 @@ class MovieViewModel
|
|||
fun setWatched(played: Boolean) =
|
||||
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||
if (played) {
|
||||
api.playStateApi.markPlayedItem(itemId)
|
||||
api.playStateApi.markPlayedItem(itemUuid)
|
||||
} else {
|
||||
api.playStateApi.markUnplayedItem(itemId)
|
||||
api.playStateApi.markUnplayedItem(itemUuid)
|
||||
}
|
||||
fetchAndSetItem(itemId)
|
||||
fetchAndSetItem(itemUuid)
|
||||
}
|
||||
|
||||
fun setFavorite(favorite: Boolean) =
|
||||
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||
if (favorite) {
|
||||
api.userLibraryApi.markFavoriteItem(itemId)
|
||||
api.userLibraryApi.markFavoriteItem(itemUuid)
|
||||
} else {
|
||||
api.userLibraryApi.unmarkFavoriteItem(itemId)
|
||||
api.userLibraryApi.unmarkFavoriteItem(itemUuid)
|
||||
}
|
||||
fetchAndSetItem(itemId)
|
||||
fetchAndSetItem(itemUuid)
|
||||
}
|
||||
|
||||
fun savePlayVersion(
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@
|
|||
<string name="watch_live">Watch live</string>
|
||||
<string name="years_old">%1$d years old</string>
|
||||
<string name="play_with_transcoding">Play with transcoding</string>
|
||||
<string name="aired_episode_order">Aired Episode Order</string>
|
||||
|
||||
<plurals name="downloads">
|
||||
<item quantity="zero">%d downloads</item>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue