init projects
This commit is contained in:
@@ -0,0 +1 @@
|
||||
build
|
||||
@@ -0,0 +1,23 @@
|
||||
plugins {
|
||||
alias(libs.plugins.convention.kmp)
|
||||
alias(libs.plugins.convention.android.lib)
|
||||
alias(libs.plugins.convention.ktor.client)
|
||||
alias(libs.plugins.convention.compose)
|
||||
alias(libs.plugins.convention.serialization)
|
||||
alias(libs.plugins.convention.koin)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation(project(":libs:base"))
|
||||
implementation(project(":libs:http-client"))
|
||||
}
|
||||
}
|
||||
val androidMain by getting
|
||||
}
|
||||
android {
|
||||
namespace = "ru.shadowsparky.vbox.common"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package ru.shadowsparky.vbox.shared.di.factory
|
||||
|
||||
typealias DispatcherProvider = ru.shadowsparky.domain.DispatcherProvider
|
||||
+35
@@ -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
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package ru.shadowsparky.vbox.shared.domain
|
||||
|
||||
interface HeathCheck {
|
||||
suspend fun check()
|
||||
|
||||
companion object {
|
||||
const val PATH = "healthCheck"
|
||||
}
|
||||
}
|
||||
+47
@@ -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>
|
||||
)
|
||||
+51
@@ -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"
|
||||
}
|
||||
+40
@@ -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>)
|
||||
+41
@@ -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
|
||||
)
|
||||
+22
@@ -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"
|
||||
}
|
||||
}
|
||||
+13
@@ -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
|
||||
}
|
||||
+10
@@ -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
|
||||
)
|
||||
+16
@@ -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)
|
||||
+11
@@ -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
|
||||
)
|
||||
+10
@@ -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
|
||||
)
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package ru.shadowsparky.vbox.shared.domain.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ServerExceptionInfo(val msg: String)
|
||||
+34
@@ -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
|
||||
+12
@@ -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
|
||||
)
|
||||
+10
@@ -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,
|
||||
)
|
||||
+12
@@ -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
|
||||
)
|
||||
+31
@@ -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>)
|
||||
+42
@@ -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
|
||||
)
|
||||
+47
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user