update
This commit is contained in:
@@ -16,6 +16,23 @@ android {
|
|||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
storeFile = file("../release-key.jks")
|
||||||
|
storePassword = "youtubeapp"
|
||||||
|
keyAlias = "youtubeapp"
|
||||||
|
keyPassword = "youtubeapp"
|
||||||
|
enableV1Signing = true
|
||||||
|
enableV2Signing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
getByName("release") {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ object ApiClient {
|
|||||||
val BASE_URL: String = if (Build.FINGERPRINT.contains("generic") || Build.FINGERPRINT.contains("sdk"))
|
val BASE_URL: String = if (Build.FINGERPRINT.contains("generic") || Build.FINGERPRINT.contains("sdk"))
|
||||||
"http://10.0.2.2:8000/"
|
"http://10.0.2.2:8000/"
|
||||||
else
|
else
|
||||||
"http://marha.local:8000/"
|
"http://192.168.178.34:8000/"
|
||||||
|
|
||||||
val api: VideoApi by lazy {
|
val api: VideoApi by lazy {
|
||||||
Retrofit.Builder()
|
Retrofit.Builder()
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package com.youtubeapp.data
|
package com.youtubeapp.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.google.gson.Gson
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
class LocalStorageService(private val context: Context) {
|
class LocalStorageService(private val context: Context) {
|
||||||
|
|
||||||
|
private val gson = Gson()
|
||||||
|
|
||||||
private fun videosDir(): File {
|
private fun videosDir(): File {
|
||||||
val dir = File(context.filesDir, "videos")
|
val dir = File(context.filesDir, "videos")
|
||||||
if (!dir.exists()) dir.mkdirs()
|
if (!dir.exists()) dir.mkdirs()
|
||||||
@@ -15,6 +18,8 @@ class LocalStorageService(private val context: Context) {
|
|||||||
|
|
||||||
private fun videoFile(videoId: Int): File = File(videosDir(), "$videoId.mp4")
|
private fun videoFile(videoId: Int): File = File(videosDir(), "$videoId.mp4")
|
||||||
|
|
||||||
|
private fun metadataFile(videoId: Int): File = File(videosDir(), "$videoId.json")
|
||||||
|
|
||||||
fun isLocallyAvailable(videoId: Int): Boolean = videoFile(videoId).exists()
|
fun isLocallyAvailable(videoId: Int): Boolean = videoFile(videoId).exists()
|
||||||
|
|
||||||
fun getLocalFile(videoId: Int): File? {
|
fun getLocalFile(videoId: Int): File? {
|
||||||
@@ -22,7 +27,10 @@ class LocalStorageService(private val context: Context) {
|
|||||||
return if (file.exists()) file else null
|
return if (file.exists()) file else null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteLocalFile(videoId: Int): Boolean = videoFile(videoId).delete()
|
fun deleteLocalFile(videoId: Int): Boolean {
|
||||||
|
metadataFile(videoId).delete()
|
||||||
|
return videoFile(videoId).delete()
|
||||||
|
}
|
||||||
|
|
||||||
fun getLocalVideoIds(): List<Int> {
|
fun getLocalVideoIds(): List<Int> {
|
||||||
return videosDir().listFiles()
|
return videosDir().listFiles()
|
||||||
@@ -31,6 +39,24 @@ class LocalStorageService(private val context: Context) {
|
|||||||
?: emptyList()
|
?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveMetadata(video: Video) {
|
||||||
|
metadataFile(video.id).writeText(gson.toJson(video))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLocalVideos(): List<Video> {
|
||||||
|
return videosDir().listFiles()
|
||||||
|
?.filter { it.extension == "json" }
|
||||||
|
?.mapNotNull { file ->
|
||||||
|
try {
|
||||||
|
val video = gson.fromJson(file.readText(), Video::class.java)
|
||||||
|
if (videoFile(video.id).exists()) video else null
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
fun downloadAndSave(videoId: Int): File {
|
fun downloadAndSave(videoId: Int): File {
|
||||||
val url = "${ApiClient.BASE_URL}videos/$videoId/file"
|
val url = "${ApiClient.BASE_URL}videos/$videoId/file"
|
||||||
val file = videoFile(videoId)
|
val file = videoFile(videoId)
|
||||||
|
|||||||
@@ -109,24 +109,15 @@ class VideoViewModel : ViewModel() {
|
|||||||
.onSuccess { videos ->
|
.onSuccess { videos ->
|
||||||
_state.value = _state.value.copy(allVideos = videos, isLoading = false)
|
_state.value = _state.value.copy(allVideos = videos, isLoading = false)
|
||||||
}
|
}
|
||||||
.onFailure { e ->
|
.onFailure {
|
||||||
_state.value = _state.value.copy(error = e.message, isLoading = false)
|
_state.value = _state.value.copy(error = "Server nicht erreichbar", isLoading = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadDownloadedVideos() {
|
fun loadDownloadedVideos() {
|
||||||
viewModelScope.launch {
|
val videos = localStorage?.getLocalVideos() ?: emptyList()
|
||||||
_state.value = _state.value.copy(isLoading = true, error = null)
|
_state.value = _state.value.copy(downloadedVideos = videos)
|
||||||
repository.getAllVideos(profileId = _state.value.selectedProfileId)
|
|
||||||
.onSuccess { videos ->
|
|
||||||
val local = videos.filter { localStorage?.isLocallyAvailable(it.id) == true }
|
|
||||||
_state.value = _state.value.copy(downloadedVideos = local, isLoading = false)
|
|
||||||
}
|
|
||||||
.onFailure { e ->
|
|
||||||
_state.value = _state.value.copy(error = e.message, isLoading = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun triggerDownload(videoId: Int) {
|
fun triggerDownload(videoId: Int) {
|
||||||
@@ -144,16 +135,24 @@ class VideoViewModel : ViewModel() {
|
|||||||
|
|
||||||
val status = result.getOrNull()
|
val status = result.getOrNull()
|
||||||
if (status == "download_started") {
|
if (status == "download_started") {
|
||||||
while (true) {
|
var attempts = 0
|
||||||
|
while (attempts < 150) {
|
||||||
delay(2000)
|
delay(2000)
|
||||||
val videosResult = repository.getAllVideos()
|
val videosResult = repository.getAllVideos()
|
||||||
val video = videosResult.getOrNull()?.find { it.id == videoId }
|
val video = videosResult.getOrNull()?.find { it.id == videoId }
|
||||||
if (video?.is_downloaded == true) break
|
if (video?.is_downloaded == true) break
|
||||||
|
attempts++
|
||||||
}
|
}
|
||||||
|
if (attempts >= 150) throw Exception("Download fehlgeschlagen")
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage?.downloadAndSave(videoId)
|
localStorage?.downloadAndSave(videoId)
|
||||||
|
val video = _state.value.allVideos.find { it.id == videoId }
|
||||||
|
if (video != null) {
|
||||||
|
localStorage?.saveMetadata(video)
|
||||||
|
}
|
||||||
repository.deleteServerFile(videoId)
|
repository.deleteServerFile(videoId)
|
||||||
|
loadDownloadedVideos()
|
||||||
_state.value = _state.value.copy(
|
_state.value = _state.value.copy(
|
||||||
isDownloading = false,
|
isDownloading = false,
|
||||||
downloadStatus = "Lokal gespeichert"
|
downloadStatus = "Lokal gespeichert"
|
||||||
@@ -187,6 +186,7 @@ class VideoViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun getVideoById(videoId: Int): Video? {
|
fun getVideoById(videoId: Int): Video? {
|
||||||
return _state.value.allVideos.find { it.id == videoId }
|
return _state.value.allVideos.find { it.id == videoId }
|
||||||
|
?: _state.value.downloadedVideos.find { it.id == videoId }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isLocallyAvailable(videoId: Int): Boolean {
|
fun isLocallyAvailable(videoId: Int): Boolean {
|
||||||
|
|||||||
BIN
app/release-key.jks
Normal file
BIN
app/release-key.jks
Normal file
Binary file not shown.
@@ -21,13 +21,13 @@
|
|||||||
- Benutzer Icon: Verfügbare Profile anzeigen
|
- Benutzer Icon: Verfügbare Profile anzeigen
|
||||||
- Klick auf ein Profile setzt dieses als das aktuelle Profil
|
- Klick auf ein Profile setzt dieses als das aktuelle Profil
|
||||||
- Es werden nur Videos zu dem Profil angezeigt
|
- Es werden nur Videos zu dem Profil angezeigt
|
||||||
- Standardprofil enthält alle Videos ohne Profilzuweisung
|
- Videos ohne Profilzuweisung werden automatisch dem Standardprofil zugeordnet
|
||||||
- Videoübersicht:
|
- Videoübersicht:
|
||||||
- Oben links: Zurück-Button
|
- Oben links: Zurück-Button
|
||||||
- Unter Zurück-Button: Thumbnail
|
- Unter Zurück-Button: Thumbnail
|
||||||
- Unten: Abspielen und Download Buttons
|
- Unten: Abspielen und Download Buttons
|
||||||
- Abspielen:
|
- Abspielen:
|
||||||
- "Zurück"-Button oben linsk
|
- "Zurück"-Button oben links
|
||||||
- Standard Videos Controls
|
- Standard Videos Controls
|
||||||
- Startet einen Stream über den Server
|
- Startet einen Stream über den Server
|
||||||
- Download:
|
- Download:
|
||||||
|
|||||||
Reference in New Issue
Block a user