Merge remote-tracking branch 'touch/develop'

This commit is contained in:
Felix 2018-09-08 18:02:11 +02:00
commit 388126d665
158 changed files with 7032 additions and 5 deletions

7
.gitignore vendored
View file

@ -1,7 +1,8 @@
*.iml *.iml
.gradle .gradle
/local.properties /local.properties
/.idea /.idea/workspace.xml
/.idea/libraries
.DS_Store .DS_Store
/build /build
/captures /captures
@ -62,7 +63,3 @@ google-services.json
freeline.py freeline.py
freeline/ freeline/
freeline_project_description.json freeline_project_description.json
projectFilesBackup/
.gradletasknamecache

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Felix Bürkle
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

15
README.md Normal file
View file

@ -0,0 +1,15 @@
# Chaosflix
A Android TV / FireTV app to watch content from media.ccc.de
[![Install from Amazon](https://images-na.ssl-images-amazon.com/images/G/01/mobile-apps/devportal2/res/images/amazon-underground-app-us-black.png)](http://www.amazon.com/gp/product/B06Y3GYYGB/ref=mas_pm_chaosflix)
[![Installieren auf Amazon](https://images-na.ssl-images-amazon.com/images/G/01/mobile-apps/devportal2/res/images/amazon-underground-app-de-black.png)](http://www.amazon.de/gp/product/B06Y3GYYGB/ref=mas_pm_chaosflix)
You can get an APK you can install under [Releases](https://github.com/NiciDieNase/chaosflix/releases).
If you don't know how install that on your device, have a look [here](http://www.aftvnews.com/sideload/)
## Screenshots
![screenshot](screenshots/homescreen.png)
![screenshot](screenshots/device-2017-04-06-191834.png)
![screenshot](screenshots/device-2017-04-06-191926.png)
![screenshot](screenshots/device-2017-04-06-192059.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

48
build.gradle Normal file
View file

@ -0,0 +1,48 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.2.30'
repositories {
jcenter()
mavenCentral()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
mavenLocal()
jcenter()
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
google()
maven { url 'https://jitpack.io' }
}
}
ext{
compileSdkVersion = 27
buildToolsVersion = "27.0.3"
supportLibraryVersion = "27.1.1"
constraintLayoutVersion = "1.0.2"
archCompVersion = "1.1.1"
minSDK = 22
targetSDK = 27
}
task clean(type: Delete) {
delete rootProject.buildDir
}
configurations.all {
resolutionStrategy {
cacheChangingModulesFor 0, 'seconds'
}
}

419
logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 149 KiB

1
settings.gradle Normal file
View file

@ -0,0 +1 @@
include ':touch'

1
touch/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

120
touch/build.gradle Normal file
View file

@ -0,0 +1,120 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
applicationId "de.nicidienase.chaosflix"
minSdkVersion rootProject.ext.minSDK
targetSdkVersion rootProject.ext.targetSDK
// odd for touch, even for leanback
versionCode 25
versionName "0.2.9"
multiDexEnabled true
}
buildTypes {
debug {
minifyEnabled false
useProguard false
}
release {
shrinkResources true
minifyEnabled true
useProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/LICENSE'
exclude 'META-INF/license.txt'
exclude 'META-INF/NOTICE'
exclude 'META-INF/notice.txt'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
lintOptions {
abortOnError false
}
dataBinding {
enabled = true
}
buildToolsVersion rootProject.ext.buildToolsVersion
}
dependencies {
// implementation 'com.github.NiciDieNase:chaosflix-common:2.0.0-SNAPSHOT'
implementation 'de.nicidienase.chaosflix:common:2.0.0-SNAPSHOT'
implementation('com.mikepenz:aboutlibraries:6.1.1@aar') {
transitive = true
}
implementation 'com.github.medyo:android-about-page:1.2.2'
implementation 'com.android.support:multidex:1.0.3'
implementation "com.android.support:support-v4:${rootProject.ext.supportLibraryVersion}"
implementation "com.android.support:recyclerview-v7:${rootProject.ext.supportLibraryVersion}"
implementation "com.android.support:cardview-v7:${rootProject.ext.supportLibraryVersion}"
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation "com.android.support:design:${rootProject.ext.supportLibraryVersion}"
implementation "com.android.support:preference-v14:${rootProject.ext.supportLibraryVersion}"
// implementation "android.arch.lifecycle:runtime:1.0.3"
implementation "android.arch.lifecycle:extensions:${rootProject.ext.archCompVersion}"
implementation "android.arch.lifecycle:common-java8:1.1.1"
implementation "android.arch.persistence.room:runtime:${rootProject.ext.archCompVersion}"
// kapt 'com.android.databinding:compiler:3.2.0-alpha10'
kapt "android.arch.lifecycle:compiler:${rootProject.ext.archCompVersion}"
// kapt "android.arch.persistence.room:compiler:${rootProject.ext.archCompVersion}"
implementation "org.jetbrains.kotlin:kotlin-reflect:1.2.61"
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.9.0'
implementation 'com.google.android.exoplayer:exoplayer:r2.5.2'
implementation 'com.squareup.picasso:picasso:2.5.2'
debugImplementation 'com.facebook.stetho:stetho:1.4.2'
debugImplementation 'com.facebook.stetho:stetho-okhttp3:1.4.2'
implementation 'net.opacapp:multiline-collapsingtoolbar:1.6.0'
implementation 'net.rdrei.android.dirchooser:library:3.2@aar'
implementation 'com.gu:option:1.3'
androidTestImplementation('com.android.support.test:rules:0.5') {
exclude module: 'support-annotations'
}
androidTestImplementation('com.android.support.test:runner:0.5') {
exclude module: 'support-annotations'
}
androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'
androidTestImplementation 'org.hamcrest:hamcrest-library:1.3'
testImplementation 'org.mockito:mockito-core:2.11.0'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
}
implementation 'commons-io:commons-io:2.4'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
}
repositories {
mavenCentral()
maven { url 'http://guardian.github.com/maven/repo-releases' }
mavenLocal()
}

69
touch/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,69 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn com.squareup.okhttp.**
# Proguard configuration for Jackson 2.x (fasterxml package instead of codehaus package)
-keep class com.fasterxml.jackson.databind.ObjectMapper {
public <methods>;
protected <methods>;
}
-keep class com.fasterxml.jackson.databind.ObjectWriter {
public ** writeValueAsString(**);
}
-keepnames class com.fasterxml.jackson.** { *; }
-dontwarn com.fasterxml.jackson.databind.**
-keep class org.jetbrains.kotlin.** { *; }
-keep class org.jetbrains.annotations.** { *; }
-keepclassmembers class ** {
@org.jetbrains.annotations.ReadOnly public *;
}
-keepattributes *Annotation*
-keep class kotlin.** { *; }
-keep class org.jetbrains.** { *; }
# Platform used when running on Java 8 VMs. Will not be used at runtime.
-dontwarn retrofit2.Platform$Java8
# Retain generic type information for use by reflection by converters and adapters.
-keepattributes Signature
# Retain declared checked exceptions for use by a Proxy instance.
-keepattributes Exceptions
-dontwarn javax.annotation.**
-dontwarn sun.misc.Unsafe
-dontwarn okio.**
-keep class de.nicidienase.chaosflix.common.entities.** { *; }
#retrofit
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes *Annotation*,Signature, Exceptions
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
#endRetrofit
-keepclassmembers public class com.cypressworks.kotlinreflectionproguard.** {
public *;
}

View file

@ -0,0 +1,30 @@
package de.nicidienase.chaosflix
import android.arch.lifecycle.ViewModelProviders
import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4
import android.test.ActivityInstrumentationTestCase2
import android.view.View
import de.nicidienase.chaosflix.common.entities.userdata.PlaybackProgress
import org.junit.Test
import org.junit.runner.RunWith
import de.nicidienase.chaosflix.touch.ViewModelFactory
import io.reactivex.functions.Consumer
/**
* Created by felix on 31.10.17.
*/
@RunWith(AndroidJUnit4::class)
class PersistenceTest {
@Test
fun test1() {
val playbackProgressDao = ViewModelFactory.database.playbackProgressDao()
playbackProgressDao.saveProgress(PlaybackProgress(23,1337))
playbackProgressDao.getProgressForEvent(23)
.observeForever { it -> assert(it?.eventId == 23L && it?.progress == 1337L) }
}
}

View file

@ -0,0 +1,18 @@
package de.nicidienase.chaosflix.touch
import android.app.Application
import android.content.Context
import com.facebook.stetho.Stetho
class ChaosflixApplication : Application() {
override fun onCreate() {
super.onCreate()
APPLICATION_CONTEXT = this
Stetho.initializeWithDefaults(this)
}
companion object {
lateinit var APPLICATION_CONTEXT: Context
}
}

View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.nicidienase.chaosflix"
tools:ignore="MissingLeanbackLauncher">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:name=".touch.ChaosflixApplication"
android:allowBackup="true"
android:icon="@drawable/icon_notext"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:replace="android:theme">
<activity
android:name=".touch.SplashActivity"
android:label="@string/app_name"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".touch.browse.BrowseActivity">
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity>
<activity
android:name=".touch.browse.eventslist.EventsListActivity"
android:parentActivityName=".touch.browse.BrowseActivity"/>
<activity
android:name=".touch.eventdetails.EventDetailsActivity"
android:parentActivityName="de.nicidienase.chaosflix.touch.browse.eventslist.EventsListActivity"/>
<activity
android:name=".touch.about.AboutActivity"
android:parentActivityName=".touch.browse.BrowseActivity"/>
<activity
android:name=".touch.playback.PlayerActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:launchMode="singleTop"
android:parentActivityName=".touch.eventdetails.EventDetailsActivity"/>
<activity
android:name=".touch.settings.SettingsActivity"
android:parentActivityName=".touch.browse.BrowseActivity"/>
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
<service
android:name=".touch.sync.DownloadJobService"
android:permission="android.permission.BIND_JOB_SERVICE"/>
</application>
</manifest>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
../../../../amazon_icons/icon_small_114x144.png

View file

@ -0,0 +1 @@
../../../../LICENSE

View file

@ -0,0 +1,29 @@
package de.nicidienase.chaosflix.touch
import android.content.Context
import android.util.AttributeSet
import android.view.animation.Animation
import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation
import android.widget.ImageView
import de.nicidienase.chaosflix.R
class ChaosflixLoadingSpinner(context: Context, attributeSet: AttributeSet) : ImageView(context, attributeSet) {
init {
val typedArray = context.theme.obtainStyledAttributes(attributeSet, R.styleable.ChaosflixLoadingSpinner, 0, 0)
val duration = typedArray.getInt(R.styleable.ChaosflixLoadingSpinner_duration, 2000)
val clockwise = typedArray.getBoolean(R.styleable.ChaosflixLoadingSpinner_clockwise, true)
val anim = if (clockwise) {
RotateAnimation(0.0f, 360.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
} else {
RotateAnimation(360.0f, 0.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
}
anim.interpolator = LinearInterpolator()
anim.duration = duration.toLong()
anim.repeatCount = Animation.INFINITE
animation = anim
}
}

View file

@ -0,0 +1,17 @@
package de.nicidienase.chaosflix.touch
import android.arch.persistence.room.Room
import android.content.Context
import de.nicidienase.chaosflix.common.ChaosflixDatabase
class DatabaseFactory (context: Context) {
val database = Room.databaseBuilder(
context.applicationContext,
ChaosflixDatabase::class.java, "mediaccc.de")
.addMigrations(
ChaosflixDatabase.migration_2_3,
ChaosflixDatabase.migration_3_4,
ChaosflixDatabase.migration_4_5)
.fallbackToDestructiveMigration()
.build()
}

View file

@ -0,0 +1,205 @@
package de.nicidienase.chaosflix.touch
import android.app.DownloadManager
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MutableLiveData
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.database.sqlite.SQLiteConstraintException
import android.databinding.ObservableField
import android.net.Uri
import android.os.Environment
import android.preference.PreferenceManager
import android.util.Log
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentRecording
import de.nicidienase.chaosflix.common.userdata.entities.download.OfflineEvent
import de.nicidienase.chaosflix.common.userdata.entities.download.OfflineEventDao
import de.nicidienase.chaosflix.common.util.ThreadHandler
import de.nicidienase.chaosflix.touch.eventdetails.DetailsViewModel
import java.io.File
class OfflineItemManager(downloadRefs: List<Long>? = emptyList(),val offlineEventDao: OfflineEventDao) {
val downloadStatus: MutableMap<Long, DownloadStatus>
val downloadManager: DownloadManager
= ChaosflixApplication.APPLICATION_CONTEXT
.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
private val handler = ThreadHandler()
init {
downloadStatus = HashMap()
downloadRefs?.map { downloadStatus.put(it, DownloadStatus()) }
}
fun updateDownloadStatus() {
updateDownloads(downloadStatus.keys.toLongArray())
}
fun updateDownloadStatus(offlineEvents: List<OfflineEvent>) {
if (offlineEvents.size > 0) {
val downloadRef = offlineEvents.map { it.downloadReference }.toTypedArray().toLongArray() ?: longArrayOf()
updateDownloads(downloadRef)
}
}
fun updateDownloads(downloadRefs: LongArray) {
val cursor = downloadManager.query(DownloadManager.Query().setFilterById(*downloadRefs))
if (cursor.moveToFirst()) {
do {
val columnId = cursor.getColumnIndex(DownloadManager.COLUMN_ID)
val id = cursor.getLong(columnId)
val columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
val status = cursor.getInt(columnIndex)
val bytesSoFarIndex = cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
val bytesSoFar = cursor.getInt(bytesSoFarIndex)
val bytesTotalIndex = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
val bytesTotal = cursor.getInt(bytesTotalIndex)
val statusText: String =
when (status) {
DownloadManager.STATUS_RUNNING -> "Running"
DownloadManager.STATUS_FAILED -> "Failed"
DownloadManager.STATUS_PAUSED -> "Paused"
DownloadManager.STATUS_SUCCESSFUL -> "Successful"
DownloadManager.STATUS_PENDING -> "Pending"
else -> "UNKNOWN"
}
if (downloadStatus.containsKey(id)) {
val item = downloadStatus[id]
item?.statusText?.set(statusText)
item?.currentBytes?.set(bytesSoFar)
item?.totalBytes?.set(bytesTotal)
item?.status = status
} else {
downloadStatus.put(
id,
DownloadStatus(statusText, bytesSoFar, bytesTotal, status))
}
} while (cursor.moveToNext())
}
}
fun download(event: PersistentEvent, recording: PersistentRecording): LiveData<Boolean> {
val result = MutableLiveData<Boolean>()
handler.runOnBackgroundThread {
val offlineEvent = offlineEventDao.getByEventGuidSync(event.guid)
if (offlineEvent == null) {
val downloadManager: DownloadManager
= ChaosflixApplication.APPLICATION_CONTEXT
.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(Uri.parse(recording.recordingUrl))
request.setTitle(event.title)
request.setDestinationUri(
Uri.withAppendedPath(Uri.fromFile(
File(getDownloadDir())), recording.filename))
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
request.setVisibleInDownloadsUi(true)
if(!PreferencesManager.getMetered()){
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI)
request.setAllowedOverMetered(false)
}
val downloadReference = downloadManager.enqueue(request)
Log.d(DetailsViewModel.TAG, "download started $downloadReference")
val cancelHandler = DownloadCancelHandler(downloadReference, offlineEventDao)
val intentFilter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
ChaosflixApplication.APPLICATION_CONTEXT.registerReceiver(cancelHandler, intentFilter)
try {
offlineEventDao.insert(
OfflineEvent(eventGuid = event.guid,
recordingId = recording.id,
localPath = getDownloadDir() + recording.filename,
downloadReference = downloadReference))
} catch (ex: SQLiteConstraintException) {
Log.d(DetailsViewModel.TAG, ex.message)
}
result.postValue(true)
}
}
return result
}
fun deleteOfflineItem(downloadId: Long) {
val offlineEvent = offlineEventDao.getByDownloadReferenceSync(downloadId)
if (offlineEvent != null) {
deleteOfflineItem(offlineEvent)
}
}
fun deleteOfflineItem(item: OfflineEvent) {
downloadManager.remove(item.downloadReference)
val file = File(item.localPath)
if (file.exists()) file.delete()
offlineEventDao.deleteById(item.id)
}
inner class DownloadStatus(statusText: String = "",
currentBytes: Int = 0,
totalBytes: Int = 0,
var status: Int = DownloadManager.STATUS_FAILED) {
val statusText: ObservableField<String> = ObservableField()
val currentBytes: ObservableField<Int> = ObservableField()
val totalBytes: ObservableField<Int> = ObservableField()
init {
this.statusText.set(statusText)
this.currentBytes.set(currentBytes)
this.totalBytes.set(totalBytes)
}
}
class DownloadCancelHandler(val id: Long, val offlineEventDao: OfflineEventDao) : BroadcastReceiver() {
private val TAG = DownloadCancelHandler::class.simpleName
val handler = ThreadHandler()
override fun onReceive(p0: Context?, p1: Intent?) {
val downloadId = p1?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0);
if (downloadId != null && downloadId == id) {
val offlineItemManager = OfflineItemManager(listOf(downloadId),offlineEventDao)
offlineItemManager.updateDownloadStatus()
val downloadStatus = offlineItemManager.downloadStatus[downloadId]
if (downloadStatus?.status == DownloadManager.STATUS_FAILED) {
Log.d(TAG, "Deleting item")
handler.runOnBackgroundThread {
offlineItemManager.deleteOfflineItem(downloadId)
}
}
p0?.unregisterReceiver(this);
}
}
}
private fun getMovieDir(): String {
val sharedPref: SharedPreferences = PreferenceManager
.getDefaultSharedPreferences(ChaosflixApplication.APPLICATION_CONTEXT);
var dir = sharedPref.getString("download_folder", null)
if (dir == null) {
dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).path
}
return dir
}
private fun getDownloadDir(): String {
return getMovieDir() + DOWNLOAD_DIR;
}
companion object {
val DOWNLOAD_DIR = "/chaosflix/"
}
}

View file

@ -0,0 +1,7 @@
package de.nicidienase.chaosflix.touch
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
interface OnEventSelectedListener {
fun onEventSelected(event: PersistentEvent);
}

View file

@ -0,0 +1,19 @@
package de.nicidienase.chaosflix.touch
import android.content.SharedPreferences
import android.preference.PreferenceManager
object PreferencesManager {
private val keyMetered = "allow_metered_networks"
private val keyAutoselectStream = "auto_select_stream"
private val keyAutoselectRecording = "auto_select_recording"
val sharedPref: SharedPreferences = PreferenceManager
.getDefaultSharedPreferences(ChaosflixApplication.APPLICATION_CONTEXT)
fun getMetered() = sharedPref.getBoolean(keyMetered, false)
fun getAutoselectStream() = sharedPref.getBoolean(keyAutoselectStream, false)
fun getAutoselectRecording() = sharedPref.getBoolean(keyAutoselectRecording, false)
}

View file

@ -0,0 +1,16 @@
package de.nicidienase.chaosflix.touch
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import de.nicidienase.chaosflix.touch.browse.BrowseActivity
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(Intent(this, BrowseActivity::class.java))
finish()
}
}

View file

@ -0,0 +1,38 @@
package de.nicidienase.chaosflix.touch
import android.arch.lifecycle.ViewModel
import android.arch.lifecycle.ViewModelProvider
import android.content.Context
import de.nicidienase.chaosflix.common.mediadata.network.ApiFactory
import de.nicidienase.chaosflix.touch.browse.BrowseViewModel
import de.nicidienase.chaosflix.touch.eventdetails.DetailsViewModel
import de.nicidienase.chaosflix.touch.playback.PlayerViewModel
class ViewModelFactory(context: Context) : ViewModelProvider.Factory {
val apiFactory = ApiFactory(context.resources)
val database = DatabaseFactory(context).database
val recordingApi = apiFactory.recordingApi
val streamingApi = apiFactory.streamingApi
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(BrowseViewModel::class.java)) {
return BrowseViewModel(database, recordingApi, streamingApi) as T
} else if (modelClass.isAssignableFrom(PlayerViewModel::class.java)) {
return PlayerViewModel(database) as T
} else if (modelClass.isAssignableFrom(DetailsViewModel::class.java)) {
return DetailsViewModel(database, recordingApi) as T
} else {
throw UnsupportedOperationException("The requested ViewModel is currently unsupported. " +
"Please make sure to implement are correct creation of it. " +
" Request: ${modelClass.getCanonicalName()}");
}
}
}

View file

@ -0,0 +1,55 @@
package de.nicidienase.chaosflix.touch.about
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.View
import de.nicidienase.chaosflix.R
import de.nicidienase.chaosflix.databinding.ActivityAboutBinding
import mehdi.sakout.aboutpage.AboutPage
import mehdi.sakout.aboutpage.Element
class AboutActivity : AppCompatActivity() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityAboutBinding>(
this, R.layout.activity_about)
binding.toolbarInc?.toolbar?.title = getString(R.string.about_chaosflix)
setSupportActionBar(binding.toolbarInc?.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val showLibs = Element()
showLibs.title = resources.getString(R.string.showLibs)
showLibs.onClickListener = object : View.OnClickListener {
override fun onClick(p0: View?) {
LibsFragment().show(supportFragmentManager, null)
}
}
val pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
val version = pInfo.versionName;
val aboutView = AboutPage(this)
.setImage(R.drawable.icon_notext_144x144)
.setDescription(resources.getString(R.string.description))
.addItem(Element().setTitle("Version ${version}"))
.addWebsite("https://github.com/NiciDieNase/chaosflix/blob/master/LICENSE",
getString(R.string.chaosflix_licence))
.addWebsite("https://morr.cc/voctocat/",
resources.getString(R.string.about_voctocat))
.addItem(showLibs)
.addGroup("Connect with us")
.addGitHub("nicidienase/chaosflix", "Find the source on Github")
.addTwitter("nicidienase", "Follow the developer on Twitter")
.addPlayStore("de.nicidienase.chaosflix", "Rate us on Google Play")
.create()
binding.container.addView(aboutView)
}
}

View file

@ -0,0 +1,29 @@
package de.nicidienase.chaosflix.touch.about
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.mikepenz.aboutlibraries.LibsBuilder
import com.mikepenz.aboutlibraries.ui.LibsSupportFragment
import de.nicidienase.chaosflix.R
class LibsFragment: DialogFragment(){
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val layout = inflater.inflate(R.layout.fragment_libs,container, false)
childFragmentManager.beginTransaction()
.replace(R.id.layout_container,getLibsFragment())
.commit()
return layout
}
private fun getLibsFragment(): LibsSupportFragment {
val aboutLibs: LibsSupportFragment = LibsBuilder()
// .withAboutIconShown(true)
// .withAboutVersionShown(true)
// .withAboutDescription(resources.getString(R.string.description))
.supportFragment()
return aboutLibs
}
}

View file

@ -0,0 +1,245 @@
package de.nicidienase.chaosflix.touch.browse
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.res.Configuration
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.os.PersistableBundle
import android.support.design.widget.NavigationView
import android.support.design.widget.Snackbar
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentTransaction
import android.support.v7.app.ActionBarDrawerToggle
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.transition.TransitionInflater
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import de.nicidienase.chaosflix.R
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentConference
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.StreamUrl
import de.nicidienase.chaosflix.databinding.ActivityBrowseBinding
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
import de.nicidienase.chaosflix.touch.PreferencesManager
import de.nicidienase.chaosflix.touch.about.AboutActivity
import de.nicidienase.chaosflix.touch.browse.download.DownloadsListFragment
import de.nicidienase.chaosflix.touch.browse.eventslist.EventsListActivity
import de.nicidienase.chaosflix.touch.browse.eventslist.EventsListFragment
import de.nicidienase.chaosflix.touch.browse.streaming.LivestreamListFragment
import de.nicidienase.chaosflix.touch.browse.streaming.StreamingItem
import de.nicidienase.chaosflix.touch.eventdetails.EventDetailsActivity
import de.nicidienase.chaosflix.touch.playback.PlayerActivity
import de.nicidienase.chaosflix.touch.settings.SettingsActivity
class BrowseActivity : AppCompatActivity(),
ConferencesTabBrowseFragment.OnInteractionListener,
LivestreamListFragment.InteractionListener,
DownloadsListFragment.InteractionListener,
OnEventSelectedListener {
private var drawerOpen: Boolean = false
private val TAG = BrowseActivity::class.simpleName
private lateinit var drawerToggle: ActionBarDrawerToggle
private lateinit var binding: ActivityBrowseBinding
protected val numColumns: Int
get() = resources.getInteger(R.integer.num_columns)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_browse)
val navigationView = findViewById<NavigationView>(R.id.navigation_view)
navigationView.setNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.nav_recordings -> showConferencesFragment()
R.id.nav_bookmarks -> showBookmarksFragment()
R.id.nav_inprogress -> showInProgressFragment()
R.id.nav_about -> showAboutPage()
R.id.nav_streams -> showStreamsFragment()
R.id.nav_downloads -> showDownloadsFragment()
R.id.nav_preferences -> showSettingsPage()
else -> Snackbar.make(binding.drawerLayout, "Not implemented yet", Snackbar.LENGTH_SHORT).show()
}
binding.drawerLayout.closeDrawers()
true
}
if (savedInstanceState == null) {
showConferencesFragment()
}
}
fun setupDrawerToggle(toolbar: Toolbar?) {
if (toolbar != null) {
drawerToggle = object : ActionBarDrawerToggle(this, binding.drawerLayout,
toolbar, R.string.drawer_open, R.string.drawer_close) {
override fun onDrawerOpened(drawerView: View) {
super.onDrawerOpened(drawerView)
drawerOpen = true
}
override fun onDrawerClosed(drawerView: View) {
super.onDrawerClosed(drawerView)
drawerOpen = false
}
}
} else {
drawerToggle = object : ActionBarDrawerToggle(this, binding.drawerLayout,
R.string.drawer_open, R.string.drawer_close) {
override fun onDrawerOpened(drawerView: View) {
super.onDrawerOpened(drawerView)
drawerOpen = true
}
override fun onDrawerClosed(drawerView: View) {
super.onDrawerClosed(drawerView)
drawerOpen = false
}
}
}
binding.drawerLayout.addDrawerListener(drawerToggle)
drawerToggle.syncState()
}
override fun onPostCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onPostCreate(savedInstanceState, persistentState)
drawerToggle.syncState()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
drawerToggle.onConfigurationChanged(newConfig)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (drawerToggle.onOptionsItemSelected(item)) {
return true
}
return super.onOptionsItemSelected(item)
}
override fun onConferenceSelected(conference: PersistentConference) {
EventsListActivity.start(this, conference)
}
override fun onStreamSelected(streamingItem: StreamingItem) {
val entries = HashMap<String, StreamUrl>()
val dashStreams = streamingItem.room.streams.filter { it.slug == "dash-native" }
if (dashStreams.size > 0
&& PreferencesManager.getAutoselectStream()) {
playStream(streamingItem.conference.conference,
streamingItem.room.display,
dashStreams.first().urls["dash"]
)
} else {
streamingItem.room.streams.flatMap { stream ->
stream.urls.map { entry ->
entries.put(stream.slug + " " + entry.key, entry.value)
}
}
val builder = AlertDialog.Builder(this)
val strings = entries.keys.sorted().toTypedArray()
builder.setTitle("Select Stream")
.setItems(strings, { _, i ->
Toast.makeText(this, strings[i], Toast.LENGTH_LONG).show()
playStream(
streamingItem.conference.conference,
streamingItem.room.display,
entries[strings[i]])
})
builder.create().show()
}
}
private fun playStream(conference: String, room: String, streamUrl: StreamUrl?) {
if (streamUrl != null) {
PlayerActivity.launch(this, conference, room, streamUrl)
}
}
private fun showConferencesFragment() {
showFragment(ConferencesTabBrowseFragment.newInstance(numColumns), "conferences")
}
private fun showBookmarksFragment() {
val bookmarksFragment = EventsListFragment.newInstance(EventsListFragment.TYPE_BOOKMARKS, null, numColumns)
showFragment(bookmarksFragment, "bookmarks")
}
private fun showInProgressFragment() {
val progressEventsFragment = EventsListFragment.newInstance(EventsListFragment.TYPE_IN_PROGRESS, null, numColumns)
showFragment(progressEventsFragment, "in_progress")
}
private fun showStreamsFragment() {
val fragment = LivestreamListFragment.newInstance(numColumns)
showFragment(fragment, "streams")
}
private fun showDownloadsFragment() {
val fragment = DownloadsListFragment.getInstance(numColumns)
showFragment(fragment, "downloads")
}
private fun showSettingsPage() {
val intent = Intent(this, SettingsActivity::class.java)
startActivity(intent)
}
private fun showAboutPage() {
val intent = Intent(this, AboutActivity::class.java)
startActivity(intent)
}
override fun onBackPressed() {
if (drawerOpen) {
binding.drawerLayout.closeDrawers()
} else {
super.onBackPressed()
}
}
protected fun showFragment(fragment: Fragment, tag: String) {
val fm = supportFragmentManager
val oldFragment = fm.findFragmentById(R.id.fragment_container)
val transitionInflater = TransitionInflater.from(this)
if (oldFragment != null) {
if (oldFragment.tag.equals(tag)) {
return
}
oldFragment.exitTransition = transitionInflater.inflateTransition(android.R.transition.fade)
}
fragment.enterTransition = transitionInflater.inflateTransition(android.R.transition.fade)
// val slideTransition = Slide(Gravity.RIGHT)
// fragment.enterTransition = slideTransition
val ft = fm.beginTransaction()
ft.replace(R.id.fragment_container, fragment, tag)
ft.setReorderingAllowed(true)
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
ft.commit()
}
override fun onEventSelected(event: PersistentEvent) {
EventDetailsActivity.launch(this, event)
}
companion object {
fun launch(context: Context){
context.startActivity(Intent(context,BrowseActivity::class.java))
}
}
}

View file

@ -0,0 +1,48 @@
package de.nicidienase.chaosflix.touch.browse
import android.arch.lifecycle.ViewModelProviders
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.View
import de.nicidienase.chaosflix.touch.ViewModelFactory
open class BrowseFragment : Fragment() {
lateinit var viewModel: BrowseViewModel
var overlay: View? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(activity!!, ViewModelFactory(requireContext())).get(BrowseViewModel::class.java)
}
@JvmOverloads
protected fun setupToolbar(toolbar: Toolbar, title: Int, isRoot: Boolean = true) {
setupToolbar(toolbar, resources.getString(title), isRoot)
}
@JvmOverloads
protected fun setupToolbar(toolbar: Toolbar, title: String, isRoot: Boolean = true) {
val activity = activity as AppCompatActivity
if (activity is BrowseActivity) {
activity.setupDrawerToggle(toolbar)
}
activity.setSupportActionBar(toolbar)
activity.supportActionBar?.setTitle(title)
if (isRoot) {
activity.supportActionBar?.setDisplayShowHomeEnabled(true)
} else {
activity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
}
protected fun setLoadingOverlayVisibility(visible: Boolean) {
if (visible) {
overlay?.setVisibility(View.VISIBLE)
} else {
overlay?.setVisibility(View.INVISIBLE)
}
}
}

View file

@ -0,0 +1,122 @@
package de.nicidienase.chaosflix.touch.browse
class BrowseViewModel(
val database: ChaosflixDatabase,
recordingApi: RecordingService,
val streamingApi: StreamingService
) : ViewModel() {
val downloader = Downloader(recordingApi, database)
lateinit var offlineItemManager: OfflineItemManager
private val handler = ThreadHandler()
init {
handler.runOnBackgroundThread {
val downloadRefs =
database
.offlineEventDao()
.getAllSync()
.map { it.downloadReference }
offlineItemManager = OfflineItemManager(downloadRefs, database.offlineEventDao())
}
}
fun getConferenceGroups(): LiveData<List<ConferenceGroup>> {
downloader.updateConferencesAndGroups()
return database.conferenceGroupDao().getAll()
}
fun getConference(conferenceId: Long)
= database.conferenceDao().findConferenceById(conferenceId)
fun getConferencesByGroup(groupId: Long)
= database.conferenceDao().findConferenceByGroup(groupId)
fun getEventsforConference(conference: PersistentConference)
= database.eventDao().findEventsByConference(conference.id)
fun updateConferences()
= downloader.updateConferencesAndGroups()
fun updateEventsForConference(conference: PersistentConference)
= downloader.updateEventsForConference(conference)
fun getBookmarkedEvents(){
// database.
// = database.eventDao().findBookmarkedEvents()
}
fun getInProgressEvents()
= database.eventDao().findInProgressEvents()
private val TAG = BrowseViewModel::class.simpleName
fun getLivestreams(): LiveData<List<LiveConference>> {
val result = MutableLiveData<List<LiveConference>>()
handler.runOnBackgroundThread {
val conferences = streamingApi.getStreamingConferences().execute()
if(!conferences.isSuccessful){
result.postValue(emptyList())
return@runOnBackgroundThread
}
result.postValue(conferences.body())
}
return result
}
fun getOfflineEvents(): LiveData<List<Pair<OfflineEvent,PersistentEvent>>> {
val result = MutableLiveData<List<Pair<OfflineEvent, PersistentEvent>>>()
handler.runOnBackgroundThread {
val offlineEventMap = database.offlineEventDao().getAllSync()
.map { it.eventGuid to it }.toMap()
val persistentEventMap = database.eventDao().findEventsByGUIDsSync(offlineEventMap.keys.toList())
.map { it.guid to it }.toMap()
val resultList = ArrayList<Pair<OfflineEvent, PersistentEvent>>()
for (key in offlineEventMap.keys){
val offlineEvent = offlineEventMap[key]
var persistentEvent: PersistentEvent? = persistentEventMap[key]
if(persistentEvent == null){
persistentEvent = downloader.updateSingleEvent(key)
}
if(persistentEvent != null && offlineEvent != null){
resultList.add(Pair(offlineEvent, persistentEvent))
}
}
result.postValue(resultList)
}
return result
}
fun getEventById(eventId: Long) = database.eventDao().findEventById(eventId)
fun getRecordingByid(recordingId: Long) = database.recordingDao().findRecordingById(recordingId)
fun updateDownloadStatus() {
handler.runOnBackgroundThread {
offlineItemManager.updateDownloadStatus(database.offlineEventDao().getAllSync())
}
}
fun deleteOfflineItem(item: OfflineEvent) {
handler.runOnBackgroundThread {
offlineItemManager.deleteOfflineItem(item)
}
}
}
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.ViewModel
import de.nicidienase.chaosflix.common.ChaosflixDatabase
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.ConferenceGroup
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentConference
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.LiveConference
import de.nicidienase.chaosflix.common.mediadata.network.RecordingService
import de.nicidienase.chaosflix.common.mediadata.network.StreamingService
import de.nicidienase.chaosflix.common.mediadata.sync.Downloader
import de.nicidienase.chaosflix.common.userdata.entities.download.OfflineEvent
import de.nicidienase.chaosflix.common.util.ThreadHandler
import de.nicidienase.chaosflix.touch.OfflineItemManager

View file

@ -0,0 +1,118 @@
package de.nicidienase.chaosflix.touch.browse;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import de.nicidienase.chaosflix.R;
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.ConferenceGroup;
import de.nicidienase.chaosflix.touch.browse.adapters.ConferenceRecyclerViewAdapter;
public class ConferenceGroupFragment extends BrowseFragment {
private static final String TAG = ConferenceGroupFragment.class.getSimpleName();
private static final String ARG_COLUMN_COUNT = "column-count";
private static final String ARG_GROUP = "group-name";
private static final String LAYOUTMANAGER_STATE = "layoutmanager-state";
private ConferencesTabBrowseFragment.OnInteractionListener listener;
private int columnCount = 1;
private ConferenceGroup conferenceGroup;
private ConferenceRecyclerViewAdapter conferencesAdapter;
private RecyclerView.LayoutManager layoutManager;
public ConferenceGroupFragment() {
}
public static ConferenceGroupFragment newInstance(ConferenceGroup group, int columnCount) {
ConferenceGroupFragment fragment = new ConferenceGroupFragment();
Bundle args = new Bundle();
args.putInt(ARG_COLUMN_COUNT, columnCount);
args.putParcelable(ARG_GROUP, group);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
columnCount = getArguments().getInt(ARG_COLUMN_COUNT);
conferenceGroup = getArguments().getParcelable(ARG_GROUP);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_conferences_page, container, false);
if (view instanceof RecyclerView) {
Context context = view.getContext();
RecyclerView recyclerView = (RecyclerView) view;
if (columnCount <= 1) {
layoutManager = new LinearLayoutManager(context);
} else {
layoutManager = new GridLayoutManager(context, columnCount);
}
recyclerView.setLayoutManager(layoutManager);
conferencesAdapter = new ConferenceRecyclerViewAdapter(listener);
recyclerView.setAdapter(conferencesAdapter);
getViewModel().getConferencesByGroup(conferenceGroup.getId()).observe(this, conferenceList -> {
if(conferenceList != null){
if(conferenceList.size() > 0){
setLoadingOverlayVisibility(false);
}
conferencesAdapter.setItems(conferenceList);
Parcelable layoutState = getArguments().getParcelable(LAYOUTMANAGER_STATE);
if (layoutState != null) {
layoutManager.onRestoreInstanceState(layoutState);
}
}
});
}
return view;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof ConferencesTabBrowseFragment.OnInteractionListener) {
listener = (ConferencesTabBrowseFragment.OnInteractionListener) context;
} else {
throw new RuntimeException(context.toString() + " must implement OnListFragmentInteractionListener");
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if(layoutManager != null){
outState.putParcelable(LAYOUTMANAGER_STATE, layoutManager.onSaveInstanceState());
}
}
@Override
public void onPause() {
super.onPause();
if(layoutManager != null){
getArguments().putParcelable(LAYOUTMANAGER_STATE, layoutManager.onSaveInstanceState());
}
}
@Override
public void onDetach() {
super.onDetach();
listener = null;
}
}

View file

@ -0,0 +1,124 @@
package de.nicidienase.chaosflix.touch.browse;
import android.content.Context;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.jetbrains.annotations.NotNull;
import de.nicidienase.chaosflix.R;
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentConference;
import de.nicidienase.chaosflix.databinding.FragmentTabPagerLayoutBinding;
import de.nicidienase.chaosflix.touch.browse.adapters.ConferenceGroupsFragmentPager;
public class ConferencesTabBrowseFragment extends BrowseFragment {
private static final String TAG = ConferencesTabBrowseFragment.class.getSimpleName();
private static final String ARG_COLUMN_COUNT = "column-count";
private static final String CURRENTTAB_KEY = "current_tab";
private static final String VIEWPAGER_STATE = "viewpager_state";
private int mColumnCount = 1;
private OnInteractionListener listener;
private FragmentTabPagerLayoutBinding binding;
private Snackbar snackbar;
public static ConferencesTabBrowseFragment newInstance(int columnCount) {
ConferencesTabBrowseFragment fragment = new ConferencesTabBrowseFragment();
Bundle args = new Bundle();
args.putInt(ARG_COLUMN_COUNT, columnCount);
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof OnInteractionListener) {
listener = (OnInteractionListener) context;
} else {
throw new RuntimeException(context.toString() + " must implement OnInteractionListener");
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate");
if (getArguments() != null) {
mColumnCount = getArguments().getInt(ARG_COLUMN_COUNT);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentTabPagerLayoutBinding.inflate(inflater, container, false);
setupToolbar(binding.incToolbar.toolbar, R.string.app_name);
setOverlay(binding.incOverlay.loadingOverlay);
getViewModel().getConferenceGroups().observe(this, conferenceGroups -> {
ConferenceGroupsFragmentPager fragmentPager = new ConferenceGroupsFragmentPager(this.getContext(), getChildFragmentManager());
fragmentPager.setContent(conferenceGroups);
binding.viewpager.setAdapter(fragmentPager);
binding.viewpager.onRestoreInstanceState(getArguments().getParcelable(VIEWPAGER_STATE));
binding.slidingTabs.setupWithViewPager(binding.viewpager);
if (conferenceGroups.size() > 0) {
setLoadingOverlayVisibility(false);
}
});
getViewModel().updateConferences().observe(this, state -> {
if(state == null){
return;
}
switch (state.getState()){
case RUNNING:
setLoadingOverlayVisibility(true);
break;
case DONE:
setLoadingOverlayVisibility(false);
break;
}
if(state.getError() != null){
showSnackbar(state.getError());
}
});
return binding.getRoot();
}
private void showSnackbar(String message) {
View view1 = getView();
if(snackbar!= null){
snackbar.dismiss();
}
if(view1 != null){
snackbar = Snackbar.make(view1, message, Snackbar.LENGTH_LONG);
snackbar.setAction("Okay", view -> snackbar.dismiss());
snackbar.show();
}
}
@Override
public void onPause() {
super.onPause();
getArguments().putParcelable(VIEWPAGER_STATE, binding.viewpager.onSaveInstanceState());
}
@Override
public void onDetach() {
super.onDetach();
listener = null;
}
public interface OnInteractionListener {
void onConferenceSelected(PersistentConference conference);
}
}

View file

@ -0,0 +1,64 @@
package de.nicidienase.chaosflix.touch.browse.adapters;
import android.content.Context;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import de.nicidienase.chaosflix.R;
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.ConferenceGroup;
import de.nicidienase.chaosflix.touch.browse.ConferenceGroupFragment;
public class ConferenceGroupsFragmentPager extends FragmentPagerAdapter {
private static final String TAG = ConferenceGroupsFragmentPager.class.getSimpleName();
private final Context mContext;
private List<ConferenceGroup> conferenceGroupList = new ArrayList<>();
public ConferenceGroupsFragmentPager(Context context, FragmentManager fm) {
super(fm);
this.mContext = context;
}
@Override
public Fragment getItem(int position) {
ConferenceGroup conferenceGroup = conferenceGroupList.get(position);
ConferenceGroupFragment conferenceFragment = ConferenceGroupFragment.newInstance(conferenceGroup, getNumColumns());
Log.d(TAG, "Created Fragment for: " + conferenceGroup.getName());
return conferenceFragment;
}
@Override
public long getItemId(int position) {
return conferenceGroupList.get(position).getId();
}
@Override
public int getCount() {
return conferenceGroupList.size();
}
@Override
public CharSequence getPageTitle(int position) {
return conferenceGroupList.get(position).getName();
}
private int getNumColumns() {
return mContext.getResources().getInteger(R.integer.num_columns);
}
public void addGroup(ConferenceGroup conferenceGroup) {
conferenceGroupList.add(conferenceGroup);
// Collections.sort(conferenceGroupList,(g1, g2) -> g1.getIndex()-g2.getIndex());
notifyDataSetChanged();
}
public void setContent(List<ConferenceGroup> conferenceGroups) {
conferenceGroupList = conferenceGroups;
notifyDataSetChanged();
}
}

View file

@ -0,0 +1,34 @@
package de.nicidienase.chaosflix.touch.browse.adapters
import com.squareup.picasso.Picasso
import de.nicidienase.chaosflix.R
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentConference
import de.nicidienase.chaosflix.touch.browse.ConferencesTabBrowseFragment
import java.util.*
class ConferenceRecyclerViewAdapter(private val mListener: ConferencesTabBrowseFragment.OnInteractionListener?) : ItemRecyclerViewAdapter<PersistentConference>() {
override fun getFilteredProperties(item: PersistentConference): List<String> {
return listOf(item.title)
}
override val layout = R.layout.item_conference_cardview
override fun getComparator(): Comparator<in PersistentConference>? {
// return Comparator { o1, o2 -> o1.acronym.compareTo(o2.acronym) * -1 }
return null
}
override fun onBindViewHolder(holder: ItemRecyclerViewAdapter<PersistentConference>.ViewHolder, position: Int) {
holder.titleText.setText(items[position].title)
holder.subtitle.setText(items[position].acronym)
Picasso.with(holder.icon.context)
.load(items[position].logoUrl)
.fit()
.centerInside()
.into(holder.icon)
holder.mView.setOnClickListener { _ ->
mListener?.onConferenceSelected((items[position]))
}
}
}

View file

@ -0,0 +1,62 @@
package de.nicidienase.chaosflix.touch.browse.adapters
import android.support.v4.view.ViewCompat
import android.view.View
import com.squareup.picasso.Picasso
import de.nicidienase.chaosflix.R
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
import java.util.*
open class EventRecyclerViewAdapter(val listener: OnEventSelectedListener) :
ItemRecyclerViewAdapter<PersistentEvent>() {
override fun getComparator(): Comparator<in PersistentEvent>? {
return Comparator { o1, o2 -> o1.title.compareTo(o2.title) }
}
override fun getFilteredProperties(item: PersistentEvent): List<String> {
return listOf(item.title,
item.subtitle,
item.description,
item.getSpeakerString()
).filterNotNull()
}
override val layout = R.layout.item_event_cardview
var showTags: Boolean = false
override fun onBindViewHolder(holder: ItemRecyclerViewAdapter<PersistentEvent>.ViewHolder, position: Int) {
val event = items[position]
holder.titleText.text = event.title
holder.subtitle.text = event.subtitle
if (showTags) {
val tagString = StringBuilder()
for (tag in event.tags!!) {
if (tagString.length > 0) {
tagString.append(", ")
}
tagString.append(tag)
}
holder.tag.text = tagString
}
Picasso.with(holder.icon.context)
.load(event.thumbUrl)
.noFade()
.fit()
.centerInside()
.into(holder.icon)
val resources = holder.titleText.context.getResources()
ViewCompat.setTransitionName(holder.titleText,
resources.getString(R.string.title) + event.id)
ViewCompat.setTransitionName(holder.subtitle,
resources.getString(R.string.subtitle) + event.id)
ViewCompat.setTransitionName(holder.icon,
resources.getString(R.string.thumbnail) + event.id)
holder.mView.setOnClickListener({ _: View -> listener.onEventSelected(items[position]) })
}
}

View file

@ -0,0 +1,95 @@
package de.nicidienase.chaosflix.touch.browse.adapters
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Filter
import android.widget.Filterable
import android.widget.ImageView
import android.widget.TextView
import de.nicidienase.chaosflix.R
import java.util.*
abstract class ItemRecyclerViewAdapter<T>()
: RecyclerView.Adapter<ItemRecyclerViewAdapter<T>.ViewHolder>(), Filterable {
internal abstract val layout: Int
abstract fun getComparator(): Comparator<in T>?
internal abstract fun getFilteredProperties(item: T): List<String>
private val _filter by lazy { ItemFilter() }
override fun getFilter(): Filter {
return _filter
}
private var _items: MutableList<T> = ArrayList<T>()
private var filteredItems: MutableList<T> = _items
var items: MutableList<T>
get() = filteredItems
set(value) {
_items = value
if (getComparator() != null) {
Collections.sort(_items, getComparator())
}
filteredItems = _items
notifyDataSetChanged()
}
fun addItem(item: T) {
if (items.contains(item)) {
val index = items.indexOf(item)
items[index] = item
notifyItemChanged(index)
} else {
items.add(item)
notifyItemInserted(items.size - 1)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(layout, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int {
return filteredItems.size
}
inner class ViewHolder(val mView: View) : RecyclerView.ViewHolder(mView) {
val icon: ImageView = mView.findViewById<View>(R.id.imageView) as ImageView
val titleText: TextView = mView.findViewById<View>(R.id.title_text) as TextView
val subtitle: TextView = mView.findViewById<View>(R.id.subtitle_text) as TextView
val tag: TextView = mView.findViewById<View>(R.id.tag_text) as TextView
}
inner class ItemFilter : Filter() {
override fun performFiltering(filterText: CharSequence?): FilterResults {
val filterResults = FilterResults()
filterText?.let { text: CharSequence ->
if (text.length > 0) {
val list = _items.filter { getFilteredProperties(it).any { it.contains(text, true) } }
filterResults.values = list
filterResults.count = list.size
}
}
return filterResults
}
override fun publishResults(filterText: CharSequence?, filterResults: FilterResults?) {
if (filterResults?.values != null) {
filteredItems = filterResults.values as MutableList<T>
} else {
filteredItems = _items
}
notifyDataSetChanged()
}
}
}

View file

@ -0,0 +1,100 @@
package de.nicidienase.chaosflix.touch.browse.download
import android.arch.lifecycle.Observer
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import de.nicidienase.chaosflix.R
import de.nicidienase.chaosflix.common.userdata.entities.download.OfflineEvent
import de.nicidienase.chaosflix.databinding.FragmentDownloadsBinding
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
import de.nicidienase.chaosflix.touch.browse.BrowseFragment
class DownloadsListFragment : BrowseFragment() {
private lateinit var listener: InteractionListener
private lateinit var binding: FragmentDownloadsBinding
private val handler = Handler()
private val UPDATE_DELAY = 700L
private var columnCount = 1;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
columnCount = arguments?.getInt(ARG_COLUMN_COUNT) ?: 1
}
override fun onAttach(context: Context?) {
super.onAttach(context)
if (context is InteractionListener) {
listener = context
} else {
throw RuntimeException(context.toString() + " must implement LivestreamListFragment.InteractionListener")
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentDownloadsBinding.inflate(inflater, container, false)
setupToolbar(binding.incToolbar?.toolbar!!, R.string.downloads)
overlay = binding.incOverlay?.loadingOverlay
val offlineEventAdapter = OfflineEventAdapter(emptyList(), viewModel, listener)
binding.list.adapter = offlineEventAdapter
if (columnCount <= 1) {
binding.list.layoutManager = LinearLayoutManager(context)
} else {
binding.list.layoutManager = GridLayoutManager(context, columnCount - 1)
}
viewModel.getOfflineEvents().observe(this, Observer { events ->
if(events != null){
offlineEventAdapter.items = events
offlineEventAdapter.notifyDataSetChanged()
setLoadingOverlayVisibility(false)
}
})
return binding.root
}
private var updateRunnable: Runnable? = null
override fun onResume() {
super.onResume()
updateRunnable = object: Runnable {
override fun run() {
viewModel.updateDownloadStatus()
handler.postDelayed(this, UPDATE_DELAY)
}
}
handler.post(updateRunnable)
}
override fun onPause() {
handler.removeCallbacks(updateRunnable)
super.onPause()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setLoadingOverlayVisibility(false)
}
companion object {
private val ARG_COLUMN_COUNT = "column_count"
fun getInstance(columnCount: Int = 1): DownloadsListFragment{
val fragment = DownloadsListFragment()
val args = Bundle()
args.putInt(ARG_COLUMN_COUNT, columnCount)
fragment.arguments = args
return fragment
}
}
interface InteractionListener: OnEventSelectedListener {
}
}

View file

@ -0,0 +1,56 @@
package de.nicidienase.chaosflix.touch.browse.download
import android.databinding.DataBindingUtil
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.squareup.picasso.Picasso
import de.nicidienase.chaosflix.R
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
import de.nicidienase.chaosflix.common.userdata.entities.download.OfflineEvent
import de.nicidienase.chaosflix.databinding.ItemOfflineEventBinding
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
import de.nicidienase.chaosflix.touch.browse.BrowseViewModel
class OfflineEventAdapter(var items: List<Pair<OfflineEvent, PersistentEvent>>, val viewModel: BrowseViewModel, val listener: OnEventSelectedListener) :
RecyclerView.Adapter<OfflineEventAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: OfflineEventAdapter.ViewHolder, position: Int) {
val item = items[position]
holder.binding.event = item.second
Picasso.with(holder.thumbnail.context)
.load(item.second.thumbUrl)
.noFade()
.fit()
.centerInside()
.into(holder.thumbnail)
with(holder.binding){
downloadStatus = viewModel.offlineItemManager.downloadStatus[item.first.downloadReference]
buttonDelete.setOnClickListener {
viewModel.deleteOfflineItem(item.first)
}
content?.setOnClickListener { view ->
listener.onEventSelected(item.second)
}
}
}
override fun getItemCount(): Int {
return items.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = DataBindingUtil.inflate<ItemOfflineEventBinding>(
LayoutInflater.from(parent.context), R.layout.item_offline_event, parent, false)
return ViewHolder(binding, binding.root)
}
inner class ViewHolder(val binding: ItemOfflineEventBinding, val view: View) : RecyclerView.ViewHolder(view) {
val thumbnail = binding.imageView
}
}

View file

@ -0,0 +1,46 @@
package de.nicidienase.chaosflix.touch.browse.eventslist
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import de.nicidienase.chaosflix.R
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentConference
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
import de.nicidienase.chaosflix.touch.eventdetails.EventDetailsActivity
class EventsListActivity : AppCompatActivity(), OnEventSelectedListener {
protected val numColumns: Int
get() = resources.getInteger(R.integer.num_columns)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_events_list)
val conference = intent.getParcelableExtra<PersistentConference>(CONFERENCE_KEY)
if (savedInstanceState == null) {
val eventsListFragment = EventsListFragment.newInstance(EventsListFragment.TYPE_EVENTS, conference, numColumns)
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, eventsListFragment)
.commit();
}
}
override fun onEventSelected(event: PersistentEvent) {
EventDetailsActivity.launch(this, event)
}
companion object {
val CONFERENCE_KEY = "conference_id"
fun start(context: Context, conference: PersistentConference) {
val i = Intent(context, EventsListActivity::class.java)
i.putExtra(CONFERENCE_KEY, conference)
context.startActivity(i)
}
}
}

View file

@ -0,0 +1,198 @@
package de.nicidienase.chaosflix.touch.browse.eventslist;
import android.app.SearchManager;
import android.arch.lifecycle.Observer;
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
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.SearchView;
import java.util.List;
import de.nicidienase.chaosflix.R;
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentConference;
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent;
import de.nicidienase.chaosflix.common.mediadata.sync.Downloader;
import de.nicidienase.chaosflix.databinding.FragmentEventsListBinding;
import de.nicidienase.chaosflix.touch.OnEventSelectedListener;
import de.nicidienase.chaosflix.touch.browse.BrowseFragment;
import de.nicidienase.chaosflix.touch.browse.adapters.EventRecyclerViewAdapter;
public class EventsListFragment extends BrowseFragment implements SearchView.OnQueryTextListener {
private static final String ARG_COLUMN_COUNT = "column-count";
private static final String ARG_TYPE = "type";
private static final String ARG_CONFERENCE = "conference";
private static final String LAYOUTMANAGER_STATE = "layoutmanager-state";
private static final String TAG = EventsListFragment.class.getSimpleName();
public static final int TYPE_EVENTS = 0;
public static final int TYPE_BOOKMARKS = 1;
public static final int TYPE_IN_PROGRESS = 2;
private int columnCount = 1;
private OnEventSelectedListener listener;
private EventRecyclerViewAdapter eventAdapter;
private PersistentConference conference;
private LinearLayoutManager layoutManager;
private Snackbar snackbar;
private int type;
public static EventsListFragment newInstance(int type, PersistentConference conference, int columnCount) {
EventsListFragment fragment = new EventsListFragment();
Bundle args = new Bundle();
args.putInt(ARG_TYPE, type);
args.putInt(ARG_COLUMN_COUNT, columnCount);
args.putParcelable(ARG_CONFERENCE, conference);
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
setHasOptionsMenu(true);
if (context instanceof OnEventSelectedListener) {
listener = (OnEventSelectedListener) context;
} else {
throw new RuntimeException(context.toString() + " must implement OnListFragmentInteractionListener");
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
columnCount = getArguments().getInt(ARG_COLUMN_COUNT);
type = getArguments().getInt(ARG_TYPE);
conference = getArguments().getParcelable(ARG_CONFERENCE);
}
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
FragmentEventsListBinding binding = FragmentEventsListBinding.inflate(inflater, container, false);
AppCompatActivity activity = (AppCompatActivity) requireActivity();
activity.setSupportActionBar(binding.incToolbar.toolbar);
setOverlay(binding.incOverlay.loadingOverlay);
if (columnCount <= 1) {
layoutManager = new LinearLayoutManager(getContext());
} else {
layoutManager = new GridLayoutManager(getContext(), columnCount);
}
binding.list.setLayoutManager(layoutManager);
eventAdapter = new EventRecyclerViewAdapter(listener);
binding.list.setAdapter(eventAdapter);
Observer<List<PersistentEvent>> listObserver = persistentEvents -> {
setEvents(persistentEvents);
if (persistentEvents.size() > 0) {
setLoadingOverlayVisibility(false);
}
};
if (type == TYPE_BOOKMARKS) {
setupToolbar(binding.incToolbar.toolbar, R.string.bookmarks);
getViewModel().getBookmarkedEvents().observe(this, listObserver);
setLoadingOverlayVisibility(false);
} else if (type == TYPE_IN_PROGRESS) {
setupToolbar(binding.incToolbar.toolbar, R.string.continue_watching);
getViewModel().getInProgressEvents().observe(this, listObserver);
setLoadingOverlayVisibility(false);
} else if (type == TYPE_EVENTS) {
{
setupToolbar(binding.incToolbar.toolbar, conference.getTitle(), false);
eventAdapter.setShowTags(conference.getTagsUsefull());
getViewModel().getEventsforConference(conference).observe(this, listObserver);
// getViewModel().updateEventsForConference(conference).observe(this, loadingFinished -> setLoadingOverlayVisibility(!loadingFinished));
getViewModel().updateEventsForConference(conference).observe(this, state -> {
Downloader.DownloaderState downloaderState = state.getState();
switch (downloaderState){
case RUNNING:
setLoadingOverlayVisibility(true);
break;
case DONE:
setLoadingOverlayVisibility(false);
break;
}
if(state.getError() != null){
showSnackbar(state.getError());
}
});
}
}
return binding.getRoot();
}
private void showSnackbar(String message) {
if(snackbar!= null){
snackbar.dismiss();
}
snackbar = Snackbar.make(getView(), message, Snackbar.LENGTH_LONG);
snackbar.setAction("Okay", view -> snackbar.dismiss());
snackbar.show();
}
private void setEvents(List<PersistentEvent> persistentEvents) {
eventAdapter.setItems(persistentEvents);
Parcelable layoutState = getArguments().getParcelable(LAYOUTMANAGER_STATE);
if (layoutState != null) { layoutManager.onRestoreInstanceState(layoutState); }
}
@Override
public void onPause() {
super.onPause();
getArguments().putParcelable(LAYOUTMANAGER_STATE, layoutManager.onSaveInstanceState());
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.events_menu, menu);
MenuItem searchMenuItem = menu.findItem(R.id.search);
SearchView searchView = (SearchView) searchMenuItem.getActionView();
SearchManager searchManager = (SearchManager) getActivity().getSystemService(Context.SEARCH_SERVICE);
searchView.setSearchableInfo(searchManager.
getSearchableInfo(getActivity().getComponentName()));
searchView.setSubmitButtonEnabled(true);
searchView.setIconified(false);
searchView.setOnQueryTextListener(this);
}
@Override
public void onDetach() {
super.onDetach();
listener = null;
}
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
eventAdapter.getFilter().filter(newText);
return true;
}
}

View file

@ -0,0 +1,69 @@
package de.nicidienase.chaosflix.touch.browse.streaming
import android.support.v7.widget.RecyclerView
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.squareup.picasso.Picasso
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.LiveConference
import de.nicidienase.chaosflix.databinding.ItemLiveeventCardviewBinding
class LivestreamAdapter(val listener: LivestreamListFragment.InteractionListener , liveConferences: List<LiveConference> = emptyList()) : RecyclerView.Adapter<LivestreamAdapter.ViewHolder>(){
lateinit var items: MutableList<StreamingItem>
init {
setContent(liveConferences)
}
private fun convertToStreamingItemList(liveConferences: List<LiveConference>) {
liveConferences.map { liveConference ->
liveConference.groups.map { group ->
group.rooms.map { room ->
items.add(StreamingItem(liveConference, group, room))
}
}
}
}
private val TAG = LivestreamAdapter::class.simpleName
fun setContent(liveConferences: List<LiveConference>){
items = ArrayList()
convertToStreamingItemList(liveConferences)
Log.d(TAG,"Size:" + items.size)
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return items.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding =
ItemLiveeventCardviewBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.binding.item = item
Picasso.with(holder.binding.root.context)
.load(item.room.thumb)
.noFade()
.fit()
.centerInside()
.into(holder.binding.imageView)
holder.binding.root.setOnClickListener(View.OnClickListener {
listener.onStreamSelected(item)
})
}
inner class ViewHolder(val binding: ItemLiveeventCardviewBinding) : RecyclerView.ViewHolder(binding.root)
}

View file

@ -0,0 +1,101 @@
package de.nicidienase.chaosflix.touch.browse.streaming
import android.arch.lifecycle.Observer
import android.content.Context
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.LinearLayoutManager
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import de.nicidienase.chaosflix.R
import de.nicidienase.chaosflix.databinding.FragmentLivestreamsBinding
import de.nicidienase.chaosflix.touch.browse.BrowseFragment
class LivestreamListFragment : BrowseFragment() {
private lateinit var listener: InteractionListener
private lateinit var binding: FragmentLivestreamsBinding
lateinit var adapter: LivestreamAdapter
lateinit var snackbar: Snackbar
private var columnCount = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments != null) {
columnCount = arguments!!.getInt(ARG_COLUMN_COUNT)
}
}
override fun onAttach(context: Context?) {
super.onAttach(context)
if (context is InteractionListener) {
listener = context
adapter = LivestreamAdapter(listener)
} else {
throw RuntimeException(context.toString() + " must implement LivestreamListFragment.InteractionListener")
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentLivestreamsBinding.inflate(inflater, container, false)
setupToolbar(binding.incToolbar?.toolbar!!, R.string.livestreams)
if (columnCount <= 1) {
binding.list.layoutManager = LinearLayoutManager(context)
} else {
binding.list.layoutManager = GridLayoutManager(context, columnCount)
}
binding.list.adapter = adapter
binding.swipeRefreshLayout.setOnRefreshListener {
updateList()
}
snackbar = Snackbar.make(binding.root,R.string.no_livestreams,Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.reload, View.OnClickListener { this.updateList() })
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
updateList()
}
private val TAG = LivestreamListFragment::class.simpleName
private fun updateList() {
// binding.swipeRefreshLayout.postDelayed( Runnable {
// binding.swipeRefreshLayout.isRefreshing = true
// }, 500)
binding.swipeRefreshLayout.isRefreshing = true
Log.d(TAG,"Refresh starting")
viewModel.getLivestreams().observe(this, Observer {
it?.let { adapter.setContent(it) }
binding.swipeRefreshLayout.isRefreshing = false
if(it?.size == 0 && !snackbar.isShown){
snackbar.show()
} else {
snackbar.dismiss()
}
Log.d(TAG, "Refresh done")
})
}
interface InteractionListener {
fun onStreamSelected(streamingItem: StreamingItem)
}
companion object {
private val ARG_COLUMN_COUNT = "column-count"
fun newInstance(columnCount: Int): LivestreamListFragment {
val fragment = LivestreamListFragment()
val args = Bundle()
args.putInt(ARG_COLUMN_COUNT, columnCount)
fragment.arguments = args
return fragment
}
}
}

View file

@ -0,0 +1,9 @@
package de.nicidienase.chaosflix.touch.browse.streaming
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.Group
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.LiveConference
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.Room
data class StreamingItem(val conference: LiveConference,
val group: Group,
val room: Room)

View file

@ -0,0 +1,134 @@
package de.nicidienase.chaosflix.touch.eventdetails
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.ViewModel
import android.os.Bundle
import de.nicidienase.chaosflix.common.ChaosflixDatabase
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentRecording
import de.nicidienase.chaosflix.common.mediadata.network.RecordingService
import de.nicidienase.chaosflix.common.mediadata.sync.Downloader
import de.nicidienase.chaosflix.common.userdata.entities.watchlist.WatchlistItem
import de.nicidienase.chaosflix.common.util.LiveEvent
import de.nicidienase.chaosflix.common.util.SingleLiveEvent
import de.nicidienase.chaosflix.common.util.ThreadHandler
import de.nicidienase.chaosflix.touch.OfflineItemManager
import java.io.File
class DetailsViewModel(
val database: ChaosflixDatabase,
recordingApi: RecordingService
) : ViewModel() {
val state: SingleLiveEvent<LiveEvent<DetailsViewModelState,Bundle,String>>
= SingleLiveEvent()
val downloader = Downloader(recordingApi, database)
var writeExternalStorageAllowed: Boolean = false
val offlineItemManager: OfflineItemManager = OfflineItemManager(offlineEventDao = database.offlineEventDao())
private val handler = ThreadHandler()
fun setEvent(persistentEvent: PersistentEvent): LiveData<PersistentEvent?> {
downloader.updateRecordingsForEvent(persistentEvent)
return database.eventDao().findEventByGuid(persistentEvent.guid)
}
fun getRecordingForEvent(persistentEvent: PersistentEvent): LiveData<List<PersistentRecording>> {
downloader.updateRecordingsForEvent(persistentEvent)
return database.recordingDao().findRecordingByEvent(persistentEvent.id)
}
fun getBookmarkForEvent(guid: String): LiveData<WatchlistItem> =
database.watchlistItemDao().getItemForEvent(guid)
fun createBookmark(guid: String) {
handler.runOnBackgroundThread {
database.watchlistItemDao().saveItem(WatchlistItem(eventGuid = guid))
}
}
fun removeBookmark(guid: String) {
handler.runOnBackgroundThread {
database.watchlistItemDao().deleteItem(guid)
}
}
fun download(event: PersistentEvent, recording: PersistentRecording)
= offlineItemManager.download(event, recording)
private fun fileExists(guid: String): Boolean {
val offlineItem = database.offlineEventDao().getByEventGuidSync(guid)
return offlineItem != null && File(offlineItem.localPath).exists()
}
fun deleteOfflineItem(event: PersistentEvent): LiveData<Boolean> {
val result = MutableLiveData<Boolean>()
handler.runOnBackgroundThread {
database.offlineEventDao().getByEventGuidSync(event.guid)?.let {
offlineItemManager.deleteOfflineItem(it)
}
result.postValue(true)
}
return result
}
fun getRelatedEvents(event: PersistentEvent): LiveData<List<PersistentEvent>>{
val data = MutableLiveData<List<PersistentEvent>>()
handler.runOnBackgroundThread {
val guids = database.relatedEventDao().getRelatedEventsForEventSync(event.id).map { it.relatedEventGuid }
data.postValue(database.eventDao().findEventsByGUIDsSync(guids))
}
return data
}
fun playEvent(event: PersistentEvent) {
handler.runOnBackgroundThread {
val offlineEvent = database.offlineEventDao().getByEventGuidSync(event.guid)
if(offlineEvent != null){
// Play offlineEvent
if(!fileExists(event.guid)){
state.postValue(LiveEvent(DetailsViewModelState.Error, error = "File is gone"))
return@runOnBackgroundThread
}
val bundle = Bundle()
bundle.putString(KEY_LOCAL_PATH, offlineEvent.localPath)
state.postValue(LiveEvent(DetailsViewModelState.PlayOfflineItem, data = bundle))
} else {
// select quality then playEvent
val items = database.recordingDao().findRecordingByEventSync(event.id).toTypedArray()
val bundle = Bundle()
bundle.putParcelableArray(KEY_SELECT_RECORDINGS, items)
state.postValue(LiveEvent(DetailsViewModelState.SelectRecording, data = bundle ))
}
}
}
fun playRecording(recording: PersistentRecording){
val bundle = Bundle()
bundle.putParcelable(KEY_PLAY_RECORDING, recording)
state.postValue(LiveEvent(DetailsViewModelState.PlayOnlineItem, data = bundle))
}
fun offlineItemExists(event: PersistentEvent): LiveData<Boolean> {
val liveData = MutableLiveData<Boolean>()
handler.runOnBackgroundThread {
database.offlineEventDao().getByEventGuidSync(event.guid)
}
return liveData
}
enum class DetailsViewModelState{
PlayOfflineItem, PlayOnlineItem, SelectRecording, Error
}
companion object {
val TAG = DetailsViewModel::class.simpleName
val KEY_LOCAL_PATH = "local_path"
val KEY_SELECT_RECORDINGS = "select_recordings"
val KEY_PLAY_RECORDING = "play_recording"
}
}

View file

@ -0,0 +1,113 @@
package de.nicidienase.chaosflix.touch.eventdetails
import android.Manifest
import android.arch.lifecycle.ViewModelProviders
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.support.v4.app.ActivityCompat
import android.support.v7.app.AppCompatActivity
import de.nicidienase.chaosflix.R
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentRecording
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
import de.nicidienase.chaosflix.touch.ViewModelFactory
import de.nicidienase.chaosflix.touch.playback.PlayerActivity
class EventDetailsActivity : AppCompatActivity(),
EventDetailsFragment.OnEventDetailsFragmentInteractionListener,
OnEventSelectedListener {
private lateinit var viewModel: DetailsViewModel
private val PERMISSION_REQUEST_CODE: Int = 1;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_eventdetails)
viewModel = ViewModelProviders.of(this, ViewModelFactory(this)).get(DetailsViewModel::class.java)
viewModel.writeExternalStorageAllowed = hasWriteStoragePermission()
val event = intent.getParcelableExtra<PersistentEvent>(EXTRA_EVENT)
showFragmentForEvent(event)
if (!ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
requestWriteStoragePermission()
}
}
private fun showFragmentForEvent(event: PersistentEvent, addToBackStack: Boolean = false) {
val detailsFragment = EventDetailsFragment.newInstance(event)
detailsFragment.allowEnterTransitionOverlap = true
detailsFragment.allowReturnTransitionOverlap = true
val ft = supportFragmentManager.beginTransaction()
ft.replace(R.id.fragment_container, detailsFragment)
if (addToBackStack) {
ft.addToBackStack(null)
}
ft.setReorderingAllowed(true)
ft.commit()
}
override fun onEventSelected(event: PersistentEvent) {
showFragmentForEvent(event, true)
}
override fun onToolbarStateChange() {
invalidateOptionsMenu()
}
override fun playItem(event: PersistentEvent, recording: PersistentRecording) {
PlayerActivity.launch(this, event, recording)
}
override fun playItem(event: PersistentEvent, uri: String) {
PlayerActivity.launch(this, event, uri)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSION_REQUEST_CODE && grantResults.size > 0) {
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
// requestWriteStoragePermission()
} else {
viewModel.writeExternalStorageAllowed = true
invalidateOptionsMenu()
}
}
}
private fun requestWriteStoragePermission() {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), PERMISSION_REQUEST_CODE);
}
private fun hasWriteStoragePermission(): Boolean {
return ActivityCompat.checkSelfPermission(
this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}
companion object {
private val EXTRA_EVENT = "extra_event"
private val EXTRA_URI = "extra_uri"
fun launch(context: Context, event: PersistentEvent) {
val intent = Intent(context, EventDetailsActivity::class.java)
intent.putExtra(EXTRA_EVENT, event)
context.startActivity(intent)
}
fun launch(context: Context, eventId: Uri) {
val intent = Intent(context, EventDetailsActivity::class.java)
intent.putExtra(EXTRA_URI, eventId)
context.startActivity(intent)
}
}
}

View file

@ -0,0 +1,338 @@
package de.nicidienase.chaosflix.touch.eventdetails
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v4.app.Fragment
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
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 com.squareup.picasso.Callback
import com.squareup.picasso.Picasso
import de.nicidienase.chaosflix.R
import de.nicidienase.chaosflix.common.Util
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentRecording
import de.nicidienase.chaosflix.common.userdata.entities.watchlist.WatchlistItem
import de.nicidienase.chaosflix.databinding.FragmentEventDetailsBinding
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
import de.nicidienase.chaosflix.touch.PreferencesManager
import de.nicidienase.chaosflix.touch.ViewModelFactory
import de.nicidienase.chaosflix.touch.browse.adapters.EventRecyclerViewAdapter
class EventDetailsFragment : Fragment() {
private var listener: OnEventDetailsFragmentInteractionListener? = null
private var appBarExpanded: Boolean = false
private lateinit var event: PersistentEvent
private var watchlistItem: WatchlistItem? = null
private var eventSelectedListener: OnEventSelectedListener? = null
private var selectDialog: AlertDialog? = null
private lateinit var viewModel: DetailsViewModel
private lateinit var relatedEventsAdapter: EventRecyclerViewAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
// postponeEnterTransition()
// val transition = TransitionInflater.from(context)
// .inflateTransition(android.R.transition.move)
// // transition.setDuration(getResources().getInteger(R.integer.anim_duration));
// sharedElementEnterTransition = transition
if (arguments != null) {
val parcelable = arguments?.getParcelable<PersistentEvent>(EVENT_PARAM)
if(parcelable != null){
event = parcelable
} else {
throw IllegalStateException("Event Missing")
}
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_event_details, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentEventDetailsBinding.bind(view)
binding.event = event
binding.playFab.setOnClickListener { _ -> play() }
if (listener != null) {
(activity as AppCompatActivity).setSupportActionBar(binding.animToolbar)
(activity as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
eventSelectedListener?.let {
relatedEventsAdapter = RelatedEventsRecyclerViewAdapter(eventSelectedListener!!)
binding.relatedItemsList.adapter = relatedEventsAdapter
binding.relatedItemsList.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
}
binding.appbar.addOnOffsetChangedListener { appBarLayout, verticalOffset ->
val v = Math.abs(verticalOffset).toDouble() / appBarLayout.totalScrollRange
if (appBarExpanded xor (v > 0.8)) {
if (listener != null) {
listener!!.onToolbarStateChange()
}
appBarExpanded = v > 0.8
// binding.collapsingToolbar.isTitleEnabled = appBarExpanded
}
}
viewModel = ViewModelProviders.of(
requireActivity(),
ViewModelFactory(requireContext()))
.get(DetailsViewModel::class.java)
viewModel.setEvent(event)
.observe(this, Observer {
Log.d(TAG,"Loading Event ${event.title}, ${event.guid}")
updateBookmark(event.guid)
binding.thumbImage.transitionName = getString(R.string.thumbnail) + event.guid
Picasso.with(context)
.load(event.thumbUrl)
.noFade()
.into(binding.thumbImage, object : Callback {
override fun onSuccess() {
// startPostponedEnterTransition()
}
override fun onError() {
// startPostponedEnterTransition()
}
})
})
viewModel.getRelatedEvents(event).observe(this, Observer {
relatedEventsAdapter.items = ArrayList(it)
})
viewModel.state.observe(this, Observer { liveEvent ->
if(liveEvent == null){
return@Observer
}
when(liveEvent.state){
DetailsViewModel.DetailsViewModelState.PlayOfflineItem -> {
liveEvent.data?.getParcelable<PersistentRecording>(DetailsViewModel.KEY_PLAY_RECORDING)?.let {
listener?.playItem(event, it)
}
}
DetailsViewModel.DetailsViewModelState.PlayOnlineItem -> {
liveEvent.data?.getParcelable<PersistentRecording>(DetailsViewModel.KEY_PLAY_RECORDING)?.let {
listener?.playItem(event,it)
}
}
DetailsViewModel.DetailsViewModelState.SelectRecording -> {
val selectItems: Array<PersistentRecording> =
liveEvent.data?.getParcelableArray(DetailsViewModel.KEY_SELECT_RECORDINGS) as Array<PersistentRecording>
selectRecording(selectItems.asList()) {
viewModel.playRecording(it)
}
}
DetailsViewModel.DetailsViewModelState.Error -> liveEvent.error?.let { showSnackbar(it) }
}
})
}
private fun updateBookmark(guid: String) {
viewModel.getBookmarkForEvent(guid)
.observe(this, Observer { watchlistItem: WatchlistItem? ->
this.watchlistItem = watchlistItem
listener?.invalidateOptionsMenu()
})
}
private fun showSnackbar(message: String, duration: Int = Snackbar.LENGTH_LONG ){
view?.let {
Snackbar.make(it, message, duration)
}
}
private fun play() {
if(listener == null){
return
}
viewModel.playEvent(event)
}
private fun selectRecording(persistentRecordings: List<PersistentRecording>, action: (recording: PersistentRecording) -> Unit) {
val stream = Util.getOptimalStream(persistentRecordings)
if (stream != null && PreferencesManager.getAutoselectStream()) {
action.invoke(stream)
} else {
val items: List<String> = persistentRecordings.map { getStringForRecording(it) }
selectRecordingFromList(items, DialogInterface.OnClickListener { dialogInterface, i ->
action.invoke(persistentRecordings[i])
})
}
}
private fun getStringForRecording(recording: PersistentRecording): String {
return "${if (recording.isHighQuality) "HD" else "SD"} ${recording.folder} [${recording.language}]"
}
private fun selectRecordingFromList(items: List<String>, resultHandler: DialogInterface.OnClickListener) {
this.context?.let { context ->
if (selectDialog != null) {
selectDialog?.dismiss()
}
val builder = AlertDialog.Builder(context)
builder.setItems(items.toTypedArray(), resultHandler)
selectDialog = builder.create()
selectDialog?.show()
}
}
override fun onAttach(context: Context?) {
super.onAttach(context)
if (context is OnEventSelectedListener) {
eventSelectedListener = context
}
if (context is OnEventDetailsFragmentInteractionListener) {
listener = context
} else {
throw RuntimeException(context!!.toString() + " must implement OnFragmentInteractionListener")
}
}
override fun onDetach() {
super.onDetach()
listener = null
}
override fun onPrepareOptionsMenu(menu: Menu) {
Log.d(TAG, "OnPrepareOptionsMenu")
super.onPrepareOptionsMenu(menu)
if (watchlistItem != null) {
menu.findItem(R.id.action_bookmark).isVisible = false
menu.findItem(R.id.action_unbookmark).isVisible = true
} else {
menu.findItem(R.id.action_bookmark).isVisible = true
menu.findItem(R.id.action_unbookmark).isVisible = false
}
menu.findItem(R.id.action_download).isVisible = viewModel.writeExternalStorageAllowed
viewModel.offlineItemExists(event).observe(this, Observer { itemExists->
itemExists?.let {exists ->
menu.findItem(R.id.action_download).isVisible =
viewModel.writeExternalStorageAllowed && !exists
menu.findItem(R.id.action_delete_offline_item).isVisible =
viewModel.writeExternalStorageAllowed && exists
}
})
menu.findItem(R.id.action_play).isVisible = appBarExpanded
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
super.onCreateOptionsMenu(menu, inflater)
// if (appBarExpanded)
inflater!!.inflate(R.menu.details_menu, menu)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item!!.itemId) {
android.R.id.home -> {
activity?.finish()
return true
}
R.id.action_play -> {
play()
return true
}
R.id.action_bookmark -> {
viewModel.createBookmark(event.guid)
updateBookmark(event.guid)
return true
}
R.id.action_unbookmark -> {
viewModel.removeBookmark(event.guid)
watchlistItem = null
listener!!.invalidateOptionsMenu()
return true
}
R.id.action_download -> {
viewModel.getRecordingForEvent(event).observe(this, Observer { recordings ->
if (recordings != null) {
selectRecording(recordings, { recording -> downloadRecording(recording) })
}
})
return true
}
R.id.action_delete_offline_item -> {
viewModel.deleteOfflineItem(event).observe(this, Observer { success ->
if (success != null) {
view?.let { Snackbar.make(it, "Deleted Download", Snackbar.LENGTH_SHORT).show() }
}
})
return true
}
R.id.action_share -> {
val shareIntent = Intent(Intent.ACTION_SEND, Uri.parse(event.frontendLink))
shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.watch_this))
shareIntent.putExtra(Intent.EXTRA_TEXT, event.frontendLink)
shareIntent.setType("text/plain")
startActivity(shareIntent)
return true
}
R.id.action_external_player -> {
viewModel.getRecordingForEvent(event).observe(this, Observer { recordings ->
if (recordings != null) {
selectRecording(recordings) { recording ->
val shareIntent = Intent(Intent.ACTION_VIEW, Uri.parse(recording.recordingUrl))
startActivity(shareIntent)
}
}
})
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
private fun downloadRecording(recording: PersistentRecording) {
viewModel.download(event, recording).observe(this, Observer {
if (it != null) {
val message = if (it) "Download started" else "Error starting download"
Snackbar.make(view!!, message, Snackbar.LENGTH_LONG).show()
}
})
}
interface OnEventDetailsFragmentInteractionListener {
fun onToolbarStateChange()
fun invalidateOptionsMenu()
fun playItem(event: PersistentEvent, recording: PersistentRecording)
fun playItem(event: PersistentEvent, uri: String)
}
companion object {
private val TAG = EventDetailsFragment::class.java.simpleName
private val EVENT_PARAM = "event_param"
fun newInstance(event: PersistentEvent): EventDetailsFragment {
val fragment = EventDetailsFragment()
val args = Bundle()
args.putParcelable(EVENT_PARAM, event)
fragment.arguments = args
return fragment
}
}
}

View file

@ -0,0 +1,9 @@
package de.nicidienase.chaosflix.touch.eventdetails
import de.nicidienase.chaosflix.R
import de.nicidienase.chaosflix.touch.OnEventSelectedListener
import de.nicidienase.chaosflix.touch.browse.adapters.EventRecyclerViewAdapter
class RelatedEventsRecyclerViewAdapter(listener: OnEventSelectedListener) : EventRecyclerViewAdapter(listener) {
override val layout = R.layout.related_event_cardview_layout
}

View file

@ -0,0 +1,256 @@
package de.nicidienase.chaosflix.touch.playback;
import android.arch.lifecycle.ViewModelProviders;
import android.content.Context;
import android.databinding.DataBindingUtil;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Util;
import de.nicidienase.chaosflix.R;
import de.nicidienase.chaosflix.databinding.FragmentExoPlayerBinding;
import de.nicidienase.chaosflix.touch.ViewModelFactory;
public class ExoPlayerFragment extends Fragment implements PlayerEventListener.PlayerStateChangeListener {
private static final String TAG = ExoPlayerFragment.class.getSimpleName();
private static final String PLAYBACK_STATE = "playback_state";
private static final String ARG_item = "item";
private OnMediaPlayerInteractionListener listener;
private final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
private String userAgent;
private Handler mainHandler = new Handler();
private boolean playbackState = true;
private SimpleExoPlayer exoPlayer;
private PlayerViewModel viewModel;
private PlaybackItem item;
FragmentExoPlayerBinding binding;
public ExoPlayerFragment() {
}
public static ExoPlayerFragment newInstance(PlaybackItem item) {
ExoPlayerFragment fragment = new ExoPlayerFragment();
Bundle args = new Bundle();
args.putParcelable(ARG_item, item);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
item = getArguments().getParcelable(ARG_item);
}
if (savedInstanceState != null) {
playbackState = savedInstanceState.getBoolean(PLAYBACK_STATE, true);
}
viewModel = ViewModelProviders.of(this, new ViewModelFactory(requireContext())).get(PlayerViewModel.class);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_exo_player, container, false);
Toolbar toolbar = binding.getRoot().findViewById(R.id.toolbar);
toolbar.setTitle(item.getTitle());
toolbar.setSubtitle(item.getSubtitle());
((AppCompatActivity) getActivity()).setSupportActionBar(toolbar);
((AppCompatActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
return binding.getRoot();
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (exoPlayer == null) {
exoPlayer = setupPlayer();
}
}
@Override
public void onStop() {
super.onStop();
if (exoPlayer != null) {
viewModel.setPlaybackProgress(item.getEventId(), exoPlayer.getCurrentPosition());
exoPlayer.setPlayWhenReady(false);
}
FragmentActivity activity = getActivity();
if(activity != null){
Window window = activity.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
}
@Override
public void onStart() {
super.onStart();
FragmentActivity activity = getActivity();
if(activity != null){
Window window = activity.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
}
if (exoPlayer != null) {
exoPlayer.setPlayWhenReady(playbackState);
viewModel.getPlaybackProgress(item.getEventId()).observe(this, playbackProgress -> {
if (playbackProgress != null) {
exoPlayer.seekTo(playbackProgress.getProgress());
}
});
binding.videoView.setPlayer(exoPlayer);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (exoPlayer != null) {
outState.putBoolean(PLAYBACK_STATE, exoPlayer.getPlayWhenReady());
}
}
private SimpleExoPlayer setupPlayer() {
Log.d(TAG, "Setting up Player.");
binding.videoView.setKeepScreenOn(true);
userAgent = Util.getUserAgent(getContext(), getResources().getString(R.string.app_name));
AdaptiveTrackSelection.Factory trackSelectorFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
DefaultTrackSelector trackSelector = new DefaultTrackSelector(trackSelectorFactory);
LoadControl loadControl = new DefaultLoadControl();
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext(), null, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF);
exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, loadControl);
PlayerEventListener listener = new PlayerEventListener(exoPlayer, this);
exoPlayer.addVideoListener(listener);
exoPlayer.addListener(listener);
exoPlayer.setPlayWhenReady(playbackState);
exoPlayer.prepare(buildMediaSource(Uri.parse(item.getUri()), ""));
return exoPlayer;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof OnMediaPlayerInteractionListener) {
listener = (OnMediaPlayerInteractionListener) context;
} else {
throw new RuntimeException(context.toString() + " must implement OnFragmentInteractionListener");
}
}
@Override
public void onDetach() {
super.onDetach();
listener = null;
}
@Override
public void notifyLoadingStart() {
if (binding.progressBar != null) {
binding.progressBar.setVisibility(View.VISIBLE);
}
}
@Override
public void notifyLoadingFinished() {
if (binding.progressBar != null) {
binding.progressBar.setVisibility(View.INVISIBLE);
}
}
@Override
public void notifyError(String errorMessage) {
Snackbar.make(binding.videoView, errorMessage, Snackbar.LENGTH_LONG).show();
}
@Override
public void notifyEnd() {
viewModel.deletePlaybackProgress(item.getEventId());
}
public interface OnMediaPlayerInteractionListener {
}
private MediaSource buildMediaSource(Uri uri, String overrideExtension) {
DataSource.Factory mediaDataSourceFactory = buildDataSourceFactory(true);
int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
switch (type) {
case C.TYPE_SS:
return new SsMediaSource(uri, buildDataSourceFactory(false), new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, null);
case C.TYPE_DASH:
return new DashMediaSource(uri, buildDataSourceFactory(false), new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mainHandler, null);
case C.TYPE_HLS:
return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, null);
case C.TYPE_OTHER:
return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(), mainHandler, null);
default: {
throw new IllegalStateException("Unsupported type: " + type);
}
}
}
private DataSource.Factory buildDataSourceFactory(boolean useBandwidthMeter) {
return buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
}
private DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
return new DefaultDataSourceFactory(getContext(), bandwidthMeter, buildHttpDataSourceFactory(bandwidthMeter));
}
private HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
return new DefaultHttpDataSourceFactory(userAgent,
bandwidthMeter,
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
true);
}
}

View file

@ -0,0 +1,36 @@
package de.nicidienase.chaosflix.touch.playback
import android.os.Parcel
import android.os.Parcelable
data class PlaybackItem (val title: String, val subtitle: String, val eventId: Long, val uri: String) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString(),
parcel.readLong(),
parcel.readString()) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(title)
parcel.writeString(subtitle)
parcel.writeLong(eventId)
parcel.writeString(uri)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<PlaybackItem> {
override fun createFromParcel(parcel: Parcel): PlaybackItem {
return PlaybackItem(parcel)
}
override fun newArray(size: Int): Array<PlaybackItem?> {
return arrayOfNulls(size)
}
}
}

View file

@ -0,0 +1,93 @@
package de.nicidienase.chaosflix.touch.playback
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.MenuItem
import de.nicidienase.chaosflix.R
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentEvent
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.PersistentRecording
import de.nicidienase.chaosflix.common.mediadata.entities.streaming.StreamUrl
class PlayerActivity : AppCompatActivity(), ExoPlayerFragment.OnMediaPlayerInteractionListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player)
if (savedInstanceState == null && intent.extras != null) {
val contentType = intent.getStringExtra(CONTENT_TYPE)
var playbackItem = PlaybackItem("Empty", "Empty", 0, "")
if (contentType.equals(CONTENT_RECORDING)) {
val event = intent.extras.getParcelable<PersistentEvent>(EVENT_KEY)
val recording = intent.extras.getParcelable<PersistentRecording>(RECORDING_KEY)
val recordingUri = intent.extras.getString(OFFLINE_URI)
playbackItem = PlaybackItem(
event?.title ?: "",
event?.subtitle ?: "",
event?.id ?: 0,
recordingUri ?: recording?.recordingUrl ?: "")
} else if (contentType.equals(CONTENT_STREAM)) {
// TODO implement Player for Stream
val conference = intent.extras.getString(CONFERENCE,"")
val room = intent.extras.getString(ROOM,"")
val stream = intent.extras.getString(STREAM, "")
playbackItem = PlaybackItem(conference,room,0, stream)
}
val ft = supportFragmentManager.beginTransaction()
val playerFragment = ExoPlayerFragment.newInstance(playbackItem)
ft.replace(R.id.fragment_container, playerFragment)
ft.commit()
}
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
if (item?.itemId == android.R.id.home) {
finish()
return true
}
return super.onOptionsItemSelected(item)
}
companion object {
val CONTENT_TYPE = "content"
val CONTENT_RECORDING = "content_recording"
val CONTENT_STREAM = "content_stream"
val EVENT_KEY = "event"
val RECORDING_KEY = "recording"
val CONFERENCE = "live_conferences"
val ROOM = "room"
val STREAM = "stream"
val OFFLINE_URI = "recording_uri"
fun launch(context: Context, event: PersistentEvent, uri: String) {
val i = Intent(context, PlayerActivity::class.java)
i.putExtra(CONTENT_TYPE, CONTENT_RECORDING)
i.putExtra(PlayerActivity.EVENT_KEY, event)
i.putExtra(PlayerActivity.OFFLINE_URI, uri)
context.startActivity(i)
}
fun launch(context: Context, event: PersistentEvent, recording: PersistentRecording) {
val i = Intent(context, PlayerActivity::class.java)
i.putExtra(CONTENT_TYPE, CONTENT_RECORDING)
i.putExtra(PlayerActivity.EVENT_KEY, event)
i.putExtra(PlayerActivity.RECORDING_KEY, recording)
context.startActivity(i)
}
fun launch(context: Context, conference: String, room: String, stream: StreamUrl) {
val i = Intent(context, PlayerActivity::class.java)
i.putExtra(CONTENT_TYPE, CONTENT_STREAM)
i.putExtra(CONFERENCE, conference)
i.putExtra(ROOM, room)
i.putExtra(STREAM, stream.url)
context.startActivity(i)
}
}
}

View file

@ -0,0 +1,106 @@
package de.nicidienase.chaosflix.touch.playback;
import android.util.Log;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
/**
* Created by felix on 27.09.17.
*/
class PlayerEventListener implements Player.EventListener, SimpleExoPlayer.VideoListener {
private static final String TAG = PlayerEventListener.class.getSimpleName();
private SimpleExoPlayer player;
private PlayerStateChangeListener listener;
public PlayerEventListener(SimpleExoPlayer player, PlayerStateChangeListener listener) {
this.player = player;
this.listener = listener;
}
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
}
@Override
public void onLoadingChanged(boolean isLoading) {
if (isLoading && player.getPlaybackState() != Player.STATE_READY) {
listener.notifyLoadingStart();
} else {
listener.notifyLoadingFinished();
}
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
switch (playbackState) {
case Player.STATE_BUFFERING:
if (player.isLoading()) {
listener.notifyLoadingStart();
}
break;
case Player.STATE_ENDED:
Log.d(TAG, "Finished Playback");
listener.notifyEnd();
break;
case Player.STATE_IDLE:
case Player.STATE_READY:
default:
listener.notifyLoadingFinished();
}
}
@Override
public void onRepeatModeChanged(int repeatMode) {
}
@Override
public void onPlayerError(ExoPlaybackException error) {
String errorMessage = error.getCause().getMessage();
listener.notifyError(errorMessage);
Log.d(TAG, errorMessage, error);
}
@Override
public void onPositionDiscontinuity() {
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
}
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
}
@Override
public void onRenderedFirstFrame() {
listener.notifyLoadingFinished();
}
public interface PlayerStateChangeListener {
void notifyLoadingStart();
void notifyLoadingFinished();
void notifyError(String errorMessage);
void notifyEnd();
}
}

View file

@ -0,0 +1,27 @@
package de.nicidienase.chaosflix.touch.playback
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.ViewModel
import de.nicidienase.chaosflix.common.ChaosflixDatabase
import de.nicidienase.chaosflix.common.userdata.entities.progress.PlaybackProgress
import de.nicidienase.chaosflix.common.util.ThreadHandler
internal class PlayerViewModel(val database: ChaosflixDatabase) : ViewModel() {
val handler = ThreadHandler()
fun getPlaybackProgress(apiID: Long): LiveData<PlaybackProgress>
= database.playbackProgressDao().getProgressForEvent(apiID)
fun setPlaybackProgress(eventId: Long, progress: Long) {
handler.runOnBackgroundThread {
database.playbackProgressDao().saveProgress(PlaybackProgress(eventId, progress))
}
}
fun deletePlaybackProgress(eventId: Long) {
handler.runOnBackgroundThread {
database.playbackProgressDao().deleteItem(eventId)
}
}
}

View file

@ -0,0 +1,18 @@
package de.nicidienase.chaosflix.touch.settings
import android.databinding.DataBindingUtil
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import de.nicidienase.chaosflix.R
import de.nicidienase.chaosflix.databinding.ActivitySettingsBinding
class SettingsActivity: AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil
.setContentView<ActivitySettingsBinding>(this, R.layout.activity_settings)
setSupportActionBar(binding.toolbarInc?.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.settings)
}
}

View file

@ -0,0 +1,68 @@
package de.nicidienase.chaosflix.touch.settings
import android.content.Intent
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v7.preference.PreferenceFragmentCompat
import de.nicidienase.chaosflix.R
import de.nicidienase.chaosflix.touch.ChaosflixApplication
import net.rdrei.android.dirchooser.DirectoryChooserActivity
import net.rdrei.android.dirchooser.DirectoryChooserConfig
class SettingsFragment : PreferenceFragmentCompat() {
private val REQUEST_DIRECTORY: Int = 0
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_DIRECTORY) {
if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) {
val dir = data!!.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR)
val sharedPref = PreferenceManager.getDefaultSharedPreferences(ChaosflixApplication.APPLICATION_CONTEXT)
val edit = sharedPref.edit()
edit.putString("download_folder", dir)
edit.apply()
this.updateSummary()
}
}
}
private fun updateSummary() {
val sharedPref = PreferenceManager.getDefaultSharedPreferences(ChaosflixApplication.APPLICATION_CONTEXT)
val folder = sharedPref.getString("download_folder", "")
val pref = this.findPreference("download_folder")
pref.setSummary(folder)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences,rootKey)
updateSummary()
val pref = this.findPreference("download_folder")
pref.setOnPreferenceClickListener({
val chooserIntent = Intent(context, DirectoryChooserActivity::class.java)
val config = DirectoryChooserConfig.builder()
.newDirectoryName("Download folder")
.allowReadOnlyDirectory(false)
.allowNewDirectoryNameModification(true)
.build()
chooserIntent.putExtra(DirectoryChooserActivity.EXTRA_CONFIG, config)
startActivityForResult(chooserIntent, REQUEST_DIRECTORY)
return@setOnPreferenceClickListener true
})
}
companion object {
fun getInstance(): SettingsFragment {
val fragment = SettingsFragment()
val args = Bundle()
fragment.arguments = args
return fragment
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Some files were not shown because too many files have changed in this diff Show more