init projects

This commit is contained in:
2026-06-17 18:58:41 +03:00
parent 8543c4e241
commit 21a768edd5
505 changed files with 19998 additions and 93 deletions
@@ -0,0 +1,10 @@
package ru.shadowsparky.vbox.shared
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Configuration
import org.koin.core.annotation.Module
@Module
@Configuration
@ComponentScan
class VBoxCommon
@@ -0,0 +1,3 @@
package ru.shadowsparky.vbox.shared.di.factory
typealias DispatcherProvider = ru.shadowsparky.domain.DispatcherProvider
@@ -0,0 +1,35 @@
package ru.shadowsparky.vbox.shared.domain
import kotlinx.serialization.Serializable
import ru.shadowsparky.http.domain.TokenInfo
import ru.shadowsparky.vbox.shared.domain.model.LoginInfo
interface AuthTokenRepository {
suspend fun register(loginInfo: LoginInfo): TokenInfo
suspend fun login(loginInfo: LoginInfo): TokenInfo
suspend fun update(refresh: TokenRequest): TokenInfo
suspend fun changePassword(request: ChangePasswordRequest)
suspend fun revoke(tokenRequest: TokenRequest)
companion object {
const val LOGIN_PATH = "$DYNAMIC_PREFIX/login"
const val REGISTER_PATH = "$DYNAMIC_PREFIX/register"
const val UPDATE_PATH = "$DYNAMIC_PREFIX/updateToken"
const val CHANGE_PASS_PATH = "$DYNAMIC_PREFIX/changePassword"
const val REVOKE_PATH = "$DYNAMIC_PREFIX/revokeToken"
}
}
@Serializable
data class TokenRequest(val token: String)
@Serializable
data class ChangePasswordRequest(
val oldPasswordHash: String,
val newPasswordHash: String
)
const val FLAG_USER_BLOCKED = 1
@@ -0,0 +1,9 @@
package ru.shadowsparky.vbox.shared.domain
interface HeathCheck {
suspend fun check()
companion object {
const val PATH = "healthCheck"
}
}
@@ -0,0 +1,47 @@
package ru.shadowsparky.vbox.shared.domain
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import ru.shadowsparky.vbox.shared.domain.model.RecentlyWatchedInfo
interface RecentlyWatchedRepository {
suspend fun remove(info: RecentlyWatchedInfo)
suspend fun write(info: RecentlyWatchedInfo)
suspend fun writeAll(info: List<RecentlyWatchedInfo>) {
info.forEach { write(it) }
}
fun queryRecentlyWatched(
movieId: Long,
seasonId: Long? = null
): Flow<List<RecentlyWatchedInfo>>
suspend fun getAllRecentlyWatched(): List<RecentlyWatchedInfo>
suspend fun clear() {
getAllRecentlyWatched().forEach { remove(it) }
}
companion object {
const val PREFIX = "$DYNAMIC_PREFIX/recent"
const val REMOVE = "$PREFIX/delete"
const val WRITE = "$PREFIX/add"
const val TOGGLE = "$PREFIX/toggle"
const val GET_INFO = "$PREFIX/query"
const val MOVIE_ID_ARG = "movieId"
const val SEASON_ID_ARG = "seasonId"
}
}
@Serializable
data class RecentlyWatchedResponse(
val info: List<RecentlyWatchedInfo>
)
@Serializable
data class RecentlyWatchedRequest(
val info: List<RecentlyWatchedInfo>
)
@@ -0,0 +1,51 @@
package ru.shadowsparky.vbox.shared.domain
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import ru.shadowsparky.vbox.shared.domain.tag.MovieTagRepository
import ru.shadowsparky.vbox.shared.domain.tag.UserTagRepository
interface RemoteEventHandler {
suspend fun notify(eventInfo: RemoteEvent)
companion object {
const val SEARCH_CHANGED = "${SearchRepository.PREFIX}/onChange"
const val RECENTLY_CHANGED = "${RecentlyWatchedRepository.PREFIX}/onChange"
const val SAVED_CHANGED = "${SavedMovieRepository.PREFIX}/onChange"
const val USER_TAG_CHANGED = "${UserTagRepository.PREFIX}/onChange"
const val MOVIE_TAG_CHANGED = "${MovieTagRepository.PREFIX}/onChange"
}
}
interface RemoteEventListener {
val event: Flow<RemoteEvent>
}
@Serializable
sealed interface RemoteEvent {
val userId: Long
@Serializable
data class OnSearch(val millis: Long, override val userId: Long) : RemoteEvent
@Serializable
data class OnRecent(val millis: Long, override val userId: Long, val movieId: Long) :
RemoteEvent
@Serializable
data class OnSaved(override val userId: Long, val movieId: Long) : RemoteEvent
@Serializable
data class OnUserTag(override val userId: Long) : RemoteEvent
@Serializable
data class OnMovieTag(override val userId: Long, val movieId: Long) : RemoteEvent
}
object EventType {
const val SEARCH = "search"
const val RECENT = "recent"
const val SAVED = "saved"
const val USER_TAG = "user_tag"
const val MOVIE_TAG = "movie_tag"
}
@@ -0,0 +1,40 @@
package ru.shadowsparky.vbox.shared.domain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable
import ru.shadowsparky.vbox.shared.domain.model.VideoDetails
interface SavedMovieRepository {
suspend fun save(details: VideoDetails)
fun getAll(): Flow<List<VideoDetails>>
fun isSaved(id: Long): Flow<Boolean>
suspend fun remove(id: Long)
suspend fun saveAll(details: List<VideoDetails>) {
details.forEach { save(it) }
}
suspend fun clear() {
getAll().first().forEach { remove(it.id) }
}
companion object {
const val MOVIE_ID_ARG = RecentlyWatchedRepository.MOVIE_ID_ARG
const val PREFIX = "$DYNAMIC_PREFIX/savedMovie"
const val REMOVE = "${PREFIX}/delete"
const val WRITE = "${PREFIX}/add"
const val QUERY = "${PREFIX}/query"
const val QUERY_ALL = "${QUERY}_all"
const val QUERY_SAVED = "${QUERY}_saved"
}
}
@Serializable
data class RemoveRequest(val movieId: Long)
@Serializable
data class IsSavedResponse(val isSaved: Boolean)
@Serializable
data class SavedMoviesResponse(val list: List<VideoDetails>)
@@ -0,0 +1,41 @@
package ru.shadowsparky.vbox.shared.domain
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
interface SearchRepository {
fun search(query: String): Flow<List<String>>
suspend fun addToSearch(query: String)
suspend fun addToSearch(query: List<String>) {
query.forEach { addToSearch(it) }
}
suspend fun deleteFromSearch(query: String)
suspend fun clear()
companion object {
const val PREFIX = "$DYNAMIC_PREFIX/search"
const val SEARCH = "$PREFIX/query"
const val ADD_TO_SEARCH = "$PREFIX/add"
const val CLEAR = "$PREFIX/delete"
const val QUERY_ARG = "query"
}
}
@Serializable
class SearchQueryResponse(
val search: List<String>
)
@Serializable
class SearchQueryRequest(
val query: List<String>
)
@Serializable
class SearchDeleteRequest(
val query: String? = null
)
@@ -0,0 +1,22 @@
package ru.shadowsparky.vbox.shared.domain
data class ServerConfiguration(
val uriString: String = "https://box.shadowsparky.ru/api"
) {
fun asHttpStr(): String {
val builder = StringBuilder(uriString)
if (uriString.endsWith("/")) {
builder.removeSuffix("/")
}
return builder.toString()
}
fun asWebSocketStr(): String {
val useTls = asHttpStr().startsWith("https")
val withoutScheme = asHttpStr()
.removePrefix("https")
.removePrefix("http")
val newPrefix = if (useTls) "wss" else "ws"
return "$newPrefix$withoutScheme"
}
}
@@ -0,0 +1,3 @@
package ru.shadowsparky.vbox.shared.domain
const val USER_ID_ARG = "user_id"
@@ -0,0 +1,23 @@
package ru.shadowsparky.vbox.shared.domain
import ru.shadowsparky.vbox.shared.domain.model.VideoDetails
import ru.shadowsparky.vbox.shared.domain.model.VideoLinksResponse
import ru.shadowsparky.vbox.shared.domain.model.VideosResponse
const val DYNAMIC_PREFIX = "dynamic"
interface VideoApi {
suspend fun fetchNewVideos(query: String? = null): VideosResponse
suspend fun fetchDetails(id: Long): VideoDetails
suspend fun fetchVideoLinks(id: Long, seasonId: Long?): VideoLinksResponse
companion object {
const val SEARCH_ARG = "search"
const val ID_ARG = "id"
const val SEASON_ID_ARG = "season_id"
const val VIDEOS_PATH = "videos"
const val DETAILS_PATH = "details"
const val LINKS_PATH = "links"
}
}
@@ -0,0 +1,13 @@
package ru.shadowsparky.vbox.shared.domain.model
import kotlinx.serialization.Serializable
@Serializable
data class Episode(
val episode: Long,
val files: List<File>,
val title: String? = null,
val isWatched: Boolean = false
) {
val url: String = files.first().url
}
@@ -0,0 +1,10 @@
package ru.shadowsparky.vbox.shared.domain.model
import kotlinx.serialization.Serializable
@Serializable
data class File(
val proPlus: Boolean?,
val quality: Int?,
val url: String
)
@@ -0,0 +1,16 @@
package ru.shadowsparky.vbox.shared.domain.model
import kotlinx.serialization.Serializable
@Serializable
data class LoginInfo(
val login: String,
val passwordHash: String
)
open class LoginException(override val message: String?) : IllegalStateException()
class EmptyLoginException : LoginException("Login must be not empty!")
class EmptyPasswordException : LoginException("Password must be not empty!")
class ServerLoginException(message: String) : LoginException(message)
class UnknownLoginException(override val cause: Throwable?) : LoginException(cause?.message)
@@ -0,0 +1,11 @@
package ru.shadowsparky.vbox.shared.domain.model
import kotlinx.serialization.Serializable
@Serializable
data class RecentlyWatchedInfo(
val movieId: Long,
val episode: Long?,
val season: Long?,
val id: Long = -1
)
@@ -0,0 +1,10 @@
package ru.shadowsparky.vbox.shared.domain.model
import kotlinx.serialization.Serializable
@Serializable
data class Season(
val episodes: List<Episode>,
val id: Long,
val title: String
)
@@ -0,0 +1,6 @@
package ru.shadowsparky.vbox.shared.domain.model
import kotlinx.serialization.Serializable
@Serializable
data class ServerExceptionInfo(val msg: String)
@@ -0,0 +1,34 @@
package ru.shadowsparky.vbox.shared.domain.model
import kotlinx.serialization.Serializable
@Serializable
data class VideoDetails(
val id: Long,
val poster: String,
val desc: String,
val title: String,
val isSerial: Boolean
) {
val isMovie = !isSerial
val flags: Int
get() {
return if (isSerial) SERIAL_FLAG else MOVIE_FLAG
}
companion object {
fun getStub(title: String, isSerial: Boolean = true): VideoDetails {
return VideoDetails(
id = 1234,
poster = "https://poster.ru/img.png",
title = title,
desc = "Short Story Description!",
isSerial = isSerial
)
}
}
}
const val SERIAL_FLAG = 1
const val MOVIE_FLAG = 1 shl 1
@@ -0,0 +1,12 @@
package ru.shadowsparky.vbox.shared.domain.model
import kotlinx.serialization.Serializable
@Serializable
data class VideoItem(
val id: Long,
val poster: String,
val title: String,
val description: String? = null,
val isSerial: Boolean = false
)
@@ -0,0 +1,10 @@
package ru.shadowsparky.vbox.shared.domain.model
import kotlinx.serialization.Serializable
@Serializable
data class VideoLinksResponse(
val name: String? = null,
val seasons: List<Season>? = null,
val files: List<File>? = null,
)
@@ -0,0 +1,12 @@
package ru.shadowsparky.vbox.shared.domain.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class VideosResponse(
@SerialName("has_next_page")
val hasNextPage: Boolean,
val items: List<VideoItem>,
val page: Int
)
@@ -0,0 +1,31 @@
package ru.shadowsparky.vbox.shared.domain.tag
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX
interface MovieTagRepository {
fun query(movieId: Long): Flow<List<TagInfo>>
fun queryMovies(tagId: Long): Flow<Set<Long>>
suspend fun link(movieId: Long, tagId: Long)
suspend fun unlink(movieId: Long, tagId: Long)
suspend fun clear() {}
companion object {
const val TAG_ID = "tagId"
const val MOVIE_ID = "movieId"
const val PREFIX = "$DYNAMIC_PREFIX/movieTag"
const val QUERY = "$PREFIX/query"
const val QUERY_MOVIES = "$PREFIX/queryMovies"
const val LINK = "$PREFIX/link"
const val UNLINK = "$PREFIX/unlink"
}
}
@Serializable
data class LinkRequest(val movieId: Long, val tagId: Long)
@Serializable
data class LinkedMovies(val ids: Set<Long>)
@@ -0,0 +1,42 @@
package ru.shadowsparky.vbox.shared.domain.tag
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.Serializable
import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX
interface UserTagRepository {
val userTags: Flow<List<TagInfo>>
suspend fun add(tag: String)
suspend fun edit(id: Long, newTag: String)
suspend fun delete(id: Long)
suspend fun clear() {}
companion object {
const val PREFIX = "$DYNAMIC_PREFIX/userTag"
const val LIST = "$PREFIX/list"
const val ADD = "$PREFIX/add"
const val EDIT = "$PREFIX/edit"
const val DELETE = "$PREFIX/delete"
}
}
@Serializable
data class TagResponse(val tags: List<TagInfo>)
@Serializable
data class AddRequest(val tag: String)
@Serializable
data class EditRequest(val id: Long, val newTag: String)
@Serializable
data class DeleteRequest(val id: Long)
@Serializable
data class TagInfo(
val id: Long? = null,
val tag: String
)
@@ -0,0 +1,47 @@
package ru.shadowsparky.vbox.shared.di.factory
import org.koin.core.annotation.Single
import java.io.File
@Single
class DesktopFileProvider {
private val homeUser by lazy { System.getProperty("user.home") }
private val appPackage = "ru.shadowsparky.vbox"
private var cacheDir: File? = null
fun getCacheFileDir(): String? {
cacheDir?.let { return it.absolutePath }
return getCacheDir().apply {
mkdirs()
cacheDir = this
}.absolutePath
}
private fun getCacheDir(): File {
getMacOsCacheDir()?.let { return it }
getLinuxCacheDir()?.let { return it }
getWindowsCacheDir()?.let { return it }
return File("cache") // fallback
}
private fun getMacOsCacheDir(): File? {
return File(homeUser, "Library").provideCacheFile()
}
private fun getLinuxCacheDir(): File? {
return File(homeUser, ".cache").provideCacheFile()
}
private fun getWindowsCacheDir(): File? {
val appData = System.getenv("APPDATA")
return File(appData).provideCacheFile()
}
private fun File.provideCacheFile(): File? {
return if (exists() && canRead() && canWrite()) {
File(this, appPackage)
} else {
null
}
}
}