1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-08 20:12:39 +01:00

enhancement/video-player (#480)

* Add toolbar visibility toggle and draw it over viewer

* Set contract bindings once available

* Fix video url param error and prepare progressanimator

* Add gif support and better transitions

* Interface a lot of things

* Reorder back press

* Clean up files and fix selector

* Add gif support

* Redraw bounds when necessary
This commit is contained in:
Allan Wang 2017-11-12 02:48:36 -05:00 committed by GitHub
parent ec7fdc2521
commit 2b51bc4bfa
25 changed files with 329 additions and 167 deletions

View File

@ -42,12 +42,14 @@
android:theme="@style/FrostTheme" />
<activity
android:name=".activities.WebOverlayActivity"
android:configChanges="orientation|screenSize|locale"
android:hardwareAccelerated="true"
android:label="@string/frost_name"
android:launchMode="singleTop"
android:theme="@style/FrostTheme.Overlay.Slide" />
<activity
android:name=".activities.WebOverlayBasicActivity"
android:configChanges="orientation|screenSize|locale"
android:hardwareAccelerated="true"
android:label="@string/frost_web"
android:launchMode="singleTop"
@ -55,6 +57,7 @@
<activity
android:name=".activities.FrostWebActivity"
android:autoRemoveFromRecents="true"
android:configChanges="orientation|screenSize|locale"
android:exported="true"
android:hardwareAccelerated="true"
android:label="@string/frost_web"
@ -70,6 +73,7 @@
android:autoVerify="true"
tools:ignore="UnusedAttribute">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@ -135,8 +139,6 @@
<activity
android:name=".activities.ImageActivity"
android:theme="@style/FrostTheme.Transparent" />
<activity android:name=".activities.VideoActivity"
android:theme="@style/FrostTheme.Video" />
<service
android:name=".services.NotificationService"

View File

@ -9,7 +9,7 @@ if (!window.hasOwnProperty('frost_media')) {
* Commonality; check for valid target
*/
var element = e.target || e.srcElement;
if (!element.hasAttribute("data-sigil") || !element.getAttribute("data-sigil").includes("playInlineVideo")) return;
if (!element.hasAttribute("data-sigil") || !element.getAttribute("data-sigil").toLowerCase().includes("inlinevideo")) return;
console.log("Found inline video");
element = element.parentNode;
if (!element.hasAttribute("data-store")) return;
@ -20,8 +20,8 @@ if (!window.hasOwnProperty('frost_media')) {
return;
}
if (!dataStore.src) return;
console.log("Inline video", dataStore.src);
if (typeof Frost !== 'undefined') Frost.loadVideo(dataStore.src);
console.log("Inline video " + dataStore.src);
if (typeof Frost !== 'undefined') Frost.loadVideo(dataStore.src, dataStore.animatedGifVideo);
e.stopPropagation();
e.preventDefault();
return;

View File

@ -1,20 +1,20 @@
if(!window.hasOwnProperty("frost_media")){
console.log("Registering frost_media"),
window.frost_media=!0
;var _frostMediaClick=function(t){
var e=t.target||t.srcElement
;if(e.hasAttribute("data-sigil")&&e.getAttribute("data-sigil").includes("playInlineVideo")&&(console.log("Found inline video"),
e=e.parentNode,
e.hasAttribute("data-store"))){
;var _frostMediaClick=function(e){
var t=e.target||e.srcElement
;if(t.hasAttribute("data-sigil")&&t.getAttribute("data-sigil").toLowerCase().includes("inlinevideo")&&(console.log("Found inline video"),
t=t.parentNode,
t.hasAttribute("data-store"))){
var i
;try{
i=JSON.parse(e.getAttribute("data-store"))
}catch(t){
i=JSON.parse(t.getAttribute("data-store"))
}catch(e){
return
}
i.src&&(console.log("Inline video",i.src),"undefined"!=typeof Frost&&Frost.loadVideo(i.src),
t.stopPropagation(),
t.preventDefault())
i.src&&(console.log("Inline video "+i.src),"undefined"!=typeof Frost&&Frost.loadVideo(i.src,i.animatedGifVideo),
e.stopPropagation(),
e.preventDefault())
}
}
;document.addEventListener("click",_frostMediaClick,!0)

View File

@ -1,10 +1,12 @@
package com.pitchedapps.frost.activities
import android.content.res.Configuration
import android.os.Bundle
import ca.allanwang.kau.internal.KauBaseActivity
import com.github.pwittchen.reactivenetwork.library.rx2.Connectivity
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.pitchedapps.frost.R
import com.pitchedapps.frost.contracts.VideoViewerContract
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.materialDialogThemed
@ -18,7 +20,10 @@ import io.reactivex.schedulers.Schedulers
*/
abstract class BaseActivity : KauBaseActivity() {
override fun onBackPressed() {
if (isTaskRoot && Prefs.exitConfirmation) {
if (this is MainActivity && searchView?.onBackPressed() == true) return
if (this is VideoViewerContract && videoOnBackPress()) return
if (this is MainActivity && currentFragment.onBackPressed()) return
if (this !is WebOverlayActivityBase && isTaskRoot && Prefs.exitConfirmation) {
materialDialogThemed {
title(R.string.kau_exit)
content(R.string.kau_exit_confirmation)
@ -27,12 +32,14 @@ abstract class BaseActivity : KauBaseActivity() {
onPositive { _, _ -> super.onBackPressed() }
checkBoxPromptRes(R.string.kau_do_not_show_again, false, { _, b -> Prefs.exitConfirmation = !b })
}
} else super.onBackPressed()
return
}
super.onBackPressed()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setFrostTheme()
if (this !is WebOverlayActivityBase) setFrostTheme()
}
private var networkDisposable: Disposable? = null
@ -47,8 +54,7 @@ abstract class BaseActivity : KauBaseActivity() {
networkDisposable = ReactiveNetwork.observeNetworkConnectivity(applicationContext)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
connectivity: Connectivity ->
.subscribe { connectivity: Connectivity ->
connectivity.apply {
L.d("Network connectivity changed: isAvailable: $isAvailable isRoaming: $isRoaming")
consumer(connectivity)
@ -64,12 +70,23 @@ abstract class BaseActivity : KauBaseActivity() {
override fun onResume() {
super.onResume()
disposeNetworkConnectivity()
observeNetworkConnectivity()
// disposeNetworkConnectivity()
// observeNetworkConnectivity()
}
override fun onPause() {
super.onPause()
disposeNetworkConnectivity()
// disposeNetworkConnectivity()
}
override fun onStop() {
if (this is VideoViewerContract) videoOnStop()
super.onStop()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (this is VideoViewerContract) videoViewer?.updateLocation()
}
}

View File

@ -5,7 +5,6 @@ import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.graphics.PointF
import android.graphics.drawable.ColorDrawable
import android.net.Uri
@ -48,6 +47,7 @@ import com.pitchedapps.frost.R
import com.pitchedapps.frost.contracts.ActivityWebContract
import com.pitchedapps.frost.contracts.FileChooserContract
import com.pitchedapps.frost.contracts.FileChooserDelegate
import com.pitchedapps.frost.contracts.VideoViewerContract
import com.pitchedapps.frost.dbflow.loadFbCookie
import com.pitchedapps.frost.dbflow.loadFbTabs
import com.pitchedapps.frost.enums.MainActivityLayout
@ -63,7 +63,6 @@ import com.pitchedapps.frost.utils.iab.FrostBilling
import com.pitchedapps.frost.utils.iab.IS_FROST_PRO
import com.pitchedapps.frost.utils.iab.IabMain
import com.pitchedapps.frost.views.BadgedIcon
import com.pitchedapps.frost.views.FrostVideoContainerContract
import com.pitchedapps.frost.views.FrostVideoViewer
import com.pitchedapps.frost.views.FrostViewPager
import io.reactivex.android.schedulers.AndroidSchedulers
@ -77,18 +76,18 @@ import java.util.concurrent.TimeUnit
class MainActivity : BaseActivity(),
ActivityWebContract, FileChooserContract by FileChooserDelegate(),
FrostVideoContainerContract,
VideoViewerContract,
FrostBilling by IabMain() {
lateinit var adapter: SectionsPagerAdapter
val frameWrapper: FrameLayout by bindView(R.id.frame_wrapper)
override val frameWrapper: FrameLayout by bindView(R.id.frame_wrapper)
val toolbar: Toolbar by bindView(R.id.toolbar)
val viewPager: FrostViewPager by bindView(R.id.container)
val fab: FloatingActionButton by bindView(R.id.fab)
val tabs: TabLayout by bindView(R.id.tabs)
val appBar: AppBarLayout by bindView(R.id.appbar)
val coordinator: CoordinatorLayout by bindView(R.id.main_content)
var videoViewer: FrostVideoViewer? = null
override var videoViewer: FrostVideoViewer? = null
lateinit var drawer: Drawer
lateinit var drawerHeader: AccountHeader
var webFragmentObservable = PublishSubject.create<Int>()!!
@ -130,8 +129,7 @@ class MainActivity : BaseActivity(),
"Frost id" to Prefs.frostId)
}
}
setContentView(R.layout.activity_frame_wrapper)
frameWrapper.inflate(Prefs.mainActivityLayout.layoutRes, true)
setFrameContentView(Prefs.mainActivityLayout.layoutRes)
setSupportActionBar(toolbar)
adapter = SectionsPagerAdapter(supportFragmentManager, loadFbTabs())
viewPager.adapter = adapter
@ -147,7 +145,7 @@ class MainActivity : BaseActivity(),
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
val delta: Float by lazy { positionOffset * (255 - 128).toFloat() }
val delta = positionOffset * (255 - 128).toFloat()
tabsForEachView { tabPosition, view ->
view.setAllAlpha(when (tabPosition) {
position -> 255.0f - delta
@ -169,14 +167,6 @@ class MainActivity : BaseActivity(),
onCreateBilling()
}
fun showVideo(url: String) {
if (videoViewer != null) {
videoViewer?.setVideo(url)
} else {
videoViewer = FrostVideoViewer.showVideo(url, this)
}
}
fun tabsForEachView(action: (position: Int, view: BadgedIcon) -> Unit) {
(0 until tabs.tabCount).asSequence().forEach { i ->
action(i, tabs.getTabAt(i)!!.customView as BadgedIcon)
@ -312,7 +302,7 @@ class MainActivity : BaseActivity(),
}
}
fun Builder.primaryFrostItem(item: FbItem) = this.primaryItem(item.titleId) {
private fun Builder.primaryFrostItem(item: FbItem) = this.primaryItem(item.titleId) {
iicon = item.icon
iconColor = Prefs.textColor.toLong()
textColor = Prefs.textColor.toLong()
@ -331,7 +321,7 @@ class MainActivity : BaseActivity(),
}
}
fun Builder.secondaryFrostItem(@StringRes title: Int, onClick: () -> Unit) = this.secondaryItem(title) {
private fun Builder.secondaryFrostItem(@StringRes title: Int, onClick: () -> Unit) = this.secondaryItem(title) {
textColor = Prefs.textColor.toLong()
selectedIconColor = Prefs.textColor.toLong()
selectedTextColor = Prefs.textColor.toLong()
@ -436,23 +426,11 @@ class MainActivity : BaseActivity(),
super.onStart()
}
override fun onStop() {
videoViewer?.pause()
super.onStop()
}
override fun onDestroy() {
onDestroyBilling()
super.onDestroy()
}
override fun onBackPressed() {
if (videoViewer?.onBackPressed() == true) return
if (searchView?.onBackPressed() == true) return
if (currentFragment.onBackPressed()) return
super.onBackPressed()
}
inline val currentFragment
get() = supportFragmentManager.findFragmentByTag("android:switcher:${R.id.container}:${viewPager.currentItem}") as WebFragment
@ -489,16 +467,4 @@ class MainActivity : BaseActivity(),
else
PointF(0f, 0f)
override val videoContainer: FrameLayout
get() = frameWrapper
override fun onVideoFinished() {
L.d("Video view released")
videoViewer = null
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
videoViewer?.updateLocation()
}
}

View File

@ -1,33 +0,0 @@
package com.pitchedapps.frost.activities
import android.os.Bundle
import android.view.ViewGroup
import ca.allanwang.kau.internal.KauBaseActivity
import ca.allanwang.kau.utils.bindView
import com.pitchedapps.frost.R
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.views.FrostVideoView
/**
* Created by Allan Wang on 2017-06-01.
*/
class VideoActivity : KauBaseActivity() {
val container: ViewGroup by bindView(R.id.video_container)
val video: FrostVideoView by bindView(R.id.video)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.view_video)
container.setOnTouchListener { _, event ->
val y = video.shouldParentAcceptTouch(event)
L.d("Video SPAT $y")
y
}
}
override fun onStop() {
video.pause()
super.onStop()
}
}

View File

@ -1,6 +1,7 @@
package com.pitchedapps.frost.activities
import android.content.Intent
import android.graphics.PointF
import android.net.Uri
import android.os.Bundle
import android.support.design.widget.CoordinatorLayout
@ -10,6 +11,7 @@ import android.view.Menu
import android.view.MenuItem
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.widget.FrameLayout
import ca.allanwang.kau.internal.KauBaseActivity
import ca.allanwang.kau.swipe.kauSwipeOnCreate
import ca.allanwang.kau.swipe.kauSwipeOnDestroy
@ -20,9 +22,11 @@ import com.pitchedapps.frost.R
import com.pitchedapps.frost.contracts.ActivityWebContract
import com.pitchedapps.frost.contracts.FileChooserContract
import com.pitchedapps.frost.contracts.FileChooserDelegate
import com.pitchedapps.frost.contracts.VideoViewerContract
import com.pitchedapps.frost.enums.OverlayContext
import com.pitchedapps.frost.facebook.*
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.views.FrostVideoViewer
import com.pitchedapps.frost.web.FrostWebView
import io.reactivex.disposables.Disposable
import okhttp3.HttpUrl
@ -95,9 +99,10 @@ class WebOverlayBasicActivity : WebOverlayActivityBase(true)
*/
class WebOverlayActivity : WebOverlayActivityBase(false)
open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : KauBaseActivity(),
ActivityWebContract, FileChooserContract by FileChooserDelegate() {
open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseActivity(),
ActivityWebContract, VideoViewerContract, FileChooserContract by FileChooserDelegate() {
override val frameWrapper: FrameLayout by bindView(R.id.frame_wrapper)
val toolbar: Toolbar by bindView(R.id.overlay_toolbar)
val frostWeb: FrostWebView by bindView(R.id.overlay_frost_webview)
val coordinator: CoordinatorLayout by bindView(R.id.overlay_main_content)
@ -122,7 +127,7 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : KauBas
finish()
return
}
setContentView(R.layout.activity_web_overlay)
setFrameContentView(R.layout.activity_web_overlay)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
@ -219,4 +224,13 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : KauBas
}
return true
}
/*
* ----------------------------------------------------
* Video Contract
* ----------------------------------------------------
*/
override var videoViewer: FrostVideoViewer? = null
override val lowerVideoPadding: PointF = PointF(0f, 0f)
}

View File

@ -0,0 +1,54 @@
package com.pitchedapps.frost.contracts
import android.app.Activity
import android.widget.FrameLayout
import ca.allanwang.kau.utils.inflate
import com.pitchedapps.frost.R
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.views.FrostVideoContainerContract
import com.pitchedapps.frost.views.FrostVideoViewer
/**
* Created by Allan Wang on 2017-11-10.
*/
interface VideoViewerContract : FrameWrapper, FrostVideoContainerContract {
var videoViewer: FrostVideoViewer?
fun showVideo(url: String)
= showVideo(url, false)
/**
* Create new viewer and reuse existing one
* The url will be formatted upon loading
*/
fun showVideo(url: String, repeat: Boolean) {
if (videoViewer != null)
videoViewer?.setVideo(url, repeat)
else
videoViewer = FrostVideoViewer.showVideo(url, repeat, this)
}
fun videoOnStop() = videoViewer?.pause()
fun videoOnBackPress() = videoViewer?.onBackPressed() ?: false
override val videoContainer: FrameLayout
get() = frameWrapper
override fun onVideoFinished() {
L.d("Video view released")
videoViewer = null
}
}
interface FrameWrapper {
val frameWrapper: FrameLayout
fun Activity.setFrameContentView(layoutRes: Int) {
setContentView(R.layout.activity_frame_wrapper)
frameWrapper.inflate(layoutRes, true)
}
}

View File

@ -5,7 +5,7 @@ package com.pitchedapps.frost.facebook
*/
const val HTTPS_FACEBOOK_COM = "https://facebook.com"
const val FACEBOOK_COM = "facebook.com"
const val FB_URL_BASE = "https://m.facebook.com/"
const val FB_URL_BASE = "https://m.$FACEBOOK_COM/"
fun PROFILE_PICTURE_URL(id: Long) = "https://graph.facebook.com/$id/picture?type=large"
const val USER_AGENT_FULL = "Mozilla/5.0 (Linux; Android 4.4.2; en-us; SAMSUNG SM-G900T Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.6 Chrome/28.0.1500.94 Mobile Safari/537.36"

View File

@ -31,7 +31,7 @@ class FbUrlFormatter(url: String) {
var cleanedUrl = url
discardable.forEach { cleanedUrl = cleanedUrl.replace(it, "", true) }
converter.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v, true) }
if (cleanedUrl != url) cleanedUrl = cleanedUrl.replaceFirst("&", "?")
if (cleanedUrl != url && !cleanedUrl.contains("?")) cleanedUrl = cleanedUrl.replaceFirst("&", "?")
cleanedUrl = URLDecoder.decode(cleanedUrl, StandardCharsets.UTF_8.name())
val qm = cleanedUrl.indexOf("?")
if (qm > -1) {
@ -70,6 +70,8 @@ class FbUrlFormatter(url: String) {
}
companion object {
const val VIDEO_REDIRECT = "/video_redirect/?src="
/**
* Items here are explicitly removed from the url
* Taken from FaceSlim
@ -82,7 +84,7 @@ class FbUrlFormatter(url: String) {
"https://m.facebook.com/l.php?u=",
"http://touch.facebook.com/l.php?u=",
"https://touch.facebook.com/l.php?u=",
"/video_redirect/?src="
VIDEO_REDIRECT
)
val misc = arrayOf("&amp;" to "&")

View File

@ -0,0 +1,70 @@
package com.pitchedapps.frost.utils
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.view.animation.Interpolator
/**
* Created by Allan Wang on 2017-11-10.
*/
class ProgressAnimator private constructor(private vararg val values: Float) {
companion object {
inline fun ofFloat(crossinline builder: ProgressAnimator.() -> Unit) = ofFloat(0f, 1f) { builder() }
fun ofFloat(vararg values: Float, builder: ProgressAnimator.() -> Unit) = ProgressAnimator(*values).apply {
builder()
build()
}
}
private val animators: MutableList<(Float) -> Unit> = mutableListOf()
private val startActions: MutableList<() -> Unit> = mutableListOf()
private val endActions: MutableList<() -> Unit> = mutableListOf()
var duration: Long = -1L
var interpolator: Interpolator? = null
/**
* Add more changes to the [ValueAnimator] before running
*/
var extraConfigs: ValueAnimator.() -> Unit = {}
fun withAnimator(from: Float, to: Float, animator: (Float) -> Unit) = animators.add {
val range = to - from
animator(range * it + from)
}
fun withAnimator(animator: (Float) -> Unit) = animators.add(animator)
fun withAnimatorInv(animator: (Float) -> Unit) = animators.add { animator(1f - it) }
fun withStartAction(action: () -> Unit) = startActions.add(action)
fun withEndAction(action: () -> Unit) = endActions.add(action)
fun build() {
ValueAnimator.ofFloat(*values).apply {
if (this@ProgressAnimator.duration > 0L)
duration = this@ProgressAnimator.duration
if (this@ProgressAnimator.interpolator != null)
interpolator = this@ProgressAnimator.interpolator
addUpdateListener {
val progress = it.animatedValue as Float
animators.forEach { it(progress) }
}
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
startActions.forEach { it() }
}
override fun onAnimationEnd(animation: Animator?) {
endActions.forEach { it() }
}
})
extraConfigs()
start()
}
}
}

View File

@ -30,6 +30,7 @@ import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.*
import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.facebook.*
import com.pitchedapps.frost.facebook.FbUrlFormatter.Companion.VIDEO_REDIRECT
import com.pitchedapps.frost.utils.iab.IS_FROST_PRO
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
@ -200,6 +201,9 @@ fun Context.resolveActivityForUri(uri: Uri): Boolean {
inline val String?.isFacebookUrl
get() = this != null && this.contains(FACEBOOK_COM)
inline val String?.isVideoUrl
get() = this != null && this.startsWith(VIDEO_REDIRECT)
fun Context.frostChangelog() = showChangelog(R.xml.frost_changelog, Prefs.textColor) {
theme()
if (System.currentTimeMillis() - Prefs.installDate > 2592000000) { //show after 1 month

View File

@ -8,10 +8,12 @@ import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import ca.allanwang.kau.utils.AnimHolder
import ca.allanwang.kau.utils.dpToPx
import ca.allanwang.kau.utils.scaleXY
import com.devbrackets.android.exomedia.ui.widget.VideoView
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.ProgressAnimator
/**
* Created by Allan Wang on 2017-10-13.
@ -29,10 +31,10 @@ class FrostVideoView @JvmOverloads constructor(
private inline val v
get() = videoViewImpl
var backgroundView: View? = null
var onFinishedListener: () -> Unit = {}
lateinit var viewerContract: FrostVideoViewerContract
private lateinit var viewerContract: FrostVideoViewerContract
lateinit var containerContract: FrostVideoContainerContract
var repeat: Boolean = false
private val videoDimensions = PointF(0f, 0f)
@ -47,8 +49,8 @@ class FrostVideoView @JvmOverloads constructor(
private val SWIPE_TO_CLOSE_HORIZONTAL_THRESHOLD = 2f.dpToPx
private val SWIPE_TO_CLOSE_VERTICAL_THRESHOLD = 5f.dpToPx
private val SWIPE_TO_CLOSE_OFFSET_THRESHOLD = 75f.dpToPx
val ANIMATION_DURATION = 300L
private val FAST_ANIMATION_DURATION = 100L
const val ANIMATION_DURATION = 200L
private const val FAST_ANIMATION_DURATION = 100L
}
private var videoBounds = RectF()
@ -59,19 +61,32 @@ class FrostVideoView @JvmOverloads constructor(
if (videoDimensions.x <= 0f || videoDimensions.y <= 0f)
return L.d("Attempted to toggle video expansion when points have not been finalized")
field = value
val origX = translationX
val origY = translationY
val origScale = scaleX
if (field) {
animate().scaleXY(1f).translationX(0f).translationY(0f).setDuration(ANIMATION_DURATION).withStartAction {
backgroundView?.animate()?.alpha(1f)?.setDuration(ANIMATION_DURATION)
viewerContract.onFade(1f, ANIMATION_DURATION)
}.withEndAction {
if (!isPlaying) showControls()
ProgressAnimator.ofFloat {
duration = ANIMATION_DURATION
interpolator = AnimHolder.fastOutSlowInInterpolator(context)
withAnimator { viewerContract.onExpand(it) }
withAnimator(origScale, 1f) { scaleXY = it }
withAnimator(origX, 0f) { translationX = it }
withAnimator(origY, 0f) { translationY = it }
withEndAction {
if (!isPlaying) showControls()
else viewerContract.onControlsHidden()
}
}
} else {
hideControls()
val (scale, tX, tY) = mapBounds()
animate().scaleXY(scale).translationX(tX).translationY(tY).setDuration(ANIMATION_DURATION).withStartAction {
backgroundView?.animate()?.alpha(0f)?.setDuration(ANIMATION_DURATION)
viewerContract.onFade(0f, ANIMATION_DURATION)
ProgressAnimator.ofFloat {
duration = ANIMATION_DURATION
interpolator = AnimHolder.fastOutSlowInInterpolator(context)
withAnimatorInv { viewerContract.onExpand(it) }
withAnimator(origScale, scale) { scaleXY = it }
withAnimator(origX, tX) { translationX = it }
withAnimator(origY, tY) { translationY = it }
}
}
}
@ -110,16 +125,28 @@ class FrostVideoView @JvmOverloads constructor(
if (isExpanded) showControls()
}
setOnCompletionListener {
viewerContract.onVideoComplete()
if (repeat) restart()
else viewerContract.onVideoComplete()
}
setOnTouchListener(FrameTouchListener(context))
v.setOnTouchListener(VideoTouchListener(context))
setOnVideoSizedChangedListener { intrinsicWidth, intrinsicHeight ->
val ratio = Math.min(width.toFloat() / intrinsicWidth, height.toFloat() / intrinsicHeight.toFloat())
/**
* Only remap if not expanded and if dimensions have changed
*/
val shouldRemap = !isExpanded
&& (videoDimensions.x != ratio * intrinsicWidth || videoDimensions.y != ratio * intrinsicHeight)
videoDimensions.set(ratio * intrinsicWidth, ratio * intrinsicHeight)
if (shouldRemap) updateLocation()
}
}
fun setViewerContract(contract: FrostVideoViewerContract) {
this.viewerContract = contract
videoControls?.setVisibilityListener(viewerContract)
}
fun jumpToStart() {
pause()
v.seekTo(0)
@ -136,7 +163,7 @@ class FrostVideoView @JvmOverloads constructor(
override fun restart(): Boolean {
videoUri ?: return false
if (videoViewImpl.restart() && isExpanded) {
if (videoViewImpl.restart() && isExpanded && !repeat) {
videoControls?.showLoading(true)
return true
}
@ -163,9 +190,11 @@ class FrostVideoView @JvmOverloads constructor(
fun destroy() {
stopPlayback()
if (alpha > 0f)
animate().alpha(0f).setDuration(FAST_ANIMATION_DURATION).withEndAction { onFinishedListener() }.withStartAction {
viewerContract.onFade(0f, FAST_ANIMATION_DURATION)
}.start()
ProgressAnimator.ofFloat(alpha, 0f) {
duration = FAST_ANIMATION_DURATION
withAnimator { alpha = it }
withEndAction { onFinishedListener() }
}
else
onFinishedListener()
}

View File

@ -13,6 +13,7 @@ import android.view.ViewTreeObserver
import android.widget.FrameLayout
import android.widget.ImageView
import ca.allanwang.kau.utils.*
import com.devbrackets.android.exomedia.listener.VideoControlsVisibilityListener
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.pitchedapps.frost.R
import com.pitchedapps.frost.facebook.formattedFbUrl
@ -34,18 +35,22 @@ class FrostVideoViewer @JvmOverloads constructor(
val restarter: ImageView by bindView(R.id.video_restart)
companion object {
/**
* Matches VideoControls.CONTROL_VISIBILITY_ANIMATION_LENGTH
*/
private const val CONTROL_ANIMATION_DURATION = 300L
/**
* Simplified binding to add video to layout, and remove it when finished
* This is under the assumption that the container allows for overlays,
* such as a FrameLayout
*/
fun showVideo(url: String, contract: FrostVideoContainerContract): FrostVideoViewer {
fun showVideo(url: String, repeat: Boolean, contract: FrostVideoContainerContract): FrostVideoViewer {
val container = contract.videoContainer
val videoViewer = FrostVideoViewer(container.context)
container.addView(videoViewer)
videoViewer.bringToFront()
L.d("Create video view", url)
videoViewer.setVideo(url)
videoViewer.setVideo(url, repeat)
videoViewer.video.containerContract = contract
videoViewer.video.onFinishedListener = { container.removeView(videoViewer); contract.onVideoFinished() }
return videoViewer
@ -56,11 +61,9 @@ class FrostVideoViewer @JvmOverloads constructor(
inflate(R.layout.view_video, true)
alpha = 0f
background.setBackgroundColor(if (Prefs.bgColor.isColorDark) Prefs.bgColor.withMinAlpha(200) else Color.BLACK)
video.backgroundView = background
video.viewerContract = this
video.setViewerContract(this)
video.pause()
toolbar.inflateMenu(R.menu.menu_video)
toolbar.setBackgroundColor(Prefs.headerColor)
context.setMenuIcons(toolbar.menu, Prefs.iconColor,
R.id.action_pip to GoogleMaterial.Icon.gmd_picture_in_picture_alt,
R.id.action_download to GoogleMaterial.Icon.gmd_file_download
@ -77,12 +80,14 @@ class FrostVideoViewer @JvmOverloads constructor(
video.restart()
restarter.fadeOut { restarter.gone() }
}
// toolbar.setOnTouchListener { _, event -> video.shouldParentAcceptTouch(event) }
}
fun setVideo(url: String) {
fun setVideo(url: String, repeat: Boolean = false) {
val formattedUrl = url.formattedFbUrl
L.d("Load video view; repeat: $repeat", url)
animate().alpha(1f).setDuration(FrostVideoView.ANIMATION_DURATION).start()
video.setVideoURI(Uri.parse(url.formattedFbUrl))
video.setVideoURI(Uri.parse(formattedUrl))
video.repeat = repeat
}
/**
@ -106,10 +111,9 @@ class FrostVideoViewer @JvmOverloads constructor(
* -------------------------------------------------------------
*/
override fun onFade(alpha: Float, duration: Long) {
toolbar.visible().animate().alpha(alpha).setDuration(duration).withEndAction {
if (alpha == 0f) toolbar.gone()
}
override fun onExpand(progress: Float) {
toolbar.goneIf(progress == 0f).alpha = progress
background.alpha = progress
}
override fun onSingleTapConfirmed(event: MotionEvent): Boolean {
@ -134,11 +138,26 @@ class FrostVideoViewer @JvmOverloads constructor(
})
}
override fun onControlsShown() {
if (video.isExpanded)
toolbar.fadeIn(duration = CONTROL_ANIMATION_DURATION, onStart = { toolbar.visible() })
}
override fun onControlsHidden() {
if (!toolbar.isGone)
toolbar.fadeOut(duration = CONTROL_ANIMATION_DURATION) { toolbar.gone() }
}
}
interface FrostVideoViewerContract {
interface FrostVideoViewerContract : VideoControlsVisibilityListener {
fun onSingleTapConfirmed(event: MotionEvent): Boolean
fun onFade(alpha: Float, duration: Long)
/**
* Process of expansion
* 1f represents an expanded view, 0f represents a minimized view
*/
fun onExpand(progress: Float)
fun onVideoComplete()
}

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.support.v4.widget.SwipeRefreshLayout
import android.webkit.JavascriptInterface
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.contracts.VideoViewerContract
import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.utils.*
@ -36,9 +37,10 @@ class FrostJSI(val webView: FrostWebViewCore) {
= if (url == null) false else webView.requestWebOverlay(url)
@JavascriptInterface
fun loadVideo(url: String?) {
fun loadVideo(url: String?, isGif: Boolean) {
if (url != null)
webView.post { activity?.showVideo(url) }
webView.post { (context as? VideoViewerContract)?.showVideo(url, isGif)
?: L.d("Could not load video; contract not implemented") }
}
@JavascriptInterface

View File

@ -3,13 +3,12 @@ package com.pitchedapps.frost.web
import com.pitchedapps.frost.activities.WebOverlayActivity
import com.pitchedapps.frost.activities.WebOverlayActivityBase
import com.pitchedapps.frost.activities.WebOverlayBasicActivity
import com.pitchedapps.frost.contracts.VideoViewerContract
import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
import com.pitchedapps.frost.facebook.formattedFbUrl
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.isFacebookUrl
import com.pitchedapps.frost.utils.launchWebOverlay
import com.pitchedapps.frost.utils.*
/**
* Created by Allan Wang on 2017-08-15.
@ -26,6 +25,12 @@ import com.pitchedapps.frost.utils.launchWebOverlay
*/
fun FrostWebViewCore.requestWebOverlay(url: String): Boolean {
if (url == "#") return false
if (url.isVideoUrl && context is VideoViewerContract) {
L.i("Found video", url)
(context as VideoViewerContract).showVideo(url)
return true
}
if (!Prefs.overlayEnabled) return false
if (context is WebOverlayActivityBase) {
L.v("Check web request from overlay", url)
//already overlay; manage user agent
@ -73,7 +78,8 @@ fun FrostWebViewCore.requestWebOverlay(url: String): Boolean {
val messageWhitelist = setOf(FbItem.MESSAGES, FbItem.CHAT, FbItem.FEED_MOST_RECENT, FbItem.FEED_TOP_STORIES).map { it.url }.toSet()
val String.shouldUseBasicAgent
get() = (messageWhitelist.any { contains(it) }) || this == FB_URL_BASE
get() = !contains("story.php") //we will use basic agent for anything that isn't a comment section
// get() = (messageWhitelist.any { contains(it) }) || this == FB_URL_BASE
/**
* The following components should never be launched in a new overlay

View File

@ -106,11 +106,11 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient
injectBackgroundColor()
webCore.jsInject(
JsActions.LOGIN_CHECK,
JsAssets.CLICK_A.maybe(Prefs.overlayEnabled),
JsAssets.CLICK_A,
JsAssets.TEXTAREA_LISTENER,
CssHider.ADS.maybe(!Prefs.showFacebookAds && IS_FROST_PRO),
JsAssets.CONTEXT_A,
JsAssets.MEDIA.maybe(webCore.baseEnum != null),
JsAssets.MEDIA,
JsAssets.HEADER_BADGES.maybe(webCore.baseEnum != null)
)
}

View File

@ -2,4 +2,5 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/frame_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:fitsSystemWindows="true" />

View File

@ -2,9 +2,9 @@
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/container"
android:paddingBottom="@dimen/kau_activity_vertical_margin"
android:paddingEnd="@dimen/kau_activity_horizontal_margin"
android:paddingStart="@dimen/kau_activity_horizontal_margin"
@ -17,7 +17,6 @@
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/kau_activity_vertical_margin"
android:text="@string/select_facebook_account"
android:textColor="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
@ -32,21 +31,10 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_select_account"
app:layout_constraintVertical_bias="0.3"
tools:layout_editor_absoluteX="0dp"
tools:layout_editor_absoluteY="0dp" />
<!--<android.support.v7.widget.AppCompatTextView-->
<!--android:layout_width="wrap_content"-->
<!--android:layout_height="wrap_content"-->
<!--android:layout_gravity="center_horizontal"-->
<!--android:text="@string/select_facebook_account"-->
<!--app:layout_constraintEnd_toEndOf="parent"-->
<!--app:layout_constraintHorizontal_bias="0.5"-->
<!--app:layout_constraintStart_toStartOf="parent"-->
<!--app:layout_constraintTop_toBottomOf="@userId/selector_recycler"-->
<!--tools:layout_editor_absoluteX="8dp"-->
<!--tools:layout_editor_absoluteY="0dp" />-->
</android.support.constraint.ConstraintLayout>

View File

@ -14,16 +14,10 @@
android:layout_height="match_parent"
android:clickable="false" />
<android.support.v7.widget.Toolbar
android:id="@+id/video_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
<com.pitchedapps.frost.views.FrostVideoView
android:id="@+id/video"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize"
android:background="@android:color/transparent"
android:theme="@style/FrostTheme.Video"
app:useDefaultControls="true"
@ -38,5 +32,10 @@
</com.pitchedapps.frost.views.FrostVideoView>
<android.support.v7.widget.Toolbar
android:id="@+id/video_toolbar"
android:layout_width="match_parent"
android:background="@drawable/exomedia_default_controls_interactive_background"
android:layout_height="?attr/actionBarSize" />
</merge>

View File

@ -6,6 +6,14 @@
<item text="" />
-->
<version title="v1.6.3" />
<item text="Allow for truly full screen videos" />
<item text="Support pip video everywhere" />
<item text="Support gifs" />
<item text="" />
<item text="" />
<item text="" />
<version title="v1.6.2" />
<item text="Fix search update from Facebook" />
<item text="Fix url parsing errors again" />

View File

@ -48,4 +48,13 @@ class FbUrlTest {
assertFbFormat("${FB_URL_BASE}relative", "$FB_URL_BASE/relative")
}
@Test
fun video() {
//note that the video numbers have been changed to maintain privacy
val url = "/video_redirect/?src=https%3A%2F%2Fvideo-yyz1-1.xx.fbcdn.net%2Fv%2Ft42.1790-2%2F2349078999904_n.mp4%3Fefg%3DeyJ87J9%26oh%3Df5777784%26oe%3D56FD4&source=media_collage&id=1735049&refid=8&_ft_=qid.6484464%3Amf_story_key.-43172431214%3Atop_level_post_id.102773&__tn__=FEH-R"
val expected = "https://video-yyz1-1.xx.fbcdn.net/v/t42.1790-2/2349078999904_n.mp4?efg=eyJ87J9&oh=f5777784&oe=56FD4?source&id=1735049&_ft_=qid.6484464:mf_story_key.-43172431214:top_level_post_id.102773&__tn__=FEH-R"
assertFbFormat(expected, url)
}
}

View File

@ -7,7 +7,7 @@ buildscript {
maven { url 'https://maven.fabric.io/public' }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0-rc1'
classpath 'com.android.tools.build:gradle:3.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${KOTLIN}"
classpath 'io.fabric.tools:gradle:1.+'
classpath 'com.github.triplet.gradle:play-publisher:1.2.0'

View File

@ -1,5 +1,10 @@
# Changelog
## v1.6.3
* Allow for truly full screen videos
* Support pip video everywhere
* Support gifs
## v1.6.2
* Fix search update from Facebook
* Fix url parsing errors again

View File

@ -45,4 +45,4 @@ JUNIT=4.12
TEST_RULE=0.5
TEST_RUNNER=1.0.0
android.enableAapt2=false
#android.enableAapt2=false