mirror of
https://github.com/NiciDieNase/chaosflix
synced 2024-11-26 22:20:24 +00:00
Merge branch 'develop' into feature/navigation
This commit is contained in:
commit
7bbb2100c6
34 changed files with 981 additions and 67 deletions
|
@ -35,6 +35,37 @@ references:
|
|||
|
||||
jobs:
|
||||
|
||||
setup:
|
||||
<<: *container_config
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
<<: *general_cache_key
|
||||
|
||||
- run:
|
||||
<<: *setup_env
|
||||
|
||||
- run:
|
||||
name: Download Dependencies
|
||||
command: |
|
||||
./gradlew $KEY_CONFIG $SIGN_CONFIG \
|
||||
-PversionCode=$VERSION_CODE_TOUCH \
|
||||
-PversionName=${VERSION_NAME} \
|
||||
androidDependencies
|
||||
|
||||
- save_cache:
|
||||
<<: *general_cache_key
|
||||
paths:
|
||||
- "~/.gradle"
|
||||
- "~/.m2"
|
||||
- "/opt/android-sdk-linux/licenses/"
|
||||
|
||||
- persist_to_workspace:
|
||||
root: *workspace_root
|
||||
paths:
|
||||
- .
|
||||
|
||||
build:
|
||||
<<: *container_config
|
||||
steps:
|
||||
|
@ -249,17 +280,20 @@ workflows:
|
|||
|
||||
build_test_publish:
|
||||
jobs:
|
||||
- build
|
||||
- setup
|
||||
- check:
|
||||
requires:
|
||||
- build
|
||||
- setup
|
||||
- test:
|
||||
requires:
|
||||
- build
|
||||
- setup
|
||||
- build:
|
||||
requires:
|
||||
- check
|
||||
- test
|
||||
- publish-appcenter:
|
||||
requires:
|
||||
- test
|
||||
- check
|
||||
- build
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
|
@ -267,8 +301,7 @@ workflows:
|
|||
- develop
|
||||
- publish-play:
|
||||
requires:
|
||||
- test
|
||||
- check
|
||||
- build
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
|
|
|
@ -147,6 +147,7 @@ dependencies {
|
|||
implementation 'com.squareup.retrofit2:retrofit:2.6.3'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.6.2'
|
||||
|
||||
api "com.google.code.gson:gson:2.8.6"
|
||||
api 'com.google.android.exoplayer:exoplayer:2.9.6'
|
||||
|
||||
api 'com.github.bumptech.glide:glide:4.9.0'
|
||||
|
@ -175,9 +176,17 @@ dependencies {
|
|||
testImplementation "junit:junit:4.13"
|
||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:5.6.0"
|
||||
|
||||
testImplementation "io.mockk:mockk:1.9"
|
||||
|
||||
testImplementation 'org.robolectric:robolectric:4.1'
|
||||
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
})
|
||||
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:5.5.2"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.5.2"
|
||||
|
||||
testImplementation "junit:junit:4.12"
|
||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:5.5.2"
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package de.nicidienase.chaosflix.common
|
||||
|
||||
import de.nicidienase.chaosflix.common.eventimport.FahrplanLecture
|
||||
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.Event
|
||||
|
||||
data class ImportItem(
|
||||
val lecture: FahrplanLecture,
|
||||
var event: Event?,
|
||||
var selected: Boolean = false
|
||||
)
|
|
@ -173,8 +173,6 @@ class OfflineItemManager(
|
|||
private val offlineEventDao: OfflineEventDao,
|
||||
private val preferencesManager: PreferencesManager
|
||||
) : BroadcastReceiver() {
|
||||
private val TAG = DownloadCancelHandler::class.java.simpleName
|
||||
|
||||
override fun onReceive(p0: Context?, p1: Intent?) {
|
||||
val downloadId = p1?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0)
|
||||
if (downloadId != null && downloadId == id) {
|
||||
|
@ -192,6 +190,10 @@ class OfflineItemManager(
|
|||
p0?.unregisterReceiver(this)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = DownloadCancelHandler::class.java.simpleName
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMovieDir(): String {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package de.nicidienase.chaosflix.common.eventimport
|
||||
|
||||
data class FahrplanExport(
|
||||
val conference: String,
|
||||
val lectures: List<FahrplanLecture>
|
||||
)
|
|
@ -0,0 +1,13 @@
|
|||
package de.nicidienase.chaosflix.common.eventimport
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class FahrplanLecture(
|
||||
@SerializedName("lecture_id")
|
||||
var lectureId: String? = null,
|
||||
var title: String,
|
||||
var subtitle: String? = null,
|
||||
var links: String? = null,
|
||||
var track: String? = null,
|
||||
var description: String? = null
|
||||
)
|
|
@ -18,6 +18,8 @@ import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.
|
|||
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.RelatedEvent
|
||||
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.RelatedEventDao
|
||||
import de.nicidienase.chaosflix.common.mediadata.network.RecordingService
|
||||
import de.nicidienase.chaosflix.common.userdata.entities.watchlist.WatchlistItem
|
||||
import de.nicidienase.chaosflix.common.userdata.entities.watchlist.WatchlistItemDao
|
||||
import de.nicidienase.chaosflix.common.util.ConferenceUtil
|
||||
import de.nicidienase.chaosflix.common.util.LiveEvent
|
||||
import de.nicidienase.chaosflix.common.util.SingleLiveEvent
|
||||
|
@ -42,6 +44,7 @@ class MediaRepository(
|
|||
private val eventDao: EventDao by lazy { database.eventDao() }
|
||||
private val recordingDao: RecordingDao by lazy { database.recordingDao() }
|
||||
private val relatedEventDao: RelatedEventDao by lazy { database.relatedEventDao() }
|
||||
private val watchlistItemDao: WatchlistItemDao by lazy { database.watchlistItemDao() }
|
||||
|
||||
fun updateConferencesAndGroups(): SingleLiveEvent<LiveEvent<State, List<Conference>, String>> {
|
||||
val updateState = SingleLiveEvent<LiveEvent<State, List<Conference>, String>>()
|
||||
|
@ -232,11 +235,22 @@ class MediaRepository(
|
|||
return conferenceDao.findConferenceByAcronymSuspend(data.lastPathSegment)
|
||||
}
|
||||
|
||||
private suspend fun searchEvent(queryString: String): Event? {
|
||||
suspend fun findEventByTitle(title: String): Event? {
|
||||
var event: Event? = eventDao.findEventByTitleSuspend(title)
|
||||
if (event == null) {
|
||||
event = searchEvent(title, true)
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
private suspend fun searchEvent(queryString: String, updateConference: Boolean = false): Event? {
|
||||
val searchEvents = recordingApi.searchEvents(queryString)
|
||||
if (searchEvents.events.isNotEmpty()) {
|
||||
val eventDto = searchEvents.events[0]
|
||||
val conference = updateConferencesAndGet(eventDto.conferenceUrl.split("/").last())
|
||||
if (updateConference && conference != null) {
|
||||
updateEventsForConference(conference)
|
||||
}
|
||||
if (conference?.id != null) {
|
||||
val event = Event(eventDto, conference.id)
|
||||
eventDao.updateOrInsert(event)
|
||||
|
@ -248,6 +262,10 @@ class MediaRepository(
|
|||
|
||||
suspend fun getAllOfflineEvents(): List<Long> = database.offlineEventDao().getAllDownloadReferences()
|
||||
|
||||
suspend fun saveOrUpdate(watchlistItem: WatchlistItem) {
|
||||
watchlistItemDao.updateOrInsert(watchlistItem)
|
||||
}
|
||||
|
||||
fun getReleatedEvents(event: Event, viewModelScope: CoroutineScope): LiveData<List<Event>> {
|
||||
val data = MutableLiveData<List<Event>>()
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
|
|
|
@ -61,6 +61,15 @@ abstract class EventDao : BaseDao<Event>() {
|
|||
@Query("DElETE FROM event")
|
||||
abstract fun delete()
|
||||
|
||||
@Query("SELECT * FROM event WHERE link = :url LIMIT 1")
|
||||
abstract suspend fun findEventByFahrplanUrl(url: String): Event?
|
||||
|
||||
@Query("SELECT * FROM event WHERE title LIKE :search LIMIT 1")
|
||||
abstract suspend fun findEventByTitleSuspend(search: String): Event?
|
||||
|
||||
@Query("SELECT * FROM event WHERE title LIKE :title LIMIT 1")
|
||||
abstract fun findSingleEventByTitle(title: String): LiveData<Event?>
|
||||
|
||||
override suspend fun updateOrInsertInternal(item: Event) {
|
||||
if (item.id != 0L) {
|
||||
update(item)
|
||||
|
|
|
@ -6,24 +6,43 @@ import androidx.room.Delete
|
|||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.BaseDao
|
||||
|
||||
@Dao
|
||||
interface WatchlistItemDao {
|
||||
@Query("SELECT * from watchlist_item")
|
||||
fun getAll(): LiveData<List<WatchlistItem>>
|
||||
abstract class WatchlistItemDao : BaseDao<WatchlistItem>() {
|
||||
|
||||
@Query("SELECT * from watchlist_item")
|
||||
fun getAllSync(): List<WatchlistItem>
|
||||
abstract fun getAll(): LiveData<List<WatchlistItem>>
|
||||
|
||||
@Query("SELECT * from watchlist_item")
|
||||
abstract fun getAllSync(): List<WatchlistItem>
|
||||
|
||||
@Query("SELECT * from watchlist_item WHERE event_guid = :guid LIMIT 1")
|
||||
fun getItemForEvent(guid: String): LiveData<WatchlistItem?>
|
||||
abstract fun getItemForEvent(guid: String): LiveData<WatchlistItem?>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun saveItem(item: WatchlistItem)
|
||||
abstract fun saveItem(item: WatchlistItem)
|
||||
|
||||
@Delete
|
||||
fun deleteItem(item: WatchlistItem)
|
||||
abstract fun deleteItem(item: WatchlistItem)
|
||||
|
||||
@Query("DELETE from watchlist_item WHERE event_guid = :guid")
|
||||
fun deleteItem(guid: String)
|
||||
abstract fun deleteItem(guid: String)
|
||||
|
||||
@Query("SELECT * from watchlist_item WHERE event_guid = :guid LIMIT 1")
|
||||
abstract suspend fun getItemForGuid(guid: String): WatchlistItem?
|
||||
|
||||
override suspend fun updateOrInsertInternal(item: WatchlistItem) {
|
||||
if (item.id != 0L) {
|
||||
update(item)
|
||||
} else {
|
||||
val existingEvent = getItemForGuid(item.eventGuid)
|
||||
if (existingEvent != null) {
|
||||
item.id = existingEvent.id
|
||||
update(item)
|
||||
} else {
|
||||
item.id = insert(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
package de.nicidienase.chaosflix.common.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.gson.Gson
|
||||
import de.nicidienase.chaosflix.common.ImportItem
|
||||
import de.nicidienase.chaosflix.common.eventimport.FahrplanExport
|
||||
import de.nicidienase.chaosflix.common.mediadata.MediaRepository
|
||||
import de.nicidienase.chaosflix.common.userdata.entities.watchlist.WatchlistItem
|
||||
import de.nicidienase.chaosflix.common.util.LiveEvent
|
||||
import de.nicidienase.chaosflix.common.util.SingleLiveEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class FavoritesImportViewModel(
|
||||
private val mediaRepository: MediaRepository
|
||||
) : ViewModel() {
|
||||
|
||||
val state: SingleLiveEvent<LiveEvent<State, List<ImportItem>, String>> = SingleLiveEvent()
|
||||
|
||||
private var lastImport: String = ""
|
||||
|
||||
private val _items = MutableLiveData<List<ImportItem>?>()
|
||||
val items: LiveData<List<ImportItem>?>
|
||||
get() = _items
|
||||
|
||||
private val _errorMessage = MutableLiveData<String?>()
|
||||
val errorMessage: LiveData<String?>
|
||||
get() = _errorMessage
|
||||
|
||||
private val _working = MutableLiveData<Boolean>(false)
|
||||
val working: LiveData<Boolean>
|
||||
get() = _working
|
||||
|
||||
private val _processCount = MutableLiveData<Pair<Int, Int>> (0 to 0)
|
||||
val processCount: LiveData<Pair<Int, Int>>
|
||||
get() = _processCount
|
||||
|
||||
val importItemCount: LiveData<Int> = Transformations.map(items) { items ->
|
||||
val selectedItems = items?.filter { it.selected && it.event != null }
|
||||
return@map selectedItems?.count() ?: 0
|
||||
}
|
||||
|
||||
val selectAll = MutableLiveData<Boolean>(true)
|
||||
|
||||
fun handleLectures(jsonImport: String) {
|
||||
if (jsonImport == lastImport) {
|
||||
return
|
||||
}
|
||||
lastImport = jsonImport
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
loading {
|
||||
handleLecturesInternal(jsonImport)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun handleLecturesInternal(jsonImport: String) {
|
||||
val export = Gson().fromJson(jsonImport, FahrplanExport::class.java)
|
||||
val events: List<ImportItem>
|
||||
_processCount.postValue(0 to export.lectures.size)
|
||||
try {
|
||||
events = export.lectures.mapIndexed { index, lecture ->
|
||||
val event = mediaRepository.findEventByTitle(lecture.title)
|
||||
_processCount.postValue(index to export.lectures.size)
|
||||
ImportItem(
|
||||
lecture = lecture,
|
||||
event = event,
|
||||
selected = false
|
||||
) }
|
||||
_items.postValue(events)
|
||||
} catch (e: Exception) {
|
||||
showErrorMessage(e.message ?: "An error occured while searching for recordings")
|
||||
}
|
||||
}
|
||||
|
||||
fun importFavorites() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val items: List<ImportItem> = _items.value?.filter { it.selected } ?: emptyList()
|
||||
if (items.isNotEmpty()) {
|
||||
loading {
|
||||
for (item in items) {
|
||||
Log.d(TAG, "${item.lecture.title}: ${item.selected}")
|
||||
val guid = item.event?.guid
|
||||
if (item.selected && guid != null) {
|
||||
mediaRepository.saveOrUpdate(WatchlistItem(eventGuid = guid))
|
||||
}
|
||||
}
|
||||
state.postValue(LiveEvent(State.IMPORT_DONE))
|
||||
}
|
||||
} else {
|
||||
showErrorMessage("No items to importFavorites")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAll(selected: Boolean? = null) {
|
||||
val items: List<ImportItem>? = _items.value
|
||||
val newList = items?.map { it.copy(selected = selected ?: (it.event != null)) }
|
||||
_items.postValue(newList)
|
||||
}
|
||||
|
||||
fun itemChanged(item: ImportItem) {
|
||||
val items: MutableList<ImportItem>? = _items.value?.toMutableList()
|
||||
val index = items?.indexOf(item)
|
||||
if (index != null) {
|
||||
items[index] = items[index].copy()
|
||||
}
|
||||
_items.postValue(items)
|
||||
}
|
||||
|
||||
fun selectNone() = selectAll(false)
|
||||
|
||||
private suspend fun loading(work: suspend () -> Unit) {
|
||||
_working.postValue(true)
|
||||
work()
|
||||
_working.postValue(false)
|
||||
}
|
||||
|
||||
fun errorShown() {
|
||||
_errorMessage.postValue(null)
|
||||
}
|
||||
|
||||
private fun showErrorMessage(message: String) {
|
||||
val currentMessage = _errorMessage.value
|
||||
if (currentMessage != null) {
|
||||
_errorMessage.postValue("$currentMessage\n$message")
|
||||
} else {
|
||||
_errorMessage.postValue(message)
|
||||
}
|
||||
}
|
||||
|
||||
fun unavailableItemClicked(item: ImportItem) {
|
||||
showErrorMessage("No recording for ${item.lecture.title} found, maybe it was not recorded or it has not been published yet.")
|
||||
}
|
||||
|
||||
enum class State {
|
||||
IMPORT_DONE
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = FavoritesImportViewModel::class.java.simpleName
|
||||
}
|
||||
}
|
|
@ -24,28 +24,48 @@ class ViewModelFactory private constructor(context: Context) : ViewModelProvider
|
|||
private val preferencesManager =
|
||||
PreferencesManager(PreferenceManager.getDefaultSharedPreferences(context.applicationContext))
|
||||
private val offlineItemManager =
|
||||
OfflineItemManager(context.applicationContext, database.offlineEventDao(), preferencesManager)
|
||||
OfflineItemManager(
|
||||
context.applicationContext,
|
||||
database.offlineEventDao(),
|
||||
preferencesManager
|
||||
)
|
||||
private val externalFilesDir = Environment.getExternalStorageDirectory()
|
||||
private val resourcesFacade by lazy { ResourcesFacade(context) }
|
||||
private val mediaRepository by lazy { MediaRepository(apiFactory.recordingApi, database) }
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(BrowseViewModel::class.java)) {
|
||||
return BrowseViewModel(offlineItemManager, mediaRepository, database, streamingRepository, preferencesManager, resourcesFacade) as T
|
||||
} else if (modelClass.isAssignableFrom(PlayerViewModel::class.java)) {
|
||||
return PlayerViewModel(database) as T
|
||||
} else if (modelClass.isAssignableFrom(DetailsViewModel::class.java)) {
|
||||
return DetailsViewModel(database, offlineItemManager, preferencesManager, mediaRepository) as T
|
||||
} else if (modelClass.isAssignableFrom(PreferencesViewModel::class.java)) {
|
||||
return PreferencesViewModel(mediaRepository, database.watchlistItemDao(), externalFilesDir) as T
|
||||
} else if (modelClass.isAssignableFrom(SplashViewModel::class.java)) {
|
||||
return SplashViewModel(mediaRepository) as T
|
||||
} else {
|
||||
throw UnsupportedOperationException("The requested ViewModel is currently unsupported. " +
|
||||
return when (modelClass) {
|
||||
BrowseViewModel::class.java -> BrowseViewModel(
|
||||
offlineItemManager,
|
||||
mediaRepository,
|
||||
database,
|
||||
streamingRepository,
|
||||
preferencesManager,
|
||||
resourcesFacade) as T
|
||||
PlayerViewModel::class.java -> PlayerViewModel(database) as T
|
||||
DetailsViewModel::class.java -> DetailsViewModel(
|
||||
database,
|
||||
offlineItemManager,
|
||||
preferencesManager,
|
||||
mediaRepository
|
||||
) as T
|
||||
PreferencesViewModel::class.java -> PreferencesViewModel(
|
||||
mediaRepository,
|
||||
database.watchlistItemDao(),
|
||||
externalFilesDir
|
||||
) as T
|
||||
FavoritesImportViewModel::class.java -> FavoritesImportViewModel(
|
||||
mediaRepository
|
||||
) as T
|
||||
SplashViewModel::class.java -> SplashViewModel(mediaRepository) as T
|
||||
else -> throw UnsupportedOperationException(
|
||||
"The requested ViewModel is currently unsupported. " +
|
||||
"Please make sure to implement are correct creation of it. " +
|
||||
" Request: ${modelClass.canonicalName}")
|
||||
" Request: ${modelClass.canonicalName}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object : SingletonHolder<ViewModelFactory, Context>(::ViewModelFactory)
|
||||
}
|
||||
|
|
9
common/src/main/res/drawable/ic_check_box.xml
Normal file
9
common/src/main/res/drawable/ic_check_box.xml
Normal 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="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
|
||||
</vector>
|
9
common/src/main/res/drawable/ic_check_box_outline.xml
Normal file
9
common/src/main/res/drawable/ic_check_box_outline.xml
Normal 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="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
|
||||
</vector>
|
|
@ -46,7 +46,8 @@
|
|||
<string name="setting_metered_networks">Allow downloads over metered networks</string>
|
||||
<string name="settings_choose_recording">Automatically choose recording</string>
|
||||
<string name="export_favorites">Export Favorites</string>
|
||||
<string name="import_favorites">Import Favorites</string>
|
||||
<string name="import_favorites">Import to Favorites</string>
|
||||
<string name="error_event_not_found">Event not found</string>
|
||||
<string name="privacy_policy">Chaosflix does not collect or transmit any personalized data.</string>
|
||||
<string name="no_recording_found_for">No Recording found for</string>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package de.nicidienase.chaosflix.common
|
||||
|
||||
import androidx.arch.core.executor.ArchTaskExecutor
|
||||
import androidx.arch.core.executor.TaskExecutor
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback
|
||||
import org.junit.jupiter.api.extension.ExtensionContext
|
||||
|
||||
class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
|
||||
|
||||
override fun beforeEach(context: ExtensionContext?) {
|
||||
ArchTaskExecutor.getInstance()
|
||||
.setDelegate(object : TaskExecutor() {
|
||||
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
|
||||
|
||||
override fun postToMainThread(runnable: Runnable) = runnable.run()
|
||||
|
||||
override fun isMainThread(): Boolean = true
|
||||
})
|
||||
}
|
||||
|
||||
override fun afterEach(context: ExtensionContext?) {
|
||||
ArchTaskExecutor.getInstance().setDelegate(null)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package de.nicidienase.chaosflix.common.viewmodel
|
||||
|
||||
import de.nicidienase.chaosflix.common.InstantExecutorExtension
|
||||
import de.nicidienase.chaosflix.common.mediadata.MediaRepository
|
||||
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.Event
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.impl.annotations.RelaxedMockK
|
||||
import io.mockk.junit5.MockKExtension
|
||||
import io.mockk.slot
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
|
||||
@ExtendWith(MockKExtension::class, InstantExecutorExtension::class)
|
||||
internal class FavoritesImportViewModelTest {
|
||||
|
||||
@RelaxedMockK
|
||||
private lateinit var mediaRepository: MediaRepository
|
||||
|
||||
private lateinit var favoritesImportViewModel: FavoritesImportViewModel
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
favoritesImportViewModel = FavoritesImportViewModel(mediaRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectAll() {
|
||||
val slot = slot<String>()
|
||||
coEvery { mediaRepository.findEventByTitle(capture(slot)) }.answers {
|
||||
Event(title = slot.captured)
|
||||
}
|
||||
runBlocking {
|
||||
favoritesImportViewModel.handleLecturesInternal(SAMPLE_JSON)
|
||||
}
|
||||
|
||||
favoritesImportViewModel.selectAll(null)
|
||||
val items = favoritesImportViewModel.items.value ?: emptyList()
|
||||
val selected = items.map { it.selected }
|
||||
assertEquals(2, selected.size)
|
||||
selected.map { value ->
|
||||
assertEquals(true, value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unselectAll() {
|
||||
val slot = slot<String>()
|
||||
coEvery { mediaRepository.findEventByTitle(capture(slot)) }.answers {
|
||||
Event(title = slot.captured)
|
||||
}
|
||||
runBlocking {
|
||||
favoritesImportViewModel.handleLecturesInternal(SAMPLE_JSON)
|
||||
}
|
||||
|
||||
favoritesImportViewModel.selectAll(false)
|
||||
val items = favoritesImportViewModel.items.value ?: emptyList()
|
||||
|
||||
assertEquals(2, items.size)
|
||||
items.map { value ->
|
||||
assertEquals(false, value.selected)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SAMPLE_JSON = "{\"conference\":\"ccc36c3\",\"lectures\":[" +
|
||||
"{\"lectureId\":\"11223\"," +
|
||||
"\"title\":\"Opening Ceremony\"," +
|
||||
"\"subtitle\":\"\",\"day\":1," +
|
||||
"\"room\":\"Ada\"," +
|
||||
"\"slug\":\"36c3-11223-opening_ceremony\"," +
|
||||
"\"url\":\"https://fahrplan.events.ccc.de/congress/2019/Fahrplan/events/11223.html\"," +
|
||||
"\"speakers\":\"bleeptrack;blinry\"," +
|
||||
"\"track\":\"CCC\"," +
|
||||
"\"type\":\"de\"," +
|
||||
"\"lang\":\"\"," +
|
||||
"\"abstract\":\"Welcome!\"," +
|
||||
"\"description\":\"\"," +
|
||||
"\"links\":\"2019-12-27\"}," +
|
||||
"{\"lectureId\":\"11224\"," +
|
||||
"\"title\":\"Closing Ceremony\"," +
|
||||
"\"subtitle\":\"\"," +
|
||||
"\"day\":4," +
|
||||
"\"room\":\"Ada\"," +
|
||||
"\"slug\":\"36c3-11224-closing_ceremony\"," +
|
||||
"\"url\":\"\"," +
|
||||
"\"speakers\":\"bleeptrack;blinry\"," +
|
||||
"\"track\":\"CCC\"," +
|
||||
"\"type\":\"de\"," +
|
||||
"\"lang\":\"\"," +
|
||||
"\"abstract\":\"Welcome!\"," +
|
||||
"\"description\":\"\"," +
|
||||
"\"links\":\"2019-12-30\"}" +
|
||||
"]}"
|
||||
}
|
||||
}
|
|
@ -52,6 +52,13 @@
|
|||
android:parentActivityName=".browse.BrowseActivity"/>
|
||||
|
||||
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
|
||||
<activity android:name=".favoritesimport.FavoritesImportActivity" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/json" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package de.nicidienase.chaosflix.touch.browse.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import de.nicidienase.chaosflix.common.ImportItem
|
||||
import de.nicidienase.chaosflix.touch.databinding.ItemFavoritImportBinding
|
||||
import java.lang.NumberFormatException
|
||||
|
||||
class ImportItemAdapter(private val onListItemClick: ((ImportItem) -> Unit)? = null, private val onLectureClick: ((ImportItem) -> Unit)? = null) : ListAdapter<ImportItem, ImportItemAdapter
|
||||
.ViewHolder>(
|
||||
object : DiffUtil.ItemCallback<ImportItem>() {
|
||||
override fun areItemsTheSame(oldItem: ImportItem, newItem: ImportItem) = oldItem === newItem
|
||||
override fun areContentsTheSame(oldItem: ImportItem, newItem: ImportItem): Boolean {
|
||||
return oldItem.selected == newItem.selected &&
|
||||
oldItem.lecture == newItem.lecture &&
|
||||
oldItem.event == newItem.event
|
||||
}
|
||||
}) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val binding = ItemFavoritImportBinding.inflate(inflater, parent, false)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
holder.binding.item = item
|
||||
holder.binding.importItemEvent.setOnClickListener {
|
||||
holder.binding.checkBox.apply {
|
||||
isChecked = !isChecked
|
||||
}
|
||||
onListItemClick?.invoke(item)
|
||||
}
|
||||
holder.binding.importItemLecture.setOnClickListener {
|
||||
onLectureClick?.invoke(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return try {
|
||||
getItem(position).lecture.lectureId?.toLong() ?: 0
|
||||
} catch (ex: NumberFormatException) {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(val binding: ItemFavoritImportBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
}
|
|
@ -5,7 +5,7 @@ import android.widget.Filterable
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import java.util.Collections
|
||||
|
||||
abstract class ItemRecyclerViewAdapter<T, VH : RecyclerView.ViewHolder?>() :
|
||||
abstract class ItemRecyclerViewAdapter<T, VH : RecyclerView.ViewHolder?> :
|
||||
RecyclerView.Adapter<VH>(), Filterable {
|
||||
|
||||
abstract fun getComparator(): Comparator<in T>?
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package de.nicidienase.chaosflix.touch.favoritesimport
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import de.nicidienase.chaosflix.touch.R
|
||||
|
||||
class FavoritesImportActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_favorites_import)
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.title = getString(R.string.import_activity_label)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = FavoritesImportActivity::class.java.simpleName
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
package de.nicidienase.chaosflix.touch.favoritesimport
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import de.nicidienase.chaosflix.common.ImportItem
|
||||
import de.nicidienase.chaosflix.common.viewmodel.FavoritesImportViewModel
|
||||
import de.nicidienase.chaosflix.common.viewmodel.ViewModelFactory
|
||||
import de.nicidienase.chaosflix.touch.R
|
||||
import de.nicidienase.chaosflix.touch.browse.adapters.ImportItemAdapter
|
||||
import de.nicidienase.chaosflix.touch.databinding.FragmentFavoritesImportBinding
|
||||
|
||||
class FavoritesImportFragment : Fragment() {
|
||||
|
||||
private lateinit var viewModel: FavoritesImportViewModel
|
||||
private lateinit var adapter: ImportItemAdapter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
setHasOptionsMenu(true)
|
||||
val binding = FragmentFavoritesImportBinding.inflate(inflater, container, false)
|
||||
binding.lifecycleOwner = this
|
||||
viewModel = ViewModelProviders.of(requireActivity(), ViewModelFactory.getInstance(requireContext())).get(FavoritesImportViewModel::class.java)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.importList.layoutManager = LinearLayoutManager(context)
|
||||
val onItemClick: (ImportItem) -> Unit = {
|
||||
viewModel.itemChanged(it)
|
||||
}
|
||||
val onLectureClick: (ImportItem) -> Unit = {
|
||||
viewModel.unavailableItemClicked(it)
|
||||
}
|
||||
adapter = ImportItemAdapter(onListItemClick = onItemClick, onLectureClick = onLectureClick)
|
||||
adapter.setHasStableIds(true)
|
||||
binding.importList.setHasFixedSize(true)
|
||||
binding.importList.adapter = adapter
|
||||
|
||||
viewModel.items.observe(viewLifecycleOwner, Observer { events ->
|
||||
if (events != null) {
|
||||
adapter.submitList(events.toList())
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.selectAll.observe(viewLifecycleOwner, Observer {
|
||||
activity?.invalidateOptionsMenu()
|
||||
})
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner, Observer {
|
||||
when (it.state) {
|
||||
FavoritesImportViewModel.State.IMPORT_DONE -> {
|
||||
// TODO navigate to favorites
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.working.observe(viewLifecycleOwner, Observer { working ->
|
||||
binding.incOverlay.loadingOverlay.visibility = if (working) View.VISIBLE else View.GONE
|
||||
})
|
||||
viewModel.processCount.observe(viewLifecycleOwner, Observer { pair ->
|
||||
Log.i(TAG, "Progress ${pair.first}/${pair.second}")
|
||||
binding.incOverlay.progressbar.apply {
|
||||
if (pair.second != 0) {
|
||||
visibility = View.VISIBLE
|
||||
progress = (100 * pair.first / pair.second)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.errorMessage.observe(viewLifecycleOwner, Observer { errorMessage ->
|
||||
if (errorMessage != null) {
|
||||
Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_LONG).apply {
|
||||
view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text).maxLines = 5
|
||||
setAction("OK", View.OnClickListener {
|
||||
this.dismiss()
|
||||
}).show()
|
||||
}
|
||||
viewModel.errorShown()
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.importItemCount.observe(viewLifecycleOwner, Observer { count ->
|
||||
if (count == 0) {
|
||||
binding.buttonImport.hide()
|
||||
} else {
|
||||
binding.buttonImport.show()
|
||||
}
|
||||
})
|
||||
|
||||
val intent = activity?.intent
|
||||
when {
|
||||
intent?.action == Intent.ACTION_SEND -> {
|
||||
when (intent.type) {
|
||||
"text/json" -> handleJson(intent)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Handle other intents, such as being started from the home screen
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.import_menu, menu)
|
||||
if (viewModel.selectAll.value != false) {
|
||||
menu.removeItem(R.id.menu_item_unselect_all)
|
||||
} else {
|
||||
menu.removeItem(R.id.menu_item_select_all)
|
||||
}
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_item_select_all -> {
|
||||
viewModel.selectAll()
|
||||
viewModel.selectAll.value = false
|
||||
true
|
||||
}
|
||||
R.id.menu_item_unselect_all -> {
|
||||
viewModel.selectNone()
|
||||
viewModel.selectAll.value = true
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleJson(intent: Intent) {
|
||||
val extra = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
if (extra.isNotEmpty()) {
|
||||
viewModel.handleLectures(extra)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = FavoritesImportFragment::class.java.simpleName
|
||||
}
|
||||
}
|
|
@ -119,7 +119,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
|
||||
importFavorites()
|
||||
} else {
|
||||
Snackbar.make(listView, "Cannot import without Storage Permission.", Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(listView, "Cannot importFavorites without Storage Permission.", Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
PERMISSION_REQUEST_EXPORT_FAVORITES -> {
|
||||
|
|
29
touch/src/main/res/layout/activity_favorites_import.xml
Normal file
29
touch/src/main/res/layout/activity_favorites_import.xml
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/import_root"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include
|
||||
android:id="@+id/inc_toolbar"
|
||||
layout="@layout/toolbar"/>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<fragment
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:name="de.nicidienase.chaosflix.touch.favoritesimport.FavoritesImportFragment"/>
|
||||
</LinearLayout>
|
|
@ -3,7 +3,7 @@
|
|||
android:layout_width="match_parent" android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar2"
|
||||
android:id="@+id/icon_not_found"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -23,7 +23,6 @@
|
|||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
style="@style/ToolbarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
|
|
|
@ -63,7 +63,6 @@
|
|||
android:id="@+id/anim_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
style="@style/ToolbarStyle"
|
||||
app:titleTextColor="@color/white"
|
||||
app:subtitleTextColor="@color/white"
|
||||
app:layout_collapseMode="pin"
|
||||
|
|
52
touch/src/main/res/layout/fragment_favorites_import.xml
Normal file
52
touch/src/main/res/layout/fragment_favorites_import.xml
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout 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">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="de.nicidienase.chaosflix.common.viewmodel.FavoritesImportViewModel" />
|
||||
</data>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/import_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:listitem="@layout/item_favorit_import" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/button_import"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:backgroundTint="@color/primary"
|
||||
android:text="@string/import_label"
|
||||
android:visibility="invisible"
|
||||
android:onClick="@{()->viewModel.importFavorites()}"
|
||||
app:icon="@drawable/ic_bookmark"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/inc_overlay"
|
||||
layout="@layout/loading_overlay"/>
|
||||
|
||||
|
||||
</FrameLayout>
|
||||
</layout>
|
|
@ -17,14 +17,14 @@
|
|||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/sliding_tabs"
|
||||
style="@style/ColoredToolbarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/primary"
|
||||
app:layout_anchor="@id/toolbar"
|
||||
app:layout_anchorGravity="bottom"
|
||||
app:tabIndicatorColor="@color/white"
|
||||
app:tabMode="scrollable"
|
||||
app:tabSelectedTextColor="@color/white"
|
||||
app:tabIndicatorColor="@color/white"
|
||||
app:tabTextColor="@color/inactive_tab_color"/>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
|
148
touch/src/main/res/layout/item_favorit_import.xml
Normal file
148
touch/src/main/res/layout/item_favorit_import.xml
Normal file
|
@ -0,0 +1,148 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout 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">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="item"
|
||||
type="de.nicidienase.chaosflix.common.ImportItem" />
|
||||
<import type="android.view.View"/>
|
||||
</data>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
app:cardElevation="0dp"
|
||||
app:cardCornerRadius="2dp"
|
||||
android:layout_margin="2dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<LinearLayout
|
||||
android:id="@+id/import_item_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/import_item_event"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="@{item.event == null ? View.GONE : View.VISIBLE}"
|
||||
tools:visibility="visible">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatCheckBox
|
||||
android:id="@+id/checkBox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:checked="@={item.selected}"
|
||||
android:text=""
|
||||
style="@android:style/Widget.Material.Light.CompoundButton.CheckBox"
|
||||
tools:checked="false"
|
||||
app:layout_constraintBottom_toBottomOf="@id/imageView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/imageView" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/imageView"
|
||||
imageUrl="@{item.event.thumbUrl}"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="70dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="@string/titleimage"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/checkBox"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:srcCompat="@drawable/unknown"
|
||||
tools:srcCompat="@drawable/unknown" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/event_title"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{item.event.title}"
|
||||
tools:text="Energiespeicher von heute für die Energie von morgen"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/event_subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@{item.event.subtitle}"
|
||||
tools:text="Wohin eigentlich mit all der erneuerbaren Energie?"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="@+id/event_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/event_title"
|
||||
app:layout_constraintVertical_bias="0.0" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/import_item_lecture"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="@{item.event == null ? View.VISIBLE : View.GONE}"
|
||||
tools:visibility="visible">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/icon_not_found"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:src="@drawable/ic_error"
|
||||
android:tint="#E53935"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/not_found_title"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@{item.lecture.title}"
|
||||
tools:text="Energiespeicher von heute für die Energie von morgen"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/icon_not_found"
|
||||
app:layout_constraintTop_toBottomOf="@+id/not_found_description" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/not_found_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/no_recording_found_for"
|
||||
app:layout_constraintStart_toStartOf="@+id/not_found_title"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</layout>
|
|
@ -3,19 +3,42 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<FrameLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/loading_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/inactive_tab_color"
|
||||
tools:visibility="visible"
|
||||
tools:showIn="@layout/fragment_tab_pager_layout">
|
||||
|
||||
<de.nicidienase.chaosflix.ChaosflixLoadingSpinner
|
||||
android:id="@+id/chaosflixLoadingSpinner"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/vector_loading_icon"
|
||||
app:duration="2000"/>
|
||||
app:duration="2000"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</FrameLayout>
|
||||
<ProgressBar
|
||||
android:id="@+id/progressbar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal|bottom"
|
||||
android:layout_marginTop="16dp"
|
||||
android:indeterminate="false"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/chaosflixLoadingSpinner"
|
||||
tools:max="10"
|
||||
tools:progress="5"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
<androidx.appcompat.widget.Toolbar
|
||||
android:visibility="gone"
|
||||
android:id="@+id/toolbar"
|
||||
style="@style/ColoredToolbarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
|
|
12
touch/src/main/res/menu/import_menu.xml
Normal file
12
touch/src/main/res/menu/import_menu.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:id="@+id/menu_item_select_all"
|
||||
android:title="@string/select_all"
|
||||
android:icon="@drawable/ic_check_box"
|
||||
app:showAsAction="ifRoom"/>
|
||||
<item android:id="@+id/menu_item_unselect_all"
|
||||
android:title="Select none"
|
||||
android:icon="@drawable/ic_check_box_outline"
|
||||
app:showAsAction="ifRoom"/>
|
||||
</menu>
|
|
@ -54,5 +54,9 @@
|
|||
<string name="play_button">Play-Button</string>
|
||||
<string name="list_of_events">List of Events</string>
|
||||
<string name="delete_item">Delete Item</string>
|
||||
<string name="import_favorites">Import Favorites</string>
|
||||
<string name="import_activity_label">Fahrplan-Import</string>
|
||||
<string name="select_all">Select all</string>
|
||||
<string name="import_label">Bookmark</string>
|
||||
<string name="empty" />
|
||||
</resources>
|
|
@ -6,24 +6,13 @@
|
|||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:windowContentTransitions">true</item>
|
||||
<item name="android:colorPrimary">@color/primary</item>
|
||||
<item name="android:colorPrimaryDark">@color/primary_dark</item>
|
||||
<item name="android:colorAccent">@color/accent</item>
|
||||
<!--<item name="android:textColorPrimary"></item>-->
|
||||
<!--<item name="android:textColorSecondary"></item>-->
|
||||
<!--<item name="android:actionBarStyle">@style/ToolbarStyle</item>-->
|
||||
<!-- <item name="colorControlNormal">@color/white</item>-->
|
||||
<item name="preferenceTheme">@style/SettingsStyle</item>
|
||||
</style>
|
||||
|
||||
<style name="ToolbarStyle" parent="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="android:colorPrimary">@color/primary</item>
|
||||
<item name="colorPrimaryDark">@color/primary_dark</item>
|
||||
<item name="android:colorPrimaryDark">@color/primary_dark</item>
|
||||
<item name="colorAccent">@color/accent</item>
|
||||
<item name="android:textColor">@color/white</item>
|
||||
<item name="android:textColorPrimary">@color/white</item>
|
||||
<item name="android:textColorSecondary">@color/white</item>
|
||||
<item name="colorControlNormal">@color/white</item>
|
||||
<item name="android:colorAccent">@color/accent</item>
|
||||
<item name="preferenceTheme">@style/SettingsStyle</item>
|
||||
</style>
|
||||
|
||||
<style name="SettingsStyle" parent="@style/PreferenceThemeOverlay.v14.Material">
|
||||
|
@ -42,10 +31,6 @@
|
|||
<item name="android:textColorSecondary">@color/white</item>
|
||||
</style>
|
||||
|
||||
<style name="ColoredToolbarStyle" parent="ToolbarStyle">
|
||||
<item name="android:background">@color/primary</item>
|
||||
</style>
|
||||
|
||||
<style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">
|
||||
<item name="android:windowBackground">@drawable/splash_screen</item>
|
||||
</style>
|
||||
|
|
Loading…
Reference in a new issue