...
This commit is contained in:
@@ -21,13 +21,13 @@ import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import bums.lunatic.launcher.R
|
||||
import bums.lunatic.launcher.player.PlayerActivity
|
||||
import com.bumptech.glide.Glide
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -166,7 +166,14 @@ class CompletedFilesFragment : Fragment() {
|
||||
currentDir = file
|
||||
loadFiles()
|
||||
} else {
|
||||
openPrivateFile(requireContext(), file)
|
||||
if (extVideos.contains(file.extension.lowercase())) {
|
||||
val intent = Intent(requireContext(), PlayerActivity::class.java).apply {
|
||||
putExtra("VIDEO_PATH", file.absolutePath)
|
||||
}
|
||||
startActivity(intent)
|
||||
} else {
|
||||
openPrivateFile(requireContext(), file) // 이미지나 문서는 기존처럼
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -339,12 +339,10 @@ open class GeckoWeb @JvmOverloads constructor(
|
||||
if (element.type == GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE ||
|
||||
element.type == GeckoSession.ContentDelegate.ContextElement.TYPE_VIDEO) {
|
||||
|
||||
showContextMenu(pageUrl, mediaUrl)
|
||||
|
||||
// 로그인 확인 후 메뉴 표시
|
||||
BookmarkUploader.loginAndGetToken(
|
||||
userId = "lunaticbum", userPw = "VioPup*383",
|
||||
onSuccess = { showContextMenu(pageUrl, mediaUrl); context.toast("로그인 성공") },
|
||||
onError = { context.toast("로그인 실패: $it") }
|
||||
)
|
||||
|
||||
} else {
|
||||
super.onContextMenu(session, screenX, screenY, element)
|
||||
}
|
||||
@@ -782,8 +780,18 @@ open class GeckoWeb @JvmOverloads constructor(
|
||||
val menuItems = arrayOf("이 미디어만 북마크", "페이지의 모든 이미지 북마크", "다운로드")
|
||||
AlertDialog.Builder(context).setTitle("작업 선택").setItems(menuItems) { _, which ->
|
||||
when (menuItems[which]) {
|
||||
"이 미디어만 북마크" -> startBookmarkSaveProcessForSingleImage(pageUrl, mediaUrl)
|
||||
"페이지의 모든 이미지 북마크" -> sendJsonMsg("fetchAllImages", "targetSrc" to mediaUrl)
|
||||
"이 미디어만 북마크" ->
|
||||
BookmarkUploader.loginAndGetToken(
|
||||
userId = "lunaticbum", userPw = "VioPup*383",
|
||||
onSuccess = { context.toast("로그인 성공");startBookmarkSaveProcessForSingleImage(pageUrl, mediaUrl) },
|
||||
onError = { context.toast("로그인 실패: $it") }
|
||||
)
|
||||
"페이지의 모든 이미지 북마크" ->
|
||||
BookmarkUploader.loginAndGetToken(
|
||||
userId = "lunaticbum", userPw = "VioPup*383",
|
||||
onSuccess = { context.toast("로그인 성공");sendJsonMsg("fetchAllImages", "targetSrc" to mediaUrl) },
|
||||
onError = { context.toast("로그인 실패: $it") }
|
||||
)
|
||||
"다운로드" -> CommonUtils.downloadFileWithOkHttp(context, Uri.parse(pageUrl), mediaUrl)
|
||||
}
|
||||
}.show()
|
||||
|
||||
@@ -617,10 +617,10 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
|
||||
}
|
||||
}
|
||||
|
||||
private var originalVolume: Int = -1
|
||||
private val audioManager by lazy {
|
||||
requireContext().getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
}
|
||||
// private var originalVolume: Int = -1
|
||||
// private val audioManager by lazy {
|
||||
// requireContext().getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
// }
|
||||
override fun onHiddenChanged(hidden: Boolean) {
|
||||
super.onHiddenChanged(hidden)
|
||||
saveContinuation = false
|
||||
@@ -836,21 +836,21 @@ class TokiFragment : RemoteGestureFragment(), PagedTextViewInterface,KeyEventHan
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
originalVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
if (contentsType.contains("youtube")) {
|
||||
// 미디어 볼륨을 0으로 설정 (FLAG_SHOW_UI를 0으로 주면 볼륨 바가 뜨지 않음)
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0)
|
||||
}
|
||||
|
||||
Blog.LOGE("RssHome 활성화: 미디어 볼륨 0 설정 (이전 볼륨: $originalVolume)")
|
||||
// originalVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
// if (contentsType.contains("youtube")) {
|
||||
// // 미디어 볼륨을 0으로 설정 (FLAG_SHOW_UI를 0으로 주면 볼륨 바가 뜨지 않음)
|
||||
// audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0)
|
||||
// }
|
||||
//
|
||||
// Blog.LOGE("RssHome 활성화: 미디어 볼륨 0 설정 (이전 볼륨: $originalVolume)")
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (originalVolume != -1 && contentsType.contains("youtube")) {
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, originalVolume, 0)
|
||||
Blog.LOGE("RssHome 비활성화: 미디어 볼륨 복구 ($originalVolume)")
|
||||
}
|
||||
// if (originalVolume != -1 && contentsType.contains("youtube")) {
|
||||
// audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, originalVolume, 0)
|
||||
// Blog.LOGE("RssHome 비활성화: 미디어 볼륨 복구 ($originalVolume)")
|
||||
// }
|
||||
}
|
||||
|
||||
fun getLastinfo() : LastInfo? {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package bums.lunatic.launcher.player
|
||||
|
||||
import android.view.Surface
|
||||
|
||||
class NativePlayer {
|
||||
private var nativeHandle: Long = 0
|
||||
private var subtitleCallback: ((String) -> Unit)? = null
|
||||
private var videoSizeCallback: ((Int, Int) -> Unit)? = null
|
||||
|
||||
fun initialize(): Boolean {
|
||||
nativeHandle = nativeInit()
|
||||
return nativeHandle != 0L
|
||||
}
|
||||
|
||||
fun setDataSource(videoFd: Int, subPath: String) = nativeSetDataSource(nativeHandle, videoFd, subPath)
|
||||
fun play(surface: Surface) = nativePlay(nativeHandle, surface)
|
||||
fun stop() = nativeStop(nativeHandle)
|
||||
fun seekBy(sec: Double) = nativeSeekBy(nativeHandle, sec)
|
||||
fun setSpeed(speed: Float) = nativeSetSpeed(nativeHandle, speed)
|
||||
fun pause() {nativePause(nativeHandle)}
|
||||
|
||||
@Suppress("unused")
|
||||
private fun onSubtitleTextDecoded(text: String) {
|
||||
subtitleCallback?.invoke(text)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
private fun onVideoSizeChanged(w: Int, h: Int) {
|
||||
videoSizeCallback?.invoke(w, h)
|
||||
}
|
||||
|
||||
fun setSubtitleCallback(cb: (String) -> Unit) { subtitleCallback = cb }
|
||||
fun setVideoSizeCallback(cb: (Int, Int) -> Unit) { videoSizeCallback = cb }
|
||||
|
||||
fun destroy() {
|
||||
if (nativeHandle != 0L) {
|
||||
nativeDestroy(nativeHandle)
|
||||
nativeHandle = 0L
|
||||
}
|
||||
}
|
||||
private external fun nativePause(h: Long)
|
||||
private external fun nativeInit(): Long
|
||||
private external fun nativeSetDataSource(h: Long, fd: Int, sub: String)
|
||||
private external fun nativePlay(h: Long, s: Surface)
|
||||
private external fun nativeStop(h: Long)
|
||||
private external fun nativeDestroy(h: Long)
|
||||
private external fun nativeSeekBy(h: Long, s: Double)
|
||||
private external fun nativeSetSpeed(h: Long, sp: Float)
|
||||
|
||||
companion object {
|
||||
init { System.loadLibrary("native_renderer") }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package bums.lunatic.launcher.player
|
||||
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.Color
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.os.Bundle
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import bums.lunatic.launcher.R
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
|
||||
class PlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
|
||||
|
||||
private lateinit var videoTextureView: TextureView
|
||||
private lateinit var subtitleView: TextView
|
||||
private lateinit var btnRotate: ImageButton
|
||||
private lateinit var btnHideVideo: ImageButton
|
||||
|
||||
private var nativePlayer: NativePlayer? = null
|
||||
private var videoPath: String = ""
|
||||
private var subtitlePath: String = ""
|
||||
|
||||
private var isPlaying = true
|
||||
private var isVideoHidden = false
|
||||
private var leftLongPressJob: Job? = null
|
||||
|
||||
private var videoWidth: Int = 0
|
||||
private var videoHeight: Int = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// 1. 데이터 확인
|
||||
videoPath = intent.getStringExtra("VIDEO_PATH") ?: ""
|
||||
if (videoPath.isEmpty()) { finish(); return }
|
||||
subtitlePath = findSubtitleFile(videoPath)
|
||||
|
||||
// 2. UI 동적 생성 및 구성
|
||||
setupUI()
|
||||
|
||||
// 3. 네이티브 플레이어 초기화
|
||||
nativePlayer = NativePlayer().apply {
|
||||
initialize()
|
||||
// 영상 해상도에 따른 화면 크기 조절 콜백
|
||||
setVideoSizeCallback { width, height ->
|
||||
videoWidth = width
|
||||
videoHeight = height
|
||||
adjustVideoAspectRatio(width, height)
|
||||
}
|
||||
// 자막 출력 콜백
|
||||
setSubtitleCallback { text ->
|
||||
runOnUiThread {
|
||||
subtitleView.text = cleanSubtitleText(text)
|
||||
subtitleView.visibility = if (text.isEmpty()) View.INVISIBLE else View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 제스처 설정
|
||||
setupGestures()
|
||||
}
|
||||
|
||||
private fun setupUI() {
|
||||
val root = FrameLayout(this).apply { setBackgroundColor(Color.BLACK) }
|
||||
|
||||
// 비디오 뷰
|
||||
videoTextureView = TextureView(this).apply {
|
||||
surfaceTextureListener = this@PlayerActivity
|
||||
}
|
||||
|
||||
// 자막 뷰
|
||||
subtitleView = TextView(this).apply {
|
||||
setTextColor(Color.WHITE)
|
||||
textSize = 22f
|
||||
setShadowLayer(8f, 0f, 0f, Color.BLACK)
|
||||
gravity = Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM
|
||||
setPadding(40, 0, 40, 180)
|
||||
}
|
||||
|
||||
// 제스처 감지용 투명 레이어 (삼등분)
|
||||
val gestureLayer = android.widget.LinearLayout(this).apply {
|
||||
orientation = android.widget.LinearLayout.HORIZONTAL
|
||||
weightSum = 3f
|
||||
}
|
||||
|
||||
val leftZone = View(this).apply { id = View.generateViewId() }
|
||||
val centerZone = View(this).apply { id = View.generateViewId() }
|
||||
val rightZone = View(this).apply { id = View.generateViewId() }
|
||||
|
||||
gestureLayer.addView(leftZone, android.widget.LinearLayout.LayoutParams(0, -1, 1f))
|
||||
gestureLayer.addView(centerZone, android.widget.LinearLayout.LayoutParams(0, -1, 1f))
|
||||
gestureLayer.addView(rightZone, android.widget.LinearLayout.LayoutParams(0, -1, 1f))
|
||||
|
||||
// 컨트롤 버튼 (좌측 하단 회전, 우측 하단 숨기기)
|
||||
val controls = FrameLayout(this)
|
||||
btnRotate = ImageButton(this).apply {
|
||||
setImageResource(android.R.drawable.ic_menu_rotate) // 기본 리소스 사용
|
||||
setBackgroundColor(Color.TRANSPARENT)
|
||||
setOnClickListener { toggleOrientation() }
|
||||
}
|
||||
btnHideVideo = ImageButton(this).apply {
|
||||
setImageResource(android.R.drawable.ic_menu_close_clear_cancel)
|
||||
setBackgroundColor(Color.TRANSPARENT)
|
||||
setOnClickListener { toggleVideoVisibility() }
|
||||
}
|
||||
|
||||
root.addView(videoTextureView, FrameLayout.LayoutParams(-2, -2, Gravity.CENTER))
|
||||
root.addView(subtitleView)
|
||||
root.addView(gestureLayer)
|
||||
|
||||
// 버튼 배치
|
||||
root.addView(btnRotate, FrameLayout.LayoutParams(150, 150, Gravity.BOTTOM or Gravity.START).apply { setMargins(30,0,0,30) })
|
||||
root.addView(btnHideVideo, FrameLayout.LayoutParams(150, 150, Gravity.BOTTOM or Gravity.END).apply { setMargins(0,0,30,30) })
|
||||
|
||||
setContentView(root)
|
||||
hideSystemUI()
|
||||
}
|
||||
|
||||
private fun setupGestures() {
|
||||
val gestureLayer = (videoTextureView.parent as FrameLayout).getChildAt(2) as android.widget.LinearLayout
|
||||
val left = gestureLayer.getChildAt(0)
|
||||
val center = gestureLayer.getChildAt(1)
|
||||
val right = gestureLayer.getChildAt(2)
|
||||
|
||||
// 1. 가운데: 재생/일시정지
|
||||
center.setOnClickListener {
|
||||
isPlaying = !isPlaying
|
||||
if (isPlaying) nativePlayer?.play(Surface(videoTextureView.surfaceTexture))
|
||||
else nativePlayer?.pause()
|
||||
}
|
||||
|
||||
// 2. 오른쪽: 롱프레스 4배속
|
||||
val rightDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onLongPress(e: MotionEvent) { nativePlayer?.setSpeed(4.0f) }
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
nativePlayer?.seekBy(20.0) // 탭하면 10초 앞으로
|
||||
return true
|
||||
}
|
||||
})
|
||||
right.setOnTouchListener { v, event ->
|
||||
rightDetector.onTouchEvent(event)
|
||||
if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
|
||||
nativePlayer?.setSpeed(1.0f)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// 3. 왼쪽: 롱프레스 주기적 뒤로가기
|
||||
val leftDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onLongPress(e: MotionEvent) {
|
||||
leftLongPressJob = CoroutineScope(Dispatchers.Main).launch {
|
||||
while (isActive) {
|
||||
nativePlayer?.seekBy(-20.0)
|
||||
delay(500)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
nativePlayer?.seekBy(-10.0) // 탭하면 10초 뒤로
|
||||
return true
|
||||
}
|
||||
})
|
||||
left.setOnTouchListener { v, event ->
|
||||
leftDetector.onTouchEvent(event)
|
||||
if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
|
||||
leftLongPressJob?.cancel()
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
|
||||
// 시스템 UI(상태바 등)를 다시 숨깁니다.
|
||||
hideSystemUI()
|
||||
|
||||
// 저장해둔 영상 해상도가 있다면 현재 바뀐 화면 크기에 맞춰 다시 정렬합니다.
|
||||
if (videoWidth > 0 && videoHeight > 0) {
|
||||
adjustVideoAspectRatio(videoWidth, videoHeight)
|
||||
}
|
||||
}
|
||||
|
||||
private fun adjustVideoAspectRatio(videoW: Int, videoH: Int) {
|
||||
runOnUiThread {
|
||||
// 1. 현재 기기의 실제 가용 화면 크기를 가져옵니다.
|
||||
val displayMetrics = resources.displayMetrics
|
||||
val screenW = displayMetrics.widthPixels
|
||||
val screenH = displayMetrics.heightPixels
|
||||
|
||||
// 2. 종횡비를 계산합니다.
|
||||
val videoRatio = videoW.toFloat() / videoH.toFloat()
|
||||
val screenRatio = screenW.toFloat() / screenH.toFloat()
|
||||
|
||||
val lp = videoTextureView.layoutParams as FrameLayout.LayoutParams
|
||||
|
||||
if (videoRatio > screenRatio) {
|
||||
// 💡 영상이 화면보다 더 가로로 긴 경우 (가로를 꽉 채우고 세로는 비율대로 축소)
|
||||
lp.width = screenW
|
||||
lp.height = (screenW / videoRatio).toInt()
|
||||
} else {
|
||||
// 💡 영상이 화면보다 더 세로로 긴 경우 (세로를 꽉 채우고 가로는 비율대로 축소)
|
||||
lp.width = (screenH * videoRatio).toInt()
|
||||
lp.height = screenH
|
||||
}
|
||||
|
||||
// 3. 뷰를 화면 정중앙에 배치합니다.
|
||||
lp.gravity = Gravity.CENTER
|
||||
videoTextureView.layoutParams = lp
|
||||
|
||||
Log.d("Player", "Video Resized: ${lp.width}x${lp.height} for Screen: ${screenW}x${screenH}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleOrientation() {
|
||||
requestedOrientation = if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
|
||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
|
||||
private fun toggleVideoVisibility() {
|
||||
isVideoHidden = !isVideoHidden
|
||||
videoTextureView.visibility = if (isVideoHidden) View.INVISIBLE else View.VISIBLE
|
||||
}
|
||||
|
||||
private fun findSubtitleFile(videoPath: String): String {
|
||||
val file = File(videoPath)
|
||||
val name = file.nameWithoutExtension
|
||||
val extensions = listOf("srt", "ass", "smi")
|
||||
for (ext in extensions) {
|
||||
val sub = File(file.parent, "$name.$ext")
|
||||
if (sub.exists()) return sub.absolutePath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun cleanSubtitleText(text: String): String = text.replace(Regex("\\{.*?\\}"), "")
|
||||
|
||||
override fun onSurfaceTextureAvailable(st: SurfaceTexture, w: Int, h: Int) {
|
||||
val file = File(videoPath)
|
||||
if (file.exists()) {
|
||||
val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
nativePlayer?.setDataSource(pfd.detachFd(), subtitlePath)
|
||||
nativePlayer?.play(Surface(st))
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideSystemUI() {
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureSizeChanged(st: SurfaceTexture, w: Int, h: Int) {}
|
||||
override fun onSurfaceTextureDestroyed(st: SurfaceTexture): Boolean { nativePlayer?.stop(); return true }
|
||||
override fun onSurfaceTextureUpdated(st: SurfaceTexture) {}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
nativePlayer?.destroy()
|
||||
leftLongPressJob?.cancel()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user