Skip to content

Commit

Permalink
[feature|optimize] Support taking screenshots of videos; optimize code
Browse files Browse the repository at this point in the history
  • Loading branch information
SkyD666 committed May 18, 2024
1 parent 3d0ea3b commit 13d13aa
Show file tree
Hide file tree
Showing 19 changed files with 330 additions and 92 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ android {
minSdk = 24
targetSdk = 34
versionCode = 16
versionName = "1.1-beta27"
versionName = "1.1-beta28"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Expand Down
11 changes: 10 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
tools:node="remove">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission
android:name="android.permission.READ_MEDIA_IMAGES"
tools:ignore="SelectedPhotoAccess" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
tools:ignore="ScopedStorage"
tools:node="replace" />

<application
android:name=".App"
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/com/skyd/anivu/config/Const.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.skyd.anivu.config

import android.os.Environment
import com.skyd.anivu.appContext
import java.io.File

Expand Down Expand Up @@ -44,4 +45,6 @@ object Const {
.apply { if (!exists()) mkdirs() }
val MPV_FONT_DIR = File(MPV_CONFIG_DIR, "Font")
.apply { if (!exists()) mkdirs() }

val PICTURES_DIR = appContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!
}
32 changes: 32 additions & 0 deletions app/src/main/java/com/skyd/anivu/ext/FileExt.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.skyd.anivu.ext

import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import androidx.core.content.FileProvider
import java.io.File

Expand All @@ -13,3 +17,31 @@ fun File.deleteRecursivelyExclude(hook: (File) -> Boolean = { true }): Boolean =
walkBottomUp().fold(true) { res, it ->
(it != this && hook(it) && (it.delete() || !it.exists())) && res
}

fun File.savePictureToMediaStore(context: Context, autoDelete: Boolean = true) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues()
contentValues.put(
MediaStore.Images.Media.RELATIVE_PATH,
"${Environment.DIRECTORY_PICTURES}/AniVu",
)
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, name)
val uri = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)!!
context.contentResolver.openOutputStream(uri)?.use { output ->
inputStream().use { input ->
input.copyTo(output)
}
}
} else {
val dir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"AniVu"
)
if (!dir.exists()) dir.mkdirs()
this.copyTo(File(dir, name))
}
if (autoDelete) delete()
}
2 changes: 2 additions & 0 deletions app/src/main/java/com/skyd/anivu/ext/PreferenceExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.skyd.anivu.model.preference.behavior.article.DeduplicateTitleInDescPr
import com.skyd.anivu.model.preference.behavior.feed.HideEmptyDefaultPreference
import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference
import com.skyd.anivu.model.preference.player.PlayerShow85sButtonPreference
import com.skyd.anivu.model.preference.player.PlayerShowScreenshotButtonPreference

fun Preferences.toSettings(): Settings {
return Settings(
Expand All @@ -38,5 +39,6 @@ fun Preferences.toSettings(): Settings {
// Player
playerDoubleTap = PlayerDoubleTapPreference.fromPreferences(this),
playerShow85sButton = PlayerShow85sButtonPreference.fromPreferences(this),
playerShowScreenshotButton = PlayerShowScreenshotButtonPreference.fromPreferences(this),
)
}
4 changes: 4 additions & 0 deletions app/src/main/java/com/skyd/anivu/model/preference/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.skyd.anivu.model.preference.behavior.article.DeduplicateTitleInDescPr
import com.skyd.anivu.model.preference.behavior.feed.HideEmptyDefaultPreference
import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference
import com.skyd.anivu.model.preference.player.PlayerShow85sButtonPreference
import com.skyd.anivu.model.preference.player.PlayerShowScreenshotButtonPreference
import com.skyd.anivu.ui.local.LocalArticleSwipeLeftAction
import com.skyd.anivu.ui.local.LocalArticleTapAction
import com.skyd.anivu.ui.local.LocalDarkMode
Expand All @@ -31,6 +32,7 @@ import com.skyd.anivu.ui.local.LocalIgnoreUpdateVersion
import com.skyd.anivu.ui.local.LocalNavigationBarLabel
import com.skyd.anivu.ui.local.LocalPlayerDoubleTap
import com.skyd.anivu.ui.local.LocalPlayerShow85sButton
import com.skyd.anivu.ui.local.LocalPlayerShowScreenshotButton
import com.skyd.anivu.ui.local.LocalTextFieldStyle
import com.skyd.anivu.ui.local.LocalTheme
import kotlinx.coroutines.Dispatchers
Expand All @@ -54,6 +56,7 @@ data class Settings(
// Player
val playerDoubleTap: String = PlayerDoubleTapPreference.default,
val playerShow85sButton: Boolean = PlayerShow85sButtonPreference.default,
val playerShowScreenshotButton: Boolean = PlayerShowScreenshotButtonPreference.default,
)

@Composable
Expand Down Expand Up @@ -82,6 +85,7 @@ fun SettingsProvider(
// Player
LocalPlayerDoubleTap provides settings.playerDoubleTap,
LocalPlayerShow85sButton provides settings.playerShow85sButton,
LocalPlayerShowScreenshotButton provides settings.playerShowScreenshotButton,
) {
content()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.skyd.anivu.model.preference.player

import android.content.Context
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import com.skyd.anivu.base.BasePreference
import com.skyd.anivu.ext.dataStore
import com.skyd.anivu.ext.put
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

object PlayerShowScreenshotButtonPreference : BasePreference<Boolean> {
private const val PLAYER_SHOW_SCREENSHOT_BUTTON = "playerShowScreenshotButton"
override val default = true

val key = booleanPreferencesKey(PLAYER_SHOW_SCREENSHOT_BUTTON)

fun put(context: Context, scope: CoroutineScope, value: Boolean) {
scope.launch(Dispatchers.IO) {
context.dataStore.put(key, value)
}
}

override fun fromPreferences(preferences: Preferences): Boolean = preferences[key] ?: default
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.app.NotificationManager
import android.content.Context
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.navigation.NavDeepLinkBuilder
Expand Down Expand Up @@ -340,7 +341,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) :
}

is TorrentErrorAlert -> {
// 下载错误
// Download error
pauseWorker(
handle = alert.handle(),
state = DownloadInfoBean.DownloadState.ErrorPaused,
Expand All @@ -360,7 +361,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) :

is StorageMovedAlert -> {
alert.handle().saveResumeData()
updateNotificationAsync() // 更新Notification
updateNotificationAsync() // update Notification
updateDownloadStateAndSessionParams(
link = torrentLink,
sessionStateData = sessionManager.saveState() ?: byteArrayOf(),
Expand All @@ -369,6 +370,13 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) :
}

is StorageMovedFailedAlert -> {
Log.e(
TAG,
"StorageMovedFailedAlert: " +
"Message: ${alert.message()}\n" +
"${alert.error()}\n" +
"Path: ${alert.filePath()}"
)
// 文件移动,例如存储空间已满
alert.handle().saveResumeData()
pauseWorker(
Expand All @@ -378,12 +386,12 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) :
}

is TorrentFinishedAlert -> {
// 下载完成更新
// Torrent download finished
val handle = alert.handle()
progress = 1f
name = handle.name
handle.saveResumeData()
updateNotificationAsync() // 更新Notification
updateNotificationAsync() // update Notification
moveFromDownloadingDirToVideoDir(handle = handle)
updateDownloadStateAndSessionParams(
link = torrentLink,
Expand All @@ -409,7 +417,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) :
val handle = alert.handle()
updateTorrentFilesToDb(link = torrentLink, files = handle.torrentFile().files())
name = handle.name
updateNotificationAsync() // 更新Notification
updateNotificationAsync() // update Notification
updateNameInfoToDb(link = torrentLink, name = name)
}

Expand All @@ -420,7 +428,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) :
}

is StateChangedAlert -> {
// 下载状态更新
// Download state update
if (alert.state == TorrentStatus.State.SEEDING) {
updateDownloadStateAndSessionParams(
link = torrentLink,
Expand All @@ -433,7 +441,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) :
val handle = alert.handle()
if (handle.isValid) {
progress = handle.status().progress()
updateNotificationAsync() // 更新Notification
updateNotificationAsync() // update Notification
updateProgressInfoToDb(link = torrentLink, progress = progress)
}
}
Expand All @@ -451,13 +459,13 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) :

is TorrentAlert<*> -> {
// Log.e("TAG", "onAlert: ${alert}")
// 下载进度更新
// Download progress update
val handle = alert.handle()
if (handle.isValid) {
updateTorrentStatusMapFlow(id.toString(), handle.status())
if (progress != handle.status().progress()) {
progress = handle.status().progress()
updateNotificationAsync() // 更新Notification
updateNotificationAsync() // update Notification
updateProgressInfoToDb(link = torrentLink, progress = progress)
updateSizeInfoToDb(
link = torrentLink,
Expand Down Expand Up @@ -497,8 +505,10 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) :
}

private fun moveFromDownloadingDirToVideoDir(handle: TorrentHandle) {
if (handle.savePath() != Const.VIDEO_DIR.path && File(handle.savePath()).exists()) {
if (handle.savePath() != Const.VIDEO_DIR.path) {
handle.moveStorage(Const.VIDEO_DIR.path, MoveFlags.ALWAYS_REPLACE_FILES)
} else {
Log.w(TAG, "handle.savePath() != Const.VIDEO_DIR.path: ${handle.savePath()}")
}
}

Expand Down
29 changes: 29 additions & 0 deletions app/src/main/java/com/skyd/anivu/ui/activity/PlayActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package com.skyd.anivu.ui.activity

import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand All @@ -15,15 +17,30 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.skyd.anivu.base.BaseComposeActivity
import com.skyd.anivu.ext.savePictureToMediaStore
import com.skyd.anivu.ui.component.showToast
import com.skyd.anivu.ui.mpv.PlayerView
import com.skyd.anivu.ui.mpv.copyAssetsForMpv
import java.io.File


class PlayActivity : BaseComposeActivity() {
companion object {
const val VIDEO_URI_KEY = "videoUri"
}


private lateinit var picture: File
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
picture.savePictureToMediaStore(this)
} else {
getString(com.skyd.anivu.R.string.player_no_permission_cannot_save_screenshot).showToast()
}
}

override fun onCreate(savedInstanceState: Bundle?) {
copyAssetsForMpv(this)

Expand Down Expand Up @@ -56,6 +73,10 @@ class PlayActivity : BaseComposeActivity() {
PlayerView(
uri = uri,
onBack = { finish() },
onSaveScreenshot = {
picture = it
saveScreenshot()
}
)
}
}
Expand All @@ -66,4 +87,12 @@ class PlayActivity : BaseComposeActivity() {
return IntentCompat.getParcelableExtra(intent, VIDEO_URI_KEY, Uri::class.java)
?: intent.data
}

private fun saveScreenshot() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
picture.savePictureToMediaStore(this)
} else {
requestPermissionLauncher.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.skyd.anivu.ext.findMainNavController
import com.skyd.anivu.ext.getOrDefault
import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference
import com.skyd.anivu.model.preference.player.PlayerShow85sButtonPreference
import com.skyd.anivu.model.preference.player.PlayerShowScreenshotButtonPreference
import dagger.hilt.android.AndroidEntryPoint


Expand Down Expand Up @@ -75,6 +76,23 @@ class PlayerConfigFragment : BasePreferenceFragmentCompat() {
playerAppearanceCategory.addPreference(this)
}

SwitchPreferenceCompat(this).apply {
key = "playerShowScreenshotButton"
title = getString(R.string.player_config_fragment_show_screenshot_button)
setIcon(R.drawable.ic_photo_camera_24)
isChecked =
requireContext().dataStore.getOrDefault(PlayerShowScreenshotButtonPreference)
setOnPreferenceChangeListener { _, newValue ->
PlayerShowScreenshotButtonPreference.put(
context = requireContext(),
scope = lifecycleScope,
value = newValue as Boolean,
)
true
}
playerAppearanceCategory.addPreference(this)
}

val playerAdvancedCategory = PreferenceCategory(this).apply {
key = "playerAdvancedCategory"
title = getString(R.string.player_config_fragment_advanced_category)
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/com/skyd/anivu/ui/local/LocalValue.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.skyd.anivu.model.preference.behavior.article.DeduplicateTitleInDescPr
import com.skyd.anivu.model.preference.behavior.feed.HideEmptyDefaultPreference
import com.skyd.anivu.model.preference.player.PlayerDoubleTapPreference
import com.skyd.anivu.model.preference.player.PlayerShow85sButtonPreference
import com.skyd.anivu.model.preference.player.PlayerShowScreenshotButtonPreference

val LocalNavController = compositionLocalOf<NavHostController> {
error("LocalNavController not initialized!")
Expand Down Expand Up @@ -44,4 +45,5 @@ val LocalHideEmptyDefault = compositionLocalOf { HideEmptyDefaultPreference.defa

// Player
val LocalPlayerDoubleTap = compositionLocalOf { PlayerDoubleTapPreference.default }
val LocalPlayerShow85sButton = compositionLocalOf { PlayerShow85sButtonPreference.default }
val LocalPlayerShow85sButton = compositionLocalOf { PlayerShow85sButtonPreference.default }
val LocalPlayerShowScreenshotButton = compositionLocalOf { PlayerShowScreenshotButtonPreference.default }
Loading

0 comments on commit 13d13aa

Please sign in to comment.