diff --git a/.github/workflows/Build-Linux.yml b/.github/workflows/Build-Linux.yml index d0d4ee0..5e930c3 100644 --- a/.github/workflows/Build-Linux.yml +++ b/.github/workflows/Build-Linux.yml @@ -40,6 +40,13 @@ jobs: env: JAVA_HOME: ${{ steps.setup-java.outputs.path }} run: "./gradlew packageReleaseUberJarForCurrentOS" + - name: "Print and save sha384 for binaries" + run: find ./build/compose -type f \( -iname Styx\*linux\*.jar -o -iname \*.deb -o -iname \*.rpm \) -exec shasum -a 384 {} \; | tee checksums.sha384 + - name: "Upload checksum file" + uses: actions/upload-artifact@v4 + with: + name: checksums.sha384 + path: checksums.sha384 - name: "Upload binaries to FTP" uses: "SamKirkland/FTP-Deploy-Action@v4.3.4" with: diff --git a/.github/workflows/Build-Windows.yml b/.github/workflows/Build-Windows.yml index cbc9df9..1fb1711 100644 --- a/.github/workflows/Build-Windows.yml +++ b/.github/workflows/Build-Windows.yml @@ -38,6 +38,18 @@ jobs: env: JAVA_HOME: ${{ steps.setup-java.outputs.path }} run: ./gradlew.bat packageReleaseUberJarForCurrentOS + - name: "Print and save sha384 for binaries" + shell: pwsh + run: | + Get-ChildItem -Path .\build\compose -Recurse -File -Include Styx*windows*.jar,*.msi | ForEach-Object { + $hash = Get-FileHash -Path $_.FullName -Algorithm SHA384 + "$($hash.Hash) $($_.Name)" + } | Tee-Object -FilePath checksums.sha384 + - name: "Upload checksum file" + uses: actions/upload-artifact@v4 + with: + name: checksums.sha384 + path: checksums.sha384 - name: "Upload binaries to FTP" uses: "SamKirkland/FTP-Deploy-Action@v4.3.4" with: diff --git a/.gitignore b/.gitignore index 488efad..69ee301 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ local.properties +checksums.sha384 ### IntelliJ IDEA ### diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index fe63bb6..c224ad5 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index c049206..2f5ee3b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,8 +1,7 @@ - - + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..931b96c --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 620c30e..bc2fe4b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,15 +1,15 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { - kotlin("jvm") version "1.9.23" - id("org.jetbrains.compose") version "1.6.1" - id("org.jetbrains.kotlin.plugin.serialization") version "1.9.23" - id("com.github.gmazzo.buildconfig") version "5.3.5" - id("org.ajoberstar.grgit") version "5.2.1" + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.buildconfig) } group = "moe.styx" -version = "0.0.7" +version = "0.1.0" repositories { google() @@ -30,18 +30,19 @@ dependencies { implementation(compose.materialIconsExtended) // Misc - implementation("org.slf4j:slf4j-simple:2.0.9") - implementation("net.lingala.zip4j:zip4j:2.11.5") - implementation("com.github.caoimhebyrne:KDiscordIPC:0.2.2") - implementation("com.squareup.okio:okio:3.9.0") + implementation(libs.slf4j.simple) + implementation(libs.zip4j) + implementation(libs.kdiscord.ipc) + implementation(libs.okio) // Styx - implementation("moe.styx:styx-common-compose-jvm:0.0.5") + implementation(libs.styx.common.compose) } compose.desktop { application { mainClass = "moe.styx.MainKt" + jvmArgs += listOf("-Xmx1250M", "-Xms300M") buildTypes.release.proguard { configurationFiles.from(project.file("proguard.rules")) } @@ -62,11 +63,13 @@ compose.desktop { vendor = "Vodes & Styx contributors" licenseFile.set(project.file("LICENSE")) windows { + packageVersion = project.version.toString().split("-")[0] menuGroup = "Styx" upgradeUuid = System.getenv("STYX_APP_GUID") iconFile.set(project.file("src/main/resources/icons/icon.ico")) } linux { + packageVersion = project.version.toString().split("-")[0] iconFile.set(project.file("src/main/resources/icons/icon.png")) menuGroup = "AudioVideo;Video" shortcut = true @@ -74,7 +77,7 @@ compose.desktop { macOS { packageVersion = project.version.toString().let { if (it.startsWith("0.")) it.replaceFirst("0.", "1.") else it - } + }.split("-")[0] appStore = false iconFile.set(project.file("src/main/resources/icons/icon.icns")) } @@ -92,8 +95,8 @@ buildConfig { buildConfigField("IMAGE_URL", System.getenv("STYX_IMAGEURL")) // Example: https://images.company.com buildConfigField("SITE", siteURL.split("https://").getOrElse(1) { siteURL }) buildConfigField("BUILD_TIME", (System.currentTimeMillis() / 1000)) - buildConfigField("VERSION_CHECK_URL", "https://raw.githubusercontent.com/Vodes/Styx-2/master/build.gradle.kts") - buildConfigField("DISCORD_CLIENT_ID", "")//System.getenv("STYX_DISCORDCLIENT")) + buildConfigField("VERSION_CHECK_URL", "https://api.github.com/repos/Vodes/Styx-2/tags") + buildConfigField("DISCORD_CLIENT_ID", "686174250259709983") } kotlin { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..fac9186 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,23 @@ +[versions] +kotlin = "2.0.21" +compose = "1.7.1" +styx-common-compose = "0.1.1" +buildconfig = "5.4.0" +slf4j-simple = "2.0.16" +okio = "3.9.0" +kdiscord-ipc = "0.2.2" +zip4j = "2.11.5" + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +compose = { id = "org.jetbrains.compose", version.ref = "compose" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +buildconfig = { id = "com.github.gmazzo.buildconfig", version.ref = "buildconfig" } + +[libraries] +zip4j = { module = "net.lingala.zip4j:zip4j", version.ref = "zip4j" } +kdiscord-ipc = { module = "com.github.caoimhebyrne:KDiscordIPC", version.ref = "kdiscord-ipc" } +okio = { module = "com.squareup.okio:okio", version.ref = "okio" } +slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j-simple" } +styx-common-compose = { module = "moe.styx:styx-common-compose-jvm", version.ref = "styx-common-compose" } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 9b96589..843c99a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,7 +4,9 @@ pluginManagement { gradlePluginPortal() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } - +} +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0" } rootProject.name = "Styx 2" diff --git a/src/main/kotlin/moe/styx/Main.kt b/src/main/kotlin/moe/styx/Main.kt index e019292..f13ec01 100644 --- a/src/main/kotlin/moe/styx/Main.kt +++ b/src/main/kotlin/moe/styx/Main.kt @@ -4,35 +4,39 @@ import androidx.compose.animation.core.Spring import androidx.compose.animation.core.VisibilityThreshold import androidx.compose.animation.core.spring import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.Surface import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.* import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.transitions.SlideTransition +import com.dokar.sonner.ToastWidthPolicy +import com.dokar.sonner.Toaster +import com.dokar.sonner.rememberToasterState import com.russhwolf.settings.get import io.kamel.image.config.LocalKamelConfig import kotlinx.coroutines.delay import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime -import moe.styx.Main.isUiModeDark -import moe.styx.Main.setupLogFile -import moe.styx.Styx__.BuildConfig +import moe.styx.Styx_2.BuildConfig import moe.styx.common.compose.AppConfig -import moe.styx.common.compose.appConfig +import moe.styx.common.compose.AppContextImpl.appConfig import moe.styx.common.compose.extensions.kamelConfig -import moe.styx.common.compose.http.* +import moe.styx.common.compose.http.Endpoints +import moe.styx.common.compose.http.sendObject import moe.styx.common.compose.settings import moe.styx.common.compose.threads.DownloadQueue import moe.styx.common.compose.threads.Heartbeats import moe.styx.common.compose.threads.RequestQueue import moe.styx.common.compose.utils.LocalGlobalNavigator -import moe.styx.common.compose.utils.ServerStatus +import moe.styx.common.compose.utils.LocalToaster import moe.styx.common.extension.formattedStrFile import moe.styx.common.http.getHttpClient import moe.styx.common.util.Log @@ -41,14 +45,14 @@ import moe.styx.logic.DiscordRPC import moe.styx.logic.Files import moe.styx.logic.runner.currentPlayer import moe.styx.theme.* -import moe.styx.views.login.LoginView -import moe.styx.views.login.OfflineView -import moe.styx.views.other.LoadingView +import moe.styx.views.anime.AnimeOverview import java.io.File import java.io.PrintStream object Main { var isUiModeDark: MutableState = mutableStateOf(true) + var useMonoFont: MutableState = mutableStateOf(false) + var densityScale: MutableState = mutableStateOf(1f) var wasLaunchedInDebug = false fun setupLogFile() { @@ -59,14 +63,18 @@ object Main { val stream = PrintStream(file.outputStream()) System.setOut(stream) System.setErr(stream) + if (settings["enable-debug-logs", false]) + Log.debugEnabled = true } } fun main(args: Array) = application { if (!args.contains("-debug")) - setupLogFile() - else + Main.setupLogFile() + else { Main.wasLaunchedInDebug = true + Log.debugEnabled = true + } getHttpClient("${BuildConfig.APP_NAME} - ${BuildConfig.APP_VERSION}") appConfig = { AppConfig( @@ -76,17 +84,19 @@ fun main(args: Array) = application { BuildConfig.IMAGE_URL, null, Files.getCacheDir().absolutePath, - Files.getDataDir().absolutePath + Files.getDataDir().absolutePath, + BuildConfig.VERSION_CHECK_URL ) } if (settings["discord-rpc", true]) { DiscordRPC.start() } - RequestQueue.start() - Heartbeats.start() - DownloadQueue.start() launchGlobal { + Heartbeats.start() + delay(10000) + RequestQueue.start() + DownloadQueue.start() while (true) { delay(3000) DiscordRPC.updateActivity() @@ -94,9 +104,11 @@ fun main(args: Array) = application { Heartbeats.mediaActivity = null } } - - isUiModeDark.value = settings["darkmode", true] - val darkMode by remember { isUiModeDark } + Main.useMonoFont.value = settings["mono-font", false] + Main.isUiModeDark.value = settings["darkmode", true] + Main.densityScale.value = settings["density-scale", 1f] + val darkMode by remember { Main.isUiModeDark } + val monoFont by remember { Main.useMonoFont } Window( onCloseRequest = { onClose() }, @@ -108,28 +120,28 @@ fun main(args: Array) = application { Log.i { "Compose window initialized with: ${this.window.renderApi}" } Log.i { "Starting ${BuildConfig.APP_NAME} v${BuildConfig.APP_VERSION}" } Surface(modifier = Modifier.fillMaxSize()) { - MaterialTheme( - colorScheme = (if (darkMode) DarkColorScheme else LightColorScheme).transition(), - typography = AppTypography, - shapes = AppShapes - ) { - val view = if (isLoggedIn()) { - Log.i { "Logged in as: ${login?.name}" } - LoadingView() - } else { - if (ServerStatus.lastKnown !in listOf(ServerStatus.ONLINE, ServerStatus.UNAUTHORIZED)) - OfflineView() - else - LoginView() - } - Navigator(view) { navigator -> - CompositionLocalProvider(LocalGlobalNavigator provides navigator, LocalKamelConfig provides kamelConfig) { - SlideTransition( - navigator, animationSpec = spring( - stiffness = Spring.StiffnessMedium, - visibilityThreshold = IntOffset.VisibilityThreshold + val currentDensity = LocalDensity.current + CompositionLocalProvider(LocalDensity provides Density(currentDensity.density * Main.densityScale.value)) { + val toasterState = rememberToasterState() + MaterialTheme( + colorScheme = if (darkMode) darkScheme else lightScheme, + typography = if (monoFont) AppFont.JetbrainsMono.Typography else AppFont.OpenSans.Typography, + shapes = AppShapes + ) { + Toaster(toasterState, darkTheme = darkMode, richColors = true, widthPolicy = { ToastWidthPolicy(0.dp, 450.dp) }) + Navigator(AnimeOverview()) { navigator -> + CompositionLocalProvider( + LocalGlobalNavigator provides navigator, + LocalKamelConfig provides kamelConfig, + LocalToaster provides toasterState + ) { + SlideTransition( + navigator, animationSpec = spring( + stiffness = Spring.StiffnessMedium, + visibilityThreshold = IntOffset.VisibilityThreshold + ) ) - ) + } } } } diff --git a/src/main/kotlin/moe/styx/components/anime/AnimeDetailItems.kt b/src/main/kotlin/moe/styx/components/anime/AnimeDetailItems.kt index 0396d4d..71a8d0e 100644 --- a/src/main/kotlin/moe/styx/components/anime/AnimeDetailItems.kt +++ b/src/main/kotlin/moe/styx/components/anime/AnimeDetailItems.kt @@ -14,17 +14,18 @@ import io.kamel.core.Resource import io.kamel.image.KamelImage @Composable -fun BigScalingCardImage(image: Resource, modifier: Modifier = Modifier) { +fun BigScalingCardImage(image: Resource?, modifier: Modifier = Modifier) { Column(modifier) { ElevatedCard( Modifier.align(Alignment.Start).padding(12.dp).requiredHeightIn(150.dp, 500.dp).aspectRatio(0.71F), ) { - KamelImage( - image, - contentDescription = "Anime", - modifier = Modifier.padding(2.dp).clip(RoundedCornerShape(4.dp)), - contentScale = ContentScale.FillBounds - ) + if (image != null) + KamelImage( + { image }, + contentDescription = "Anime", + modifier = Modifier.padding(2.dp).clip(RoundedCornerShape(4.dp)), + contentScale = ContentScale.FillBounds + ) } } } diff --git a/src/main/kotlin/moe/styx/components/anime/AnimeDialogs.kt b/src/main/kotlin/moe/styx/components/anime/AnimeDialogs.kt index e08f714..c9dc20a 100644 --- a/src/main/kotlin/moe/styx/components/anime/AnimeDialogs.kt +++ b/src/main/kotlin/moe/styx/components/anime/AnimeDialogs.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import moe.styx.common.data.MediaEntry +import moe.styx.logic.runner.MpvFinishStatus import moe.styx.logic.runner.launchMPV @Composable @@ -30,8 +31,7 @@ fun AppendDialog( modifier: Modifier = Modifier, buttonModifier: Modifier = Modifier, onDismiss: () -> Unit = {}, - execUpdate: () -> Unit = {}, - onFail: (String) -> Unit = {} + onResult: (MpvFinishStatus) -> Unit = {} ) { AlertDialog( { onDismiss() }, @@ -40,13 +40,13 @@ fun AppendDialog( text = { Text("Do you want to start playing now or append to the current playlist?") }, dismissButton = { Button({ - launchMPV(mediaEntry, false, { onFail(it) }, execUpdate = execUpdate) + launchMPV(mediaEntry, false, onResult) onDismiss() }, modifier = buttonModifier) { Text("Play now") } }, confirmButton = { Button({ - launchMPV(mediaEntry, true, { onFail(it) }, execUpdate = execUpdate) + launchMPV(mediaEntry, true, onResult) onDismiss() }, modifier = buttonModifier) { Text("Append") } } diff --git a/src/main/kotlin/moe/styx/components/overviews/MediaListing.kt b/src/main/kotlin/moe/styx/components/overviews/MediaListing.kt index 9b47cc6..52c31d2 100644 --- a/src/main/kotlin/moe/styx/components/overviews/MediaListing.kt +++ b/src/main/kotlin/moe/styx/components/overviews/MediaListing.kt @@ -1,40 +1,49 @@ package moe.styx.components.overviews -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.* import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import moe.styx.common.compose.components.anime.AnimeCard import moe.styx.common.compose.components.anime.AnimeListItem -import moe.styx.common.compose.files.Storage -import moe.styx.common.compose.files.collectWithEmptyInitial import moe.styx.common.compose.utils.LocalGlobalNavigator +import moe.styx.common.compose.viewmodels.ListPosViewModel +import moe.styx.common.compose.viewmodels.MainDataViewModelStorage import moe.styx.common.data.Media +import moe.styx.common.extension.eqI import moe.styx.logic.utils.pushMediaView -@OptIn(ExperimentalFoundationApi::class) @Composable -fun MediaGrid(media: List, showUnseen: Boolean = false) { +fun MediaGrid(storage: MainDataViewModelStorage, mediaList: List, listPosViewModel: ListPosViewModel, showUnseen: Boolean = false) { val nav = LocalGlobalNavigator.current + val listState = rememberLazyGridState(listPosViewModel.scrollIndex, listPosViewModel.scrollOffset) + LaunchedEffect(listState.isScrollInProgress) { + if (!listState.isScrollInProgress) { + listPosViewModel.scrollIndex = listState.firstVisibleItemIndex + listPosViewModel.scrollOffset = listState.firstVisibleItemScrollOffset + } + } if (showUnseen) { - val entryList by Storage.stores.entryStore.collectWithEmptyInitial() - val watchedList by Storage.stores.watchedStore.collectWithEmptyInitial() LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 160.dp), contentPadding = PaddingValues(10.dp, 7.dp), + state = listState ) { - items(media, key = { it.GUID }) { - Row(modifier = Modifier.animateItemPlacement()) { - AnimeCard(it, showUnseen, entryList = entryList, watchedEntries = watchedList) { nav.pushMediaView(it) } + items(mediaList, key = { it.GUID }) { + Row(modifier = Modifier.animateItem()) { + AnimeCard( + it to storage.imageList.find { img -> img.GUID eqI it.thumbID }, + true, + entryList = storage.entryList, + watchedEntries = storage.watchedList + ) { nav.pushMediaView(it) } } } } @@ -42,24 +51,31 @@ fun MediaGrid(media: List, showUnseen: Boolean = false) { LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 160.dp), contentPadding = PaddingValues(10.dp, 7.dp), + state = listState ) { - items(media, key = { it.GUID }) { - Row(modifier = Modifier.animateItemPlacement()) { - AnimeCard(it, showUnseen) { nav.pushMediaView(it) } + items(mediaList, key = { it.GUID }) { + Row(modifier = Modifier.animateItem()) { + AnimeCard(it to storage.imageList.find { img -> img.GUID eqI it.thumbID }) { nav.pushMediaView(it) } } } } } } -@OptIn(ExperimentalFoundationApi::class) @Composable -fun MediaList(media: List) { +fun MediaList(storage: MainDataViewModelStorage, mediaList: List, listPosViewModel: ListPosViewModel) { val nav = LocalGlobalNavigator.current - LazyColumn { - items(media, key = { it.GUID }) { - Row(Modifier.animateItemPlacement().padding(3.dp)) { - AnimeListItem(it) { nav.pushMediaView(it) } + val listState = rememberLazyListState(listPosViewModel.scrollIndex, listPosViewModel.scrollOffset) + LaunchedEffect(listState.isScrollInProgress) { + if (!listState.isScrollInProgress) { + listPosViewModel.scrollIndex = listState.firstVisibleItemIndex + listPosViewModel.scrollOffset = listState.firstVisibleItemScrollOffset + } + } + LazyColumn(state = listState) { + items(mediaList, key = { it.GUID }) { + Row(Modifier.animateItem().padding(3.dp)) { + AnimeListItem(it, storage.imageList.find { img -> img.GUID eqI it.thumbID }) { nav.pushMediaView(it) } } } } diff --git a/src/main/kotlin/moe/styx/logic/DiscordRPC.kt b/src/main/kotlin/moe/styx/logic/DiscordRPC.kt index abedf9b..e231525 100644 --- a/src/main/kotlin/moe/styx/logic/DiscordRPC.kt +++ b/src/main/kotlin/moe/styx/logic/DiscordRPC.kt @@ -7,15 +7,14 @@ import dev.cbyrne.kdiscordipc.core.event.impl.ErrorEvent import dev.cbyrne.kdiscordipc.core.event.impl.ReadyEvent import dev.cbyrne.kdiscordipc.data.activity.* import io.github.xxfast.kstore.extensions.getOrEmpty +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import moe.styx.Styx__.BuildConfig -import moe.styx.common.compose.extensions.getThumb +import moe.styx.Styx_2.BuildConfig import moe.styx.common.compose.extensions.getURL -import moe.styx.common.compose.files.Storage +import moe.styx.common.compose.files.Stores import moe.styx.common.compose.settings import moe.styx.common.data.MediaActivity -import moe.styx.common.extension.currentUnixSeconds import moe.styx.common.extension.eqI import moe.styx.common.extension.toBoolean import moe.styx.common.util.Log @@ -43,6 +42,7 @@ object DiscordRPC { ipc!!.on { errored = false Log.i { "Discord-RPC initialized." } + delay(5000) updateActivity() }.start() ipc!!.connect() @@ -67,8 +67,9 @@ object DiscordRPC { val mediaActivity = if (currentPlayer != null && MpvStatus.current.file.isNotEmpty() && MpvStatus.current.percentage > -1) MediaActivity(MpvStatus.current.file, MpvStatus.current.seconds.toLong(), !MpvStatus.current.paused) else null - val entry = mediaActivity?.let { act -> runBlocking { Storage.stores.entryStore.getOrEmpty() }.find { it.GUID eqI act.mediaEntry } } - val media = entry?.let { ent -> runBlocking { Storage.stores.mediaStore.getOrEmpty() }.find { it.GUID eqI ent.mediaID } } + val entry = mediaActivity?.let { act -> runBlocking { Stores.entryStore.getOrEmpty() }.find { it.GUID eqI act.mediaEntry } } + val media = entry?.let { ent -> runBlocking { Stores.mediaStore.getOrEmpty() }.find { it.GUID eqI ent.mediaID } } + val image = media?.let { ent -> runBlocking { Stores.imageStore.getOrEmpty() }.find { it.GUID eqI ent.thumbID } } ipc!!.scope.launch { if (errored) return@launch @@ -90,9 +91,8 @@ object DiscordRPC { media.name + if (media.isSeries.toBoolean()) " - ${entry.entryNumber}" else "" ) { button("View on GitHub", "https://github.com/Vodes?tab=repositories&q=Styx&language=kotlin") - val image = media.getThumb() if (mediaActivity.playing) - timestamps(currentUnixSeconds(), currentUnixSeconds() + MpvStatus.current.timeRemaining) + timestamps(1, 1 + MpvStatus.current.seconds.toLong()) if (image == null) { largeImage("styx", "v${BuildConfig.APP_VERSION}") } else { diff --git a/src/main/kotlin/moe/styx/logic/Files.kt b/src/main/kotlin/moe/styx/logic/Files.kt index 67c1bec..410d9bf 100644 --- a/src/main/kotlin/moe/styx/logic/Files.kt +++ b/src/main/kotlin/moe/styx/logic/Files.kt @@ -1,5 +1,6 @@ package moe.styx.logic +import moe.styx.common.extension.containsAny import moe.styx.common.extension.currentUnixSeconds import java.io.File @@ -64,8 +65,8 @@ object Files { val currentMillis = currentUnixSeconds() * 1000 val installerFiles = getAppDir().listFiles()?.toList() ?: emptyList() installerFiles - .filter { it.name.contains(".msi", true) } - .filter { (it.lastModified() - 30000) > currentMillis } + .filter { it.name.containsAny("msi", "rpm", "deb") } + .filter { it.lastModified() < (currentMillis - 30000) } .forEach { runCatching { it.delete() } } diff --git a/src/main/kotlin/moe/styx/logic/runner/MPVRunner.kt b/src/main/kotlin/moe/styx/logic/runner/MPVRunner.kt index 94ac725..f1b0a9d 100644 --- a/src/main/kotlin/moe/styx/logic/runner/MPVRunner.kt +++ b/src/main/kotlin/moe/styx/logic/runner/MPVRunner.kt @@ -8,6 +8,7 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import moe.styx.Main import moe.styx.common.compose.files.Storage +import moe.styx.common.compose.files.getBlocking import moe.styx.common.compose.http.Endpoints import moe.styx.common.compose.http.login import moe.styx.common.compose.settings @@ -28,27 +29,33 @@ import java.io.* var currentPlayer: MpvInstance? = null -fun launchMPV(entry: MediaEntry, append: Boolean, onFail: (String) -> Unit = {}, execUpdate: () -> Unit = {}) { +data class MpvFinishStatus(val statusCode: Int, val message: String = "") { + val isOK: Boolean = statusCode == 0 +} + +fun launchMPV(entry: MediaEntry, append: Boolean, onClose: (MpvFinishStatus) -> Unit = {}) { if (currentPlayer == null) { currentPlayer = MpvInstance() - currentPlayer!!.start(entry, onFail, execUpdate) { processCode -> + currentPlayer!!.start(entry, onClose) { processCode -> currentPlayer = null if (processCode > 0) { - onFail("Playback ended with a bad status code:\n$processCode") + onClose(MpvFinishStatus(processCode, "Playback ended with a bad status code: $processCode")) } else { val currentEntry = runBlocking { Storage.stores.entryStore.getOrEmpty() }.find { MpvStatus.current.file eqI it.GUID } if (MpvStatus.current.file.isNotBlank() && currentEntry != null && MpvStatus.current.seconds > 5) { - val watched = MediaWatched( - currentEntry.GUID, - login?.userID ?: "", - currentUnixSeconds(), - MpvStatus.current.seconds.toLong(), - MpvStatus.current.percentage.toFloat(), - MpvStatus.current.percentage.toFloat() - ) - RequestQueue.updateWatched(watched) - execUpdate() + launchThreaded { + val watched = MediaWatched( + currentEntry.GUID, + login?.userID ?: "", + currentUnixSeconds(), + MpvStatus.current.seconds.toLong(), + MpvStatus.current.percentage.toFloat(), + MpvStatus.current.percentage.toFloat() + ) + RequestQueue.updateWatched(watched).first.join() + onClose(MpvFinishStatus(0)) + } } } MpvStatus.current = MpvStatus.current.copy(file = "", paused = true) @@ -56,7 +63,7 @@ fun launchMPV(entry: MediaEntry, append: Boolean, onFail: (String) -> Unit = {}, } else { val result = currentPlayer!!.play(entry, append) if (!result) - onFail("Failed to add episode to the queue!") + onClose(MpvFinishStatus(-1, "Failed to add episode to the queue!")) } } @@ -66,7 +73,7 @@ class MpvInstance { private val tryFlatpak = settings["mpv-flatpak", false] private val instanceJob = Job() private var firstPrint = true - val execUpdate: () -> Unit = {} + var onClose: (MpvFinishStatus) -> Unit = {} private fun openRandomAccessFile(): RandomAccessFile { val socket = File(if (isWindows) "\\\\.\\pipe\\styx-mpvsocket" else "/tmp/styx-mpvsocket") @@ -92,7 +99,8 @@ class MpvInstance { return CoroutineScope(instanceJob) } - fun start(mediaEntry: MediaEntry, onFail: (String) -> Unit = {}, execUpdate: () -> Unit = {}, onFinish: (Int) -> Unit = {}): Boolean { + fun start(mediaEntry: MediaEntry, onClose: (MpvFinishStatus) -> Unit = {}, onFinish: (Int) -> Unit): Boolean { + this.onClose = onClose val systemMpv = settings["mpv-system", !isWindows] val useConfigRegardless = settings["mpv-system-styx-conf", !isWindows] val mpvExecutable = if (systemMpv || !isWindows) { @@ -104,14 +112,14 @@ class MpvInstance { File(Files.getMpvDir(), "mpv.exe") if (mpvExecutable == null || !mpvExecutable.exists()) { - onFail("MPV could not be found.") + onClose(MpvFinishStatus(404, "MPV executable not found!")) currentPlayer = null return false } val downloadedEntry = runBlocking { Storage.stores.downloadedStore.getOrEmpty() }.find { it.entryID eqI mediaEntry.GUID } - val uri = downloadedEntry?.path ?: "${Endpoints.WATCH.url}/${mediaEntry.GUID}?token=${login?.watchToken}" + val uri = downloadedEntry?.path ?: "${Endpoints.WATCH.url()}/${mediaEntry.GUID}?token=${login?.watchToken}" if ((login == null || login!!.watchToken.isBlank()) && downloadedEntry == null) { - onFail("You are not logged in right now.") + onClose(MpvFinishStatus(403, "You are not logged in or online and don't have this downloaded!")) currentPlayer = null return false } @@ -187,7 +195,7 @@ class MpvInstance { val current = MpvStatus.current.copy() val downloadedEntry = runBlocking { Storage.stores.downloadedStore.getOrEmpty() }.find { it.entryID eqI mediaEntry.GUID } val uri = downloadedEntry?.okioPath?.normalized()?.toString()?.replace("\\", "\\\\") - ?: "${Endpoints.WATCH.url}/${mediaEntry.GUID}?token=${login?.watchToken}" + ?: "${Endpoints.WATCH.url()}/${mediaEntry.GUID}?token=${login?.watchToken}" val appendType = if (current.percentage == 100 && current.eof) "append-play" else "append" val options = if (append) " $appendType" else "" val loaded = runCommand("loadfile \"$uri\"$options") @@ -196,7 +204,7 @@ class MpvInstance { runCommand("set pause no").also { return it } if (!append) { - val watched = Storage.watchedList.find { it.entryID eqI mediaEntry.GUID } + val watched = Storage.stores.watchedStore.getBlocking().find { it.entryID eqI mediaEntry.GUID } if (watched != null) launchThreaded { delay(100) @@ -290,8 +298,10 @@ data class MpvStatus( current.percentage.toFloat(), current.percentage.toFloat() ) - RequestQueue.updateWatched(watched) - currentPlayer?.let { it.execUpdate() } + launchThreaded { + RequestQueue.updateWatched(watched).first.join() + currentPlayer?.onClose?.let { it(MpvFinishStatus(0)) } + } } } current = new diff --git a/src/main/kotlin/moe/styx/logic/runner/ProcessUtils.kt b/src/main/kotlin/moe/styx/logic/runner/ProcessUtils.kt index b39bee6..e11ac1d 100644 --- a/src/main/kotlin/moe/styx/logic/runner/ProcessUtils.kt +++ b/src/main/kotlin/moe/styx/logic/runner/ProcessUtils.kt @@ -2,7 +2,10 @@ package moe.styx.logic.runner import moe.styx.common.extension.eqI import moe.styx.common.isWindows +import moe.styx.common.util.Log +import java.awt.Desktop import java.io.File +import java.net.URI fun getExecutableFromPath(name: String): File? { var name = name @@ -12,4 +15,18 @@ fun getExecutableFromPath(name: String): File? { .map { File(it) }.filter { it.exists() && it.isDirectory } return pathDirs.flatMap { it.listFiles()?.asList() ?: listOf() }.find { (if (isWindows) it.name else it.nameWithoutExtension) eqI name } +} + +fun openURI(uri: String) = openURI(URI(uri)) + +fun openURI(uri: URI) { + val xdgOpen = getExecutableFromPath("xdg-open") + if (xdgOpen != null) { + val result = ProcessBuilder(listOf(xdgOpen.absolutePath, uri.toString())).start().waitFor() + if (result == 0) + return + } + if (Desktop.isDesktopSupported()) { + runCatching { Desktop.getDesktop().browse(uri) }.onFailure { Log.e(exception = it) { "Failed to open URI: $uri" } } + } } \ No newline at end of file diff --git a/src/main/kotlin/moe/styx/logic/utils/MpvUtils.kt b/src/main/kotlin/moe/styx/logic/utils/MpvUtils.kt index 776f9bf..d7fde90 100644 --- a/src/main/kotlin/moe/styx/logic/utils/MpvUtils.kt +++ b/src/main/kotlin/moe/styx/logic/utils/MpvUtils.kt @@ -71,7 +71,7 @@ object MpvUtils { return launchGlobal { delay(8000) - val response = httpClient.get(Endpoints.MPV.url) + val response = httpClient.get(Endpoints.MPV.url()) if (!response.status.isSuccess()) { Log.w("MpvUtils::checkVersionAndDownload") { "Failed to check for mpv version." } @@ -92,7 +92,7 @@ object MpvUtils { launch { Log.i { "Downloading latest mpv bundle" } - val downloadResp = httpClient.get(Endpoints.MPV_DOWNLOAD.url) + val downloadResp = httpClient.get(Endpoints.MPV_DOWNLOAD.url()) if (Files.getMpvDir().exists() && downloadResp.status.isSuccess()) Files.getMpvDir().deleteRecursively() val openChannel = downloadResp.bodyAsChannel() diff --git a/src/main/kotlin/moe/styx/logic/utils/VersionCheck.kt b/src/main/kotlin/moe/styx/logic/utils/VersionCheck.kt index 4cac646..520dc5f 100644 --- a/src/main/kotlin/moe/styx/logic/utils/VersionCheck.kt +++ b/src/main/kotlin/moe/styx/logic/utils/VersionCheck.kt @@ -5,10 +5,11 @@ import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.util.cio.* import io.ktor.utils.io.* -import kotlinx.coroutines.* -import moe.styx.Styx__.BuildConfig +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import moe.styx.Styx_2.BuildConfig import moe.styx.common.compose.http.login -import moe.styx.common.extension.eqI import moe.styx.common.http.httpClient import moe.styx.common.util.launchGlobal import moe.styx.logic.Files @@ -16,26 +17,6 @@ import java.awt.Desktop import java.io.File import kotlin.system.exitProcess -// TODO: Actually check at some point lol. -private val versionRegex = "^version = \\\"(?(?\\d)\\.(?\\d)(?:\\.(?\\d))?)\\\"\$".toRegex(RegexOption.IGNORE_CASE) -fun isUpToDate(): Boolean = runBlocking { - val response = httpClient.get(BuildConfig.VERSION_CHECK_URL) - if (!response.status.isSuccess()) - return@runBlocking true - - val lines = response.bodyAsText().lines() - for (line in lines) { - val match = versionRegex.matchEntire(line) ?: continue - runCatching { - val parsed = match.groups["version"]!!.value - if (parsed eqI BuildConfig.APP_VERSION) - return@runBlocking true - } - } - - return@runBlocking false -} - fun downloadNewInstaller() = launchGlobal { val response = httpClient.get("${BuildConfig.BASE_URL}/download/desktop?token=${login?.accessToken}") if (!response.status.isSuccess()) diff --git a/src/main/kotlin/moe/styx/logic/viewmodels/DesktopOverViewModel.kt b/src/main/kotlin/moe/styx/logic/viewmodels/DesktopOverViewModel.kt new file mode 100644 index 0000000..990a318 --- /dev/null +++ b/src/main/kotlin/moe/styx/logic/viewmodels/DesktopOverViewModel.kt @@ -0,0 +1,8 @@ +package moe.styx.logic.viewmodels + +import moe.styx.common.compose.viewmodels.OverviewViewModel +import moe.styx.logic.utils.MpvUtils + +class DesktopOverViewModel : OverviewViewModel() { + override fun checkPlayerVersion() = MpvUtils.checkVersionAndDownload() +} \ No newline at end of file diff --git a/src/main/kotlin/moe/styx/theme/Color.kt b/src/main/kotlin/moe/styx/theme/Color.kt index f2ec1f8..8c42659 100644 --- a/src/main/kotlin/moe/styx/theme/Color.kt +++ b/src/main/kotlin/moe/styx/theme/Color.kt @@ -4,132 +4,297 @@ import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.ui.graphics.Color -val md_theme_light_primary = Color(0xFF006D31) -val md_theme_light_onPrimary = Color(0xFFFFFFFF) -val md_theme_light_primaryContainer = Color(0xFF6EFE94) -val md_theme_light_onPrimaryContainer = Color(0xFF00210A) -val md_theme_light_secondary = Color(0xFF842BD2) -val md_theme_light_onSecondary = Color(0xFFFFFFFF) -val md_theme_light_secondaryContainer = Color(0xFFF0DBFF) -val md_theme_light_onSecondaryContainer = Color(0xFF2C0051) -val md_theme_light_tertiary = Color(0xFF206C2F) -val md_theme_light_onTertiary = Color(0xFFFFFFFF) -val md_theme_light_tertiaryContainer = Color(0xFFA6F5A8) -val md_theme_light_onTertiaryContainer = Color(0xFF002106) -val md_theme_light_error = Color(0xFFBA1A1A) -val md_theme_light_errorContainer = Color(0xFFFFDAD6) -val md_theme_light_onError = Color(0xFFFFFFFF) -val md_theme_light_onErrorContainer = Color(0xFF410002) -val md_theme_light_background = Color(0xFFFCFDF7) -val md_theme_light_onBackground = Color(0xFF1A1C19) -val md_theme_light_surface = Color(0xFFFCFDF7) -val md_theme_light_onSurface = Color(0xFF1A1C19) -val md_theme_light_surfaceVariant = Color(0xFFDDE5DA) -val md_theme_light_onSurfaceVariant = Color(0xFF414941) -val md_theme_light_outline = Color(0xFF727970) -val md_theme_light_inverseOnSurface = Color(0xFFF0F1EC) -val md_theme_light_inverseSurface = Color(0xFF2E312E) -val md_theme_light_inversePrimary = Color(0xFF4FE17B) -val md_theme_light_shadow = Color(0xFF000000) -val md_theme_light_surfaceTint = Color(0xFF006D31) -val md_theme_light_outlineVariant = Color(0xFFC1C9BE) -val md_theme_light_scrim = Color(0xFF000000) +val primaryLight = Color(0xFF006D31) +val onPrimaryLight = Color(0xFFFFFFFF) +val primaryContainerLight = Color(0xFF59EA83) +val onPrimaryContainerLight = Color(0xFF00461D) +val secondaryLight = Color(0xFF611E9A) +val onSecondaryLight = Color(0xFFFFFFFF) +val secondaryContainerLight = Color(0xFF8749C1) +val onSecondaryContainerLight = Color(0xFFFFFFFF) +val tertiaryLight = Color(0xFF00561C) +val onTertiaryLight = Color(0xFFFFFFFF) +val tertiaryContainerLight = Color(0xFF327C3D) +val onTertiaryContainerLight = Color(0xFFFFFFFF) +val errorLight = Color(0xFFBA1A1A) +val onErrorLight = Color(0xFFFFFFFF) +val errorContainerLight = Color(0xFFFFDAD6) +val onErrorContainerLight = Color(0xFF410002) +val backgroundLight = Color(0xFFF3FCF0) +val onBackgroundLight = Color(0xFF161D16) +val surfaceLight = Color(0xFFF3FCF0) +val onSurfaceLight = Color(0xFF161D16) +val surfaceVariantLight = Color(0xFFD8E7D5) +val onSurfaceVariantLight = Color(0xFF3D4A3D) +val outlineLight = Color(0xFF6D7B6C) +val outlineVariantLight = Color(0xFFBCCBBA) +val scrimLight = Color(0xFF000000) +val inverseSurfaceLight = Color(0xFF2B322B) +val inverseOnSurfaceLight = Color(0xFFEBF3E7) +val inversePrimaryLight = Color(0xFF4FE17B) +val surfaceDimLight = Color(0xFFD4DCD1) +val surfaceBrightLight = Color(0xFFF3FCF0) +val surfaceContainerLowestLight = Color(0xFFFFFFFF) +val surfaceContainerLowLight = Color(0xFFEEF6EA) +val surfaceContainerLight = Color(0xFFE8F0E4) +val surfaceContainerHighLight = Color(0xFFE2EBDF) +val surfaceContainerHighestLight = Color(0xFFDDE5D9) -val md_theme_dark_primary = Color(0xFF4FE17B) -val md_theme_dark_onPrimary = Color(0xFF003916) -val md_theme_dark_primaryContainer = Color(0xFF005323) -val md_theme_dark_onPrimaryContainer = Color(0xFF6EFE94) -val md_theme_dark_secondary = Color(0xFFDDB8FF) -val md_theme_dark_onSecondary = Color(0xFF490080) -val md_theme_dark_secondaryContainer = Color(0xFF6800B3) -val md_theme_dark_onSecondaryContainer = Color(0xFFF0DBFF) -val md_theme_dark_tertiary = Color(0xFF8BD88E) -val md_theme_dark_onTertiary = Color(0xFF003910) -val md_theme_dark_tertiaryContainer = Color(0xFF00531B) -val md_theme_dark_onTertiaryContainer = Color(0xFFA6F5A8) -val md_theme_dark_error = Color(0xFFFFB4AB) -val md_theme_dark_errorContainer = Color(0xFF93000A) -val md_theme_dark_onError = Color(0xFF690005) -val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) -val md_theme_dark_background = Color(0xFF1A1C19) -val md_theme_dark_onBackground = Color(0xFFE2E3DE) -val md_theme_dark_surface = Color(0xFF1A1C19) -val md_theme_dark_onSurface = Color(0xFFE2E3DE) -val md_theme_dark_surfaceVariant = Color(0xFF414941) -val md_theme_dark_onSurfaceVariant = Color(0xFFC1C9BE) -val md_theme_dark_outline = Color(0xFF8B9389) -val md_theme_dark_inverseOnSurface = Color(0xFF1A1C19) -val md_theme_dark_inverseSurface = Color(0xFFE2E3DE) -val md_theme_dark_inversePrimary = Color(0xFF006D31) -val md_theme_dark_shadow = Color(0xFF000000) -val md_theme_dark_surfaceTint = Color(0xFF4FE17B) -val md_theme_dark_outlineVariant = Color(0xFF414941) -val md_theme_dark_scrim = Color(0xFF000000) +val primaryLightMediumContrast = Color(0xFF004E21) +val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) +val primaryContainerLightMediumContrast = Color(0xFF00873E) +val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val secondaryLightMediumContrast = Color(0xFF5F1C99) +val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) +val secondaryContainerLightMediumContrast = Color(0xFF8749C1) +val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryLightMediumContrast = Color(0xFF004E19) +val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightMediumContrast = Color(0xFF327C3D) +val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val errorLightMediumContrast = Color(0xFF8C0009) +val onErrorLightMediumContrast = Color(0xFFFFFFFF) +val errorContainerLightMediumContrast = Color(0xFFDA342E) +val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) +val backgroundLightMediumContrast = Color(0xFFF3FCF0) +val onBackgroundLightMediumContrast = Color(0xFF161D16) +val surfaceLightMediumContrast = Color(0xFFF3FCF0) +val onSurfaceLightMediumContrast = Color(0xFF161D16) +val surfaceVariantLightMediumContrast = Color(0xFFD8E7D5) +val onSurfaceVariantLightMediumContrast = Color(0xFF394639) +val outlineLightMediumContrast = Color(0xFF556355) +val outlineVariantLightMediumContrast = Color(0xFF707E70) +val scrimLightMediumContrast = Color(0xFF000000) +val inverseSurfaceLightMediumContrast = Color(0xFF2B322B) +val inverseOnSurfaceLightMediumContrast = Color(0xFFEBF3E7) +val inversePrimaryLightMediumContrast = Color(0xFF4FE17B) +val surfaceDimLightMediumContrast = Color(0xFFD4DCD1) +val surfaceBrightLightMediumContrast = Color(0xFFF3FCF0) +val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightMediumContrast = Color(0xFFEEF6EA) +val surfaceContainerLightMediumContrast = Color(0xFFE8F0E4) +val surfaceContainerHighLightMediumContrast = Color(0xFFE2EBDF) +val surfaceContainerHighestLightMediumContrast = Color(0xFFDDE5D9) +val primaryLightHighContrast = Color(0xFF00290E) +val onPrimaryLightHighContrast = Color(0xFFFFFFFF) +val primaryContainerLightHighContrast = Color(0xFF004E21) +val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) +val secondaryLightHighContrast = Color(0xFF36005F) +val onSecondaryLightHighContrast = Color(0xFFFFFFFF) +val secondaryContainerLightHighContrast = Color(0xFF5F1C99) +val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) +val tertiaryLightHighContrast = Color(0xFF002909) +val onTertiaryLightHighContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightHighContrast = Color(0xFF004E19) +val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) +val errorLightHighContrast = Color(0xFF4E0002) +val onErrorLightHighContrast = Color(0xFFFFFFFF) +val errorContainerLightHighContrast = Color(0xFF8C0009) +val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) +val backgroundLightHighContrast = Color(0xFFF3FCF0) +val onBackgroundLightHighContrast = Color(0xFF161D16) +val surfaceLightHighContrast = Color(0xFFF3FCF0) +val onSurfaceLightHighContrast = Color(0xFF000000) +val surfaceVariantLightHighContrast = Color(0xFFD8E7D5) +val onSurfaceVariantLightHighContrast = Color(0xFF1A271C) +val outlineLightHighContrast = Color(0xFF394639) +val outlineVariantLightHighContrast = Color(0xFF394639) +val scrimLightHighContrast = Color(0xFF000000) +val inverseSurfaceLightHighContrast = Color(0xFF2B322B) +val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) +val inversePrimaryLightHighContrast = Color(0xFFACFFB8) +val surfaceDimLightHighContrast = Color(0xFFD4DCD1) +val surfaceBrightLightHighContrast = Color(0xFFF3FCF0) +val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightHighContrast = Color(0xFFEEF6EA) +val surfaceContainerLightHighContrast = Color(0xFFE8F0E4) +val surfaceContainerHighLightHighContrast = Color(0xFFE2EBDF) +val surfaceContainerHighestLightHighContrast = Color(0xFFDDE5D9) -val seed = Color(0xFF4FE17B) +val primaryDark = Color(0xFFA3FFB2) +val onPrimaryDark = Color(0xFF003916) +val primaryContainerDark = Color(0xFF47DA75) +val onPrimaryContainerDark = Color(0xFF003B17) +val secondaryDark = Color(0xFFDEB7FF) +val onSecondaryDark = Color(0xFF4A007F) +val secondaryContainerDark = Color(0xFF7E3FB7) +val onSecondaryContainerDark = Color(0xFFFFFFFF) +val tertiaryDark = Color(0xFF8BD88E) +val onTertiaryDark = Color(0xFF003910) +val tertiaryContainerDark = Color(0xFF126226) +val onTertiaryContainerDark = Color(0xFFE4FFDF) +val errorDark = Color(0xFFFFB4AB) +val onErrorDark = Color(0xFF690005) +val errorContainerDark = Color(0xFF93000A) +val onErrorContainerDark = Color(0xFFFFDAD6) +val backgroundDark = Color(0xFF0E150E) +val onBackgroundDark = Color(0xFFDDE5D9) +val surfaceDark = Color(0xFF0E150E) +val onSurfaceDark = Color(0xFFDDE5D9) +val surfaceVariantDark = Color(0xFF3D4A3D) +val onSurfaceVariantDark = Color(0xFFBCCBBA) +val outlineDark = Color(0xFF869585) +val outlineVariantDark = Color(0xFF3D4A3D) +val scrimDark = Color(0xFF000000) +val inverseSurfaceDark = Color(0xFFDDE5D9) +val inverseOnSurfaceDark = Color(0xFF2B322B) +val inversePrimaryDark = Color(0xFF006D31) +val surfaceDimDark = Color(0xFF0E150E) +val surfaceBrightDark = Color(0xFF333B33) +val surfaceContainerLowestDark = Color(0xFF091009) +val surfaceContainerLowDark = Color(0xFF161D16) +val surfaceContainerDark = Color(0xFF1A211A) +val surfaceContainerHighDark = Color(0xFF242C24) +val surfaceContainerHighestDark = Color(0xFF2F372F) + +val primaryDarkMediumContrast = Color(0xFFA3FFB2) +val onPrimaryDarkMediumContrast = Color(0xFF003815) +val primaryContainerDarkMediumContrast = Color(0xFF47DA75) +val onPrimaryContainerDarkMediumContrast = Color(0xFF000A02) +val secondaryDarkMediumContrast = Color(0xFFE1BDFF) +val onSecondaryDarkMediumContrast = Color(0xFF250044) +val secondaryContainerDarkMediumContrast = Color(0xFFB374EE) +val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) +val tertiaryDarkMediumContrast = Color(0xFF8FDD92) +val onTertiaryDarkMediumContrast = Color(0xFF001B05) +val tertiaryContainerDarkMediumContrast = Color(0xFF56A15D) +val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) +val errorDarkMediumContrast = Color(0xFFFFBAB1) +val onErrorDarkMediumContrast = Color(0xFF370001) +val errorContainerDarkMediumContrast = Color(0xFFFF5449) +val onErrorContainerDarkMediumContrast = Color(0xFF000000) +val backgroundDarkMediumContrast = Color(0xFF0E150E) +val onBackgroundDarkMediumContrast = Color(0xFFDDE5D9) +val surfaceDarkMediumContrast = Color(0xFF0E150E) +val onSurfaceDarkMediumContrast = Color(0xFFF5FDF1) +val surfaceVariantDarkMediumContrast = Color(0xFF3D4A3D) +val onSurfaceVariantDarkMediumContrast = Color(0xFFC0CFBE) +val outlineDarkMediumContrast = Color(0xFF98A797) +val outlineVariantDarkMediumContrast = Color(0xFF798778) +val scrimDarkMediumContrast = Color(0xFF000000) +val inverseSurfaceDarkMediumContrast = Color(0xFFDDE5D9) +val inverseOnSurfaceDarkMediumContrast = Color(0xFF242C24) +val inversePrimaryDarkMediumContrast = Color(0xFF005424) +val surfaceDimDarkMediumContrast = Color(0xFF0E150E) +val surfaceBrightDarkMediumContrast = Color(0xFF333B33) +val surfaceContainerLowestDarkMediumContrast = Color(0xFF091009) +val surfaceContainerLowDarkMediumContrast = Color(0xFF161D16) +val surfaceContainerDarkMediumContrast = Color(0xFF1A211A) +val surfaceContainerHighDarkMediumContrast = Color(0xFF242C24) +val surfaceContainerHighestDarkMediumContrast = Color(0xFF2F372F) + +val primaryDarkHighContrast = Color(0xFFF0FFED) +val onPrimaryDarkHighContrast = Color(0xFF000000) +val primaryContainerDarkHighContrast = Color(0xFF54E57E) +val onPrimaryContainerDarkHighContrast = Color(0xFF000000) +val secondaryDarkHighContrast = Color(0xFFFFF9FC) +val onSecondaryDarkHighContrast = Color(0xFF000000) +val secondaryContainerDarkHighContrast = Color(0xFFE1BDFF) +val onSecondaryContainerDarkHighContrast = Color(0xFF000000) +val tertiaryDarkHighContrast = Color(0xFFF0FFEB) +val onTertiaryDarkHighContrast = Color(0xFF000000) +val tertiaryContainerDarkHighContrast = Color(0xFF8FDD92) +val onTertiaryContainerDarkHighContrast = Color(0xFF000000) +val errorDarkHighContrast = Color(0xFFFFF9F9) +val onErrorDarkHighContrast = Color(0xFF000000) +val errorContainerDarkHighContrast = Color(0xFFFFBAB1) +val onErrorContainerDarkHighContrast = Color(0xFF000000) +val backgroundDarkHighContrast = Color(0xFF0E150E) +val onBackgroundDarkHighContrast = Color(0xFFDDE5D9) +val surfaceDarkHighContrast = Color(0xFF0E150E) +val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkHighContrast = Color(0xFF3D4A3D) +val onSurfaceVariantDarkHighContrast = Color(0xFFF0FFED) +val outlineDarkHighContrast = Color(0xFFC0CFBE) +val outlineVariantDarkHighContrast = Color(0xFFC0CFBE) +val scrimDarkHighContrast = Color(0xFF000000) +val inverseSurfaceDarkHighContrast = Color(0xFFDDE5D9) +val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) +val inversePrimaryDarkHighContrast = Color(0xFF003212) +val surfaceDimDarkHighContrast = Color(0xFF0E150E) +val surfaceBrightDarkHighContrast = Color(0xFF333B33) +val surfaceContainerLowestDarkHighContrast = Color(0xFF091009) +val surfaceContainerLowDarkHighContrast = Color(0xFF161D16) +val surfaceContainerDarkHighContrast = Color(0xFF1A211A) +val surfaceContainerHighDarkHighContrast = Color(0xFF242C24) +val surfaceContainerHighestDarkHighContrast = Color(0xFF2F372F) -val LightColorScheme = lightColorScheme( - primary = md_theme_light_primary, - onPrimary = md_theme_light_onPrimary, - primaryContainer = md_theme_light_primaryContainer, - onPrimaryContainer = md_theme_light_onPrimaryContainer, - secondary = md_theme_light_secondary, - onSecondary = md_theme_light_onSecondary, - secondaryContainer = md_theme_light_secondaryContainer, - onSecondaryContainer = md_theme_light_onSecondaryContainer, - tertiary = md_theme_light_tertiary, - onTertiary = md_theme_light_onTertiary, - tertiaryContainer = md_theme_light_tertiaryContainer, - onTertiaryContainer = md_theme_light_onTertiaryContainer, - error = md_theme_light_error, - errorContainer = md_theme_light_errorContainer, - onError = md_theme_light_onError, - onErrorContainer = md_theme_light_onErrorContainer, - background = md_theme_light_background, - onBackground = md_theme_light_onBackground, - surface = md_theme_light_surface, - onSurface = md_theme_light_onSurface, - surfaceVariant = md_theme_light_surfaceVariant, - onSurfaceVariant = md_theme_light_onSurfaceVariant, - outline = md_theme_light_outline, - inverseOnSurface = md_theme_light_inverseOnSurface, - inverseSurface = md_theme_light_inverseSurface, - inversePrimary = md_theme_light_inversePrimary, - surfaceTint = md_theme_light_surfaceTint, - outlineVariant = md_theme_light_outlineVariant, - scrim = md_theme_light_scrim, +val seed = Color(0xFF4FE17B) + +val lightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, ) -val DarkColorScheme = darkColorScheme( - primary = md_theme_dark_primary, - onPrimary = md_theme_dark_onPrimary, - primaryContainer = md_theme_dark_primaryContainer, - onPrimaryContainer = md_theme_dark_onPrimaryContainer, - secondary = md_theme_dark_secondary, - onSecondary = md_theme_dark_onSecondary, - secondaryContainer = md_theme_dark_secondaryContainer, - onSecondaryContainer = md_theme_dark_onSecondaryContainer, - tertiary = md_theme_dark_tertiary, - onTertiary = md_theme_dark_onTertiary, - tertiaryContainer = md_theme_dark_tertiaryContainer, - onTertiaryContainer = md_theme_dark_onTertiaryContainer, - error = md_theme_dark_error, - errorContainer = md_theme_dark_errorContainer, - onError = md_theme_dark_onError, - onErrorContainer = md_theme_dark_onErrorContainer, - background = md_theme_dark_background, - onBackground = md_theme_dark_onBackground, - surface = md_theme_dark_surface, - onSurface = md_theme_dark_onSurface, - surfaceVariant = md_theme_dark_surfaceVariant, - onSurfaceVariant = md_theme_dark_onSurfaceVariant, - outline = md_theme_dark_outline, - inverseOnSurface = md_theme_dark_inverseOnSurface, - inverseSurface = md_theme_dark_inverseSurface, - inversePrimary = md_theme_dark_inversePrimary, - surfaceTint = md_theme_dark_surfaceTint, - outlineVariant = md_theme_dark_outlineVariant, - scrim = md_theme_dark_scrim, +val darkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, ) \ No newline at end of file diff --git a/src/main/kotlin/moe/styx/theme/Theme.kt b/src/main/kotlin/moe/styx/theme/Theme.kt index ace1ac7..6fd5bee 100644 --- a/src/main/kotlin/moe/styx/theme/Theme.kt +++ b/src/main/kotlin/moe/styx/theme/Theme.kt @@ -33,37 +33,43 @@ object AppFont { Font("fonts/OpenSans-Italic.ttf", FontWeight.Normal, FontStyle.Italic), Font("fonts/OpenSans-Medium.ttf", FontWeight.Medium, FontStyle.Normal), Font("fonts/OpenSans-MediumItalic.ttf", FontWeight.Medium, FontStyle.Italic), - Font("fonts/OpenSans-SemiBold.ttf", FontWeight.SemiBold, FontStyle.Normal), - Font("fonts/OpenSans-SemiBoldItalic.ttf", FontWeight.SemiBold, FontStyle.Italic), Font("fonts/OpenSans-Bold.ttf", FontWeight.Bold, FontStyle.Normal), Font("fonts/OpenSans-BoldItalic.ttf", FontWeight.Bold, FontStyle.Italic), - Font("fonts/OpenSans-ExtraBold.ttf", FontWeight.ExtraBold, FontStyle.Normal), - Font("fonts/OpenSans-ExtraBoldItalic.ttf", FontWeight.ExtraBold, FontStyle.Italic), + ) + val JetbrainsMono = FontFamily( + Font("fonts/JetBrainsMono-Light.ttf", FontWeight.Light, FontStyle.Normal), + Font("fonts/JetBrainsMono-LightItalic.ttf", FontWeight.Light, FontStyle.Italic), + Font("fonts/JetBrainsMono-Regular.ttf", FontWeight.Normal, FontStyle.Normal), + Font("fonts/JetBrainsMono-Italic.ttf", FontWeight.Normal, FontStyle.Italic), + Font("fonts/JetBrainsMono-Medium.ttf", FontWeight.Medium, FontStyle.Normal), + Font("fonts/JetBrainsMono-MediumItalic.ttf", FontWeight.Medium, FontStyle.Italic), + Font("fonts/JetBrainsMono-Bold.ttf", FontWeight.Bold, FontStyle.Normal), + Font("fonts/JetBrainsMono-BoldItalic.ttf", FontWeight.Bold, FontStyle.Italic), ) } -val AppTypography = Typography( - displayLarge = defaultTypo.displayLarge.copy(fontFamily = AppFont.OpenSans), - displayMedium = defaultTypo.displayMedium.copy(fontFamily = AppFont.OpenSans), - displaySmall = defaultTypo.displaySmall.copy(fontFamily = AppFont.OpenSans), - - headlineLarge = defaultTypo.headlineLarge.copy(fontFamily = AppFont.OpenSans), - headlineMedium = defaultTypo.headlineMedium.copy(fontFamily = AppFont.OpenSans), - headlineSmall = defaultTypo.headlineSmall.copy(fontFamily = AppFont.OpenSans), +val FontFamily.Typography: Typography + get() = Typography( + displayLarge = defaultTypo.displayLarge.copy(fontFamily = this), + displayMedium = defaultTypo.displayMedium.copy(fontFamily = this), + displaySmall = defaultTypo.displaySmall.copy(fontFamily = this), - titleLarge = defaultTypo.titleLarge.copy(fontFamily = AppFont.OpenSans), - titleMedium = defaultTypo.titleMedium.copy(fontFamily = AppFont.OpenSans), - titleSmall = defaultTypo.titleSmall.copy(fontFamily = AppFont.OpenSans), + headlineLarge = defaultTypo.headlineLarge.copy(fontFamily = this), + headlineMedium = defaultTypo.headlineMedium.copy(fontFamily = this), + headlineSmall = defaultTypo.headlineSmall.copy(fontFamily = this), - bodyLarge = defaultTypo.bodyLarge.copy(fontFamily = AppFont.OpenSans), - bodyMedium = defaultTypo.bodyMedium.copy(fontFamily = AppFont.OpenSans), - bodySmall = defaultTypo.bodySmall.copy(fontFamily = AppFont.OpenSans), + titleLarge = defaultTypo.titleLarge.copy(fontFamily = this), + titleMedium = defaultTypo.titleMedium.copy(fontFamily = this), + titleSmall = defaultTypo.titleSmall.copy(fontFamily = this), - labelLarge = defaultTypo.labelLarge.copy(fontFamily = AppFont.OpenSans), - labelMedium = defaultTypo.labelMedium.copy(fontFamily = AppFont.OpenSans), - labelSmall = defaultTypo.labelSmall.copy(fontFamily = AppFont.OpenSans) -) + bodyLarge = defaultTypo.bodyLarge.copy(fontFamily = this), + bodyMedium = defaultTypo.bodyMedium.copy(fontFamily = this), + bodySmall = defaultTypo.bodySmall.copy(fontFamily = this), + labelLarge = defaultTypo.labelLarge.copy(fontFamily = this), + labelMedium = defaultTypo.labelMedium.copy(fontFamily = this), + labelSmall = defaultTypo.labelSmall.copy(fontFamily = this) + ) private val animationSpec: TweenSpec = tween(durationMillis = 650) diff --git a/src/main/kotlin/moe/styx/views/Tabs.kt b/src/main/kotlin/moe/styx/views/Tabs.kt index 9ebec75..e9feee1 100644 --- a/src/main/kotlin/moe/styx/views/Tabs.kt +++ b/src/main/kotlin/moe/styx/views/Tabs.kt @@ -11,26 +11,31 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.debounce import moe.styx.common.compose.components.search.MediaSearch import moe.styx.common.compose.utils.SearchState +import moe.styx.common.compose.viewmodels.ListPosViewModel +import moe.styx.common.compose.viewmodels.MainDataViewModelStorage import moe.styx.common.data.Favourite import moe.styx.common.data.Media import moe.styx.components.overviews.MediaGrid import moe.styx.components.overviews.MediaList -import moe.styx.views.anime.tabs.* -import moe.styx.views.settings.SettingsView +import moe.styx.views.anime.tabs.MediaListView +import moe.styx.views.anime.tabs.ScheduleView +import moe.styx.views.settings.SettingsTab -val defaultTab = AnimeListView() -val movieTab = MovieListView() -var favsTab = FavouritesListView() +val defaultTab = MediaListView() +val movieTab = MediaListView(movies = true) +var favsTab = MediaListView(favourites = true) val scheduleTab = ScheduleView() -val settingsTab = SettingsView() +val settingsTab = SettingsTab() @OptIn(FlowPreview::class) @Composable internal fun Tab.barWithListComp( mediaSearch: MediaSearch, initialState: SearchState, + storage: MainDataViewModelStorage, filtered: List, useList: Boolean = false, + listPosViewModel: ListPosViewModel, showUnseen: Boolean = false, favourites: List = emptyList() ) { @@ -40,9 +45,9 @@ internal fun Tab.barWithListComp( val flow by mediaSearch.stateEmitter.debounce(150L).collectAsState(initialState) val processedMedia = flow.filterMedia(filtered, favourites) if (!useList) - MediaGrid(processedMedia, showUnseen) + MediaGrid(storage, processedMedia, listPosViewModel, showUnseen) else - MediaList(processedMedia) + MediaList(storage, processedMedia, listPosViewModel) } } } \ No newline at end of file diff --git a/src/main/kotlin/moe/styx/views/anime/AnimeDetailView.kt b/src/main/kotlin/moe/styx/views/anime/AnimeDetailView.kt index 727ab1a..e42c5e9 100644 --- a/src/main/kotlin/moe/styx/views/anime/AnimeDetailView.kt +++ b/src/main/kotlin/moe/styx/views/anime/AnimeDetailView.kt @@ -14,33 +14,34 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey +import com.dokar.sonner.TextToastAction +import com.dokar.sonner.Toast +import com.dokar.sonner.ToastType import com.russhwolf.settings.get import moe.styx.common.compose.components.anime.* import moe.styx.common.compose.components.buttons.FavouriteIconButton import moe.styx.common.compose.components.layout.MainScaffold import moe.styx.common.compose.components.misc.OnlineUsersIcon import moe.styx.common.compose.extensions.getPainter -import moe.styx.common.compose.extensions.getThumb -import moe.styx.common.compose.files.Storage -import moe.styx.common.compose.files.getCurrentAndCollectFlow import moe.styx.common.compose.settings -import moe.styx.common.compose.threads.Heartbeats import moe.styx.common.compose.utils.LocalGlobalNavigator +import moe.styx.common.compose.utils.LocalToaster +import moe.styx.common.compose.viewmodels.MainDataViewModel +import moe.styx.common.compose.viewmodels.MediaStorage import moe.styx.common.data.Media import moe.styx.common.data.MediaEntry -import moe.styx.common.extension.eqI import moe.styx.components.anime.AppendDialog import moe.styx.components.anime.BigScalingCardImage -import moe.styx.components.anime.FailedDialog import moe.styx.logic.runner.currentPlayer import moe.styx.logic.runner.launchMPV +import moe.styx.logic.runner.openURI import moe.styx.logic.utils.* import moe.styx.theme.AppShapes +import moe.styx.views.settings.SettingsTab import moe.styx.views.settings.SettingsView -import java.awt.Desktop -import java.net.URI class AnimeDetailView(private val mediaID: String) : Screen { @@ -51,70 +52,70 @@ class AnimeDetailView(private val mediaID: String) : Screen { @Composable override fun Content() { val nav = LocalGlobalNavigator.current + val toaster = LocalToaster.current + val sm = nav.rememberNavigatorScreenModel("main-vm") { MainDataViewModel() } + val storage by sm.storageFlow.collectAsState() + val mediaStorage = remember(storage) { sm.getMediaStorageForID(mediaID, storage) } - val mediaList by Storage.stores.mediaStore.getCurrentAndCollectFlow() - val media = remember { mediaList.find { it.GUID eqI mediaID } } - if (media == null) { - nav.pop() - return - } - val entries = fetchEntries(mediaID) - - val preferGerman = settings["prefer-german-metadata", false] + val preferGerman = remember { settings["prefer-german-metadata", false] } val scrollState = rememberScrollState() val showSelection = remember { mutableStateOf(false) } - MainScaffold(title = media.name, actions = { + MainScaffold(title = mediaStorage.media.name, actions = { OnlineUsersIcon { nav.pushMediaView(it, true) } - FavouriteIconButton(media) + FavouriteIconButton(mediaStorage.media, sm, storage) }) { var failedToPlayMessage by remember { mutableStateOf("") } if (failedToPlayMessage.isNotBlank()) { - FailedDialog(failedToPlayMessage, Modifier.fillMaxWidth(0.6F)) { - failedToPlayMessage = "" - if (it) nav.push(SettingsView()) - } + toaster.show(Toast(failedToPlayMessage, type = ToastType.Error, action = TextToastAction("Open Settings") { + nav.push(SettingsView()) + })) + failedToPlayMessage = "" } var appendEntry by remember { mutableStateOf(null) } if (appendEntry != null) { AppendDialog(appendEntry!!, Modifier.fillMaxWidth(0.6F), onDismiss = { appendEntry = null }) { - failedToPlayMessage = it - appendEntry = null + if (!it.isOK) + failedToPlayMessage = it.message + else + sm.updateData(true) } } ElevatedCard( - Modifier.padding(8.dp).fillMaxSize(), - colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.surface), - elevation = CardDefaults.elevatedCardElevation(8.dp) + Modifier.padding(8.dp).fillMaxSize() ) { Row(Modifier.padding(5.dp).fillMaxSize()) { Column(Modifier.fillMaxHeight().fillMaxWidth(.52F).verticalScroll(scrollState)) { - StupidImageNameArea(media) + StupidImageNameArea(mediaStorage) Spacer(Modifier.height(6.dp)) Text("About", Modifier.padding(6.dp, 2.dp), style = MaterialTheme.typography.titleLarge) - MediaGenreListing(media) - val synopsis = if (!media.synopsisDE.isNullOrBlank() && preferGerman) media.synopsisDE else media.synopsisEN + MediaGenreListing(mediaStorage.media) + val synopsis = + if (!mediaStorage.media.synopsisDE.isNullOrBlank() && preferGerman) mediaStorage.media.synopsisDE else mediaStorage.media.synopsisEN if (!synopsis.isNullOrBlank()) SelectionContainer { Text(synopsis.removeSomeHTMLTags(), Modifier.padding(6.dp), style = MaterialTheme.typography.bodyMedium) } - if (media.sequel != null || media.prequel != null) { + if (mediaStorage.sequel != null || mediaStorage.prequel != null) { HorizontalDivider(Modifier.fillMaxWidth().padding(0.dp, 4.dp, 0.dp, 2.dp), thickness = 2.dp) - MediaRelations(media, mediaList) { nav.pushMediaView(it, true) } + MediaRelations(mediaStorage) { nav.pushMediaView(it, true) } } } VerticalDivider(Modifier.fillMaxHeight().padding(6.dp), thickness = 3.dp) - EpisodeList(entries, showSelection, SettingsView(), onPlay = { entry -> + EpisodeList(storage, mediaStorage, showSelection, SettingsTab(), onPlay = { entry -> if (currentPlayer == null) { - launchMPV(entry, false, { - failedToPlayMessage = it - }) + launchMPV(entry, false) { + if (!it.isOK) + failedToPlayMessage = it.message + else + sm.updateData(true) + } } else { appendEntry = entry } @@ -128,14 +129,14 @@ class AnimeDetailView(private val mediaID: String) : Screen { @Composable fun StupidImageNameArea( - media: Media, + mediaStorage: MediaStorage, dynamicMaxWidth: Dp = 760.dp, requiredWidth: Dp = 385.dp, requiredHeight: Dp = 535.dp, otherContent: @Composable () -> Unit = {} ) { - val img = media.getThumb()!! - val painter = img.getPainter() + val (media, img) = mediaStorage.media to mediaStorage.image + val painter = img?.getPainter() BoxWithConstraints { val width = this.maxWidth Row(Modifier.align(Alignment.TopStart).height(IntrinsicSize.Max).fillMaxWidth()) { @@ -156,16 +157,6 @@ fun StupidImageNameArea( } } -@Composable -fun fetchEntries(mediaID: String): List { - Heartbeats.mediaActivity = null - val flow by Storage.stores.entryStore.getCurrentAndCollectFlow() - val filtered = flow.filter { it.mediaID eqI mediaID } - return if (settings["episode-asc", false]) filtered.sortedBy { - it.entryNumber.toDoubleOrNull() ?: 0.0 - } else filtered.sortedByDescending { it.entryNumber.toDoubleOrNull() ?: 0.0 } -} - @Composable fun MappingIcons(media: Media) { val malURL = media.getURLFromMap(StackType.MAL) @@ -178,8 +169,7 @@ fun MappingIcons(media: Media) { painterResource("icons/al.svg"), "AniList", Modifier.padding(8.dp, 3.dp).size(25.dp).clip(AppShapes.small).clickable { - if (Desktop.isDesktopSupported()) - Desktop.getDesktop().browse(URI(anilistURL)) + openURI(anilistURL) }, contentScale = ContentScale.FillWidth, colorFilter = filter @@ -189,8 +179,7 @@ fun MappingIcons(media: Media) { painterResource("icons/myanimelist.svg"), "MyAnimeList", Modifier.padding(8.dp, 3.dp).size(25.dp).clip(AppShapes.small).clickable { - if (Desktop.isDesktopSupported()) - Desktop.getDesktop().browse(URI(malURL)) + openURI(malURL) }, contentScale = ContentScale.FillWidth, colorFilter = filter @@ -200,8 +189,7 @@ fun MappingIcons(media: Media) { painterResource("icons/tmdb.svg"), "TheMovieDB", Modifier.padding(8.dp, 3.dp).size(25.dp).clip(AppShapes.small).clickable { - if (Desktop.isDesktopSupported()) - Desktop.getDesktop().browse(URI(tmdbURL)) + openURI(tmdbURL) }, contentScale = ContentScale.FillWidth, colorFilter = filter diff --git a/src/main/kotlin/moe/styx/views/anime/AnimeOverview.kt b/src/main/kotlin/moe/styx/views/anime/AnimeOverview.kt index f4b179b..3cad725 100644 --- a/src/main/kotlin/moe/styx/views/anime/AnimeOverview.kt +++ b/src/main/kotlin/moe/styx/views/anime/AnimeOverview.kt @@ -1,43 +1,117 @@ package moe.styx.views.anime +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.TooltipArea import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.QuestionMark -import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.* import androidx.compose.material3.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.tab.* +import com.dokar.sonner.TextToastAction +import com.dokar.sonner.Toast +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import moe.styx.Main -import moe.styx.Styx__.BuildConfig +import moe.styx.Styx_2.BuildConfig +import moe.styx.common.compose.components.AppShapes import moe.styx.common.compose.components.buttons.IconButtonWithTooltip import moe.styx.common.compose.components.layout.MainScaffold import moe.styx.common.compose.components.misc.OnlineUsersIcon import moe.styx.common.compose.files.Storage +import moe.styx.common.compose.http.login import moe.styx.common.compose.utils.LocalGlobalNavigator +import moe.styx.common.compose.utils.LocalToaster +import moe.styx.common.compose.utils.ServerStatus +import moe.styx.common.compose.viewmodels.MainDataViewModel import moe.styx.common.data.Changes import moe.styx.logic.utils.pushMediaView +import moe.styx.logic.viewmodels.DesktopOverViewModel import moe.styx.views.* +import moe.styx.views.login.LoginView import moe.styx.views.other.FontSizeView -import moe.styx.views.other.LoadingView +import moe.styx.views.other.OutdatedView class AnimeOverview() : Screen { + @OptIn(ExperimentalFoundationApi::class) @Composable override fun Content() { + val toaster = LocalToaster.current + val overviewSm = rememberScreenModel { DesktopOverViewModel() } + val nav = LocalGlobalNavigator.current + if (overviewSm.isOutdated == true) { + nav.replaceAll(OutdatedView()) + } + + if (overviewSm.isLoggedIn == false && ServerStatus.lastKnown == ServerStatus.UNAUTHORIZED) { + nav.replaceAll(LoginView()) + } + + LaunchedEffect(overviewSm.availablePreRelease) { + val ver = overviewSm.availablePreRelease + if (!ver.isNullOrBlank()) { + toaster.show( + Toast( + "New Pre-Release version available: $ver", + action = TextToastAction("Download") { + nav.push(OutdatedView(ver)) + } + ) + ) + overviewSm.availablePreRelease = null + } + } + + val sm = nav.rememberNavigatorScreenModel("main-vm") { MainDataViewModel() } + val isLoading by sm.isLoadingStateFlow.collectAsState() + val loadingState by sm.loadingStateFlow.collectAsState() + + MainScaffold(title = BuildConfig.APP_NAME, addPopButton = false, addAnimatedTitleBackground = true, actions = { + if (isLoading) { + TooltipArea({ Text(loadingState) }, Modifier.fillMaxHeight(.9f), delayMillis = 200) { + Row(Modifier.fillMaxHeight(), verticalAlignment = Alignment.CenterVertically) { + LinearProgressIndicator( + trackColor = MaterialTheme.colorScheme.surfaceColorAtElevation(18.dp), + gapSize = 0.dp, + modifier = Modifier.requiredWidthIn(20.dp, 40.dp) + ) + } + } + } + if (overviewSm.isOffline == true) { + IconButtonWithTooltip(Icons.Filled.CloudOff, ServerStatus.getLastKnownText()) {} + } + + if (overviewSm.isLoggedIn == false) { + IconButtonWithTooltip(Icons.Filled.NoAccounts, "You are not logged in!\nClick to retry.") { + overviewSm.screenModelScope.launch { + val loginJob = overviewSm.runLoginAndChecks() + loginJob.join() + if (login != null) { + sm.updateData(updateStores = true) + } + } + } + } - MainScaffold(title = BuildConfig.APP_NAME, addPopButton = false, actions = { OnlineUsersIcon { nav.pushMediaView(it, false) } if (Main.wasLaunchedInDebug) IconButton(onClick = { nav.push(FontSizeView()) }, content = { Icon(Icons.Filled.QuestionMark, null) }) IconButtonWithTooltip(Icons.Filled.Refresh, "Reload") { runBlocking { Storage.stores.changesStore.set(Changes(0, 0)) } - nav.replaceAll(LoadingView()) + sm.updateData(forceUpdate = true, updateStores = true) } }) { TabNavigator(defaultTab) { @@ -51,7 +125,11 @@ class AnimeOverview() : Screen { @Composable private fun SideNavRail() { - NavigationRail(Modifier.fillMaxHeight().padding(0.dp, 0.dp, 5.dp, 0.dp)) { + NavigationRail( + Modifier.fillMaxHeight().padding(7.dp, 6.dp, 3.dp, 8.dp).shadow(2.dp, AppShapes.large).clip(AppShapes.large), + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp) + ) { + Spacer(Modifier.height(8.dp)) RailNavItem(defaultTab) RailNavItem(movieTab) RailNavItem(favsTab) @@ -66,6 +144,7 @@ class AnimeOverview() : Screen { indicatorColor = MaterialTheme.colorScheme.secondary ) ) + Spacer(Modifier.height(5.dp)) } } @@ -78,7 +157,7 @@ class AnimeOverview() : Screen { selected = tabNavigator.current.key == tab.key, onClick = { tabNavigator.current = tab }, icon = { Icon(painter = tab.options.icon!!, contentDescription = tab.options.title) }, - label = { Text(tab.options.title) }, + label = { Text(tab.options.title, modifier = Modifier.padding(3.dp, 1.dp)) }, alwaysShowLabel = true, colors = colors ?: NavigationRailItemDefaults.colors( diff --git a/src/main/kotlin/moe/styx/views/anime/MovieDetailView.kt b/src/main/kotlin/moe/styx/views/anime/MovieDetailView.kt index f0bf533..5832071 100644 --- a/src/main/kotlin/moe/styx/views/anime/MovieDetailView.kt +++ b/src/main/kotlin/moe/styx/views/anime/MovieDetailView.kt @@ -12,20 +12,28 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey +import com.dokar.sonner.TextToastAction +import com.dokar.sonner.Toast +import com.dokar.sonner.ToastType import com.russhwolf.settings.get import moe.styx.common.compose.components.anime.* import moe.styx.common.compose.components.buttons.FavouriteIconButton import moe.styx.common.compose.components.buttons.IconButtonWithTooltip import moe.styx.common.compose.components.layout.MainScaffold import moe.styx.common.compose.components.misc.OnlineUsersIcon -import moe.styx.common.compose.files.* +import moe.styx.common.compose.files.Storage +import moe.styx.common.compose.files.collectWithEmptyInitial +import moe.styx.common.compose.files.updateList import moe.styx.common.compose.http.login import moe.styx.common.compose.settings import moe.styx.common.compose.threads.DownloadQueue import moe.styx.common.compose.threads.RequestQueue import moe.styx.common.compose.utils.LocalGlobalNavigator +import moe.styx.common.compose.utils.LocalToaster +import moe.styx.common.compose.viewmodels.MainDataViewModel import moe.styx.common.data.MediaEntry import moe.styx.common.data.MediaWatched import moe.styx.common.extension.currentUnixSeconds @@ -34,7 +42,6 @@ import moe.styx.common.extension.toBoolean import moe.styx.common.util.SYSTEMFILES import moe.styx.common.util.launchThreaded import moe.styx.components.anime.AppendDialog -import moe.styx.components.anime.FailedDialog import moe.styx.logic.runner.currentPlayer import moe.styx.logic.runner.launchMPV import moe.styx.logic.utils.pushMediaView @@ -51,13 +58,17 @@ class MovieDetailView(private val mediaID: String) : Screen { @Composable override fun Content() { val nav = LocalGlobalNavigator.current - val mediaList by Storage.stores.mediaStore.getCurrentAndCollectFlow() - val media = remember { mediaList.find { it.GUID eqI mediaID } } - val movieEntry = fetchEntries(mediaID).minByOrNull { it.entryNumber.toDoubleOrNull() ?: 0.0 } - if (media == null) { + val toaster = LocalToaster.current + val sm = nav.rememberNavigatorScreenModel("main-vm") { MainDataViewModel() } + val storage by sm.storageFlow.collectAsState() + val mediaStorage = remember(storage) { sm.getMediaStorageForID(mediaID, storage) } + val movieEntry = mediaStorage.entries.getOrNull(0) + + if (mediaStorage.image == null) { nav.pop() return } + val watchedList by Storage.stores.watchedStore.collectWithEmptyInitial() val watched = movieEntry?.let { watchedList.find { it.entryID eqI movieEntry.GUID } } var showMediaInfoDialog by remember { mutableStateOf(false) } @@ -65,9 +76,9 @@ class MovieDetailView(private val mediaID: String) : Screen { MediaInfoDialog(movieEntry) { showMediaInfoDialog = false } } - MainScaffold(title = media.name, actions = { + MainScaffold(title = mediaStorage.media.name, actions = { OnlineUsersIcon { nav.replace(if (it.isSeries.toBoolean()) AnimeDetailView(it.GUID) else MovieDetailView(it.GUID)) } - FavouriteIconButton(media) + FavouriteIconButton(mediaStorage.media, sm, storage) }) { val scrollState = rememberScrollState() ElevatedCard( @@ -76,31 +87,37 @@ class MovieDetailView(private val mediaID: String) : Screen { ) { var failedToPlayMessage by remember { mutableStateOf("") } if (failedToPlayMessage.isNotBlank()) { - FailedDialog(failedToPlayMessage, Modifier.fillMaxWidth(0.6F)) { - failedToPlayMessage = "" - if (it) nav.push(SettingsView()) - } + toaster.show(Toast(failedToPlayMessage, type = ToastType.Error, action = TextToastAction("Open Settings") { + nav.push(SettingsView()) + })) + failedToPlayMessage = "" } var showAppendDialog by remember { mutableStateOf(false) } if (showAppendDialog && movieEntry != null) { AppendDialog(movieEntry, Modifier.fillMaxWidth(0.6F), Modifier.align(Alignment.CenterHorizontally), { showAppendDialog = false }) { - failedToPlayMessage = it + if (!it.isOK) + failedToPlayMessage = it.message + else + sm.updateData(true) } } Column(Modifier.fillMaxSize().verticalScroll(scrollState)) { - StupidImageNameArea(media) { + StupidImageNameArea(mediaStorage) { Column(Modifier.padding(6.dp).widthIn(0.dp, 560.dp).fillMaxWidth()) { Row(Modifier.padding(3.dp).fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { IconButton({ if (movieEntry == null) return@IconButton if (currentPlayer == null) { - launchMPV(movieEntry, false, { - failedToPlayMessage = it - }) + launchMPV(movieEntry, false) { + if (!it.isOK) + failedToPlayMessage = it.message + else + sm.updateData(true) + } } else { showAppendDialog = true } @@ -111,14 +128,22 @@ class MovieDetailView(private val mediaID: String) : Screen { IconButton(onClick = { movieEntry?.let { - RequestQueue.updateWatched( - MediaWatched(movieEntry.GUID, login?.userID ?: "", currentUnixSeconds(), 0, 0F, 100F) - ) + launchThreaded { + RequestQueue.updateWatched( + MediaWatched(movieEntry.GUID, login?.userID ?: "", currentUnixSeconds(), 0, 0F, 100F) + ).first.join() + sm.updateData(true) + } } }) { Icon(Icons.Default.Visibility, "Set Watched") } IconButton(onClick = { - movieEntry?.let { RequestQueue.removeWatched(movieEntry) } + movieEntry?.let { + launchThreaded { + RequestQueue.removeWatched(movieEntry).first.join() + sm.updateData(true) + } + } }) { Icon(Icons.Default.VisibilityOff, "Set Unwatched") } Spacer(Modifier.weight(1f)) @@ -134,17 +159,23 @@ class MovieDetailView(private val mediaID: String) : Screen { Spacer(Modifier.height(6.dp)) Text("About", Modifier.padding(6.dp, 2.dp), style = MaterialTheme.typography.titleLarge) - MediaGenreListing(media) + MediaGenreListing(mediaStorage.media) val preferGerman = settings["prefer-german-metadata", false] - val synopsis = if (!media.synopsisDE.isNullOrBlank() && preferGerman) media.synopsisDE else media.synopsisEN + val synopsis = + if (!mediaStorage.media.synopsisDE.isNullOrBlank() && preferGerman) mediaStorage.media.synopsisDE else mediaStorage.media.synopsisEN if (!synopsis.isNullOrBlank()) SelectionContainer { Text(synopsis.removeSomeHTMLTags(), Modifier.padding(6.dp), style = MaterialTheme.typography.bodyMedium) } - if (media.sequel != null || media.prequel != null) { + if (mediaStorage.sequel != null || mediaStorage.prequel != null) { HorizontalDivider(Modifier.fillMaxWidth().padding(8.dp, 6.dp), thickness = 2.dp) - MediaRelations(media, mediaList) { nav.pushMediaView(it, true) } + MediaRelations(mediaStorage) { + nav.pushMediaView( + it, + true + ) + } } } } diff --git a/src/main/kotlin/moe/styx/views/anime/tabs/AnimeListView.kt b/src/main/kotlin/moe/styx/views/anime/tabs/AnimeListView.kt deleted file mode 100644 index 327f19e..0000000 --- a/src/main/kotlin/moe/styx/views/anime/tabs/AnimeListView.kt +++ /dev/null @@ -1,33 +0,0 @@ -package moe.styx.views.anime.tabs - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Tv -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import com.russhwolf.settings.get -import kotlinx.coroutines.runBlocking -import moe.styx.common.compose.components.search.MediaSearch -import moe.styx.common.compose.extensions.SimpleTab -import moe.styx.common.compose.extensions.getDistinctCategories -import moe.styx.common.compose.extensions.getDistinctGenres -import moe.styx.common.compose.files.Storage -import moe.styx.common.compose.files.getCurrentAndCollectFlow -import moe.styx.common.compose.utils.SearchState -import moe.styx.common.extension.toBoolean -import moe.styx.views.barWithListComp - -class AnimeListView : SimpleTab("Shows", Icons.Default.Tv) { - - @Composable - override fun Content() { - val media by Storage.stores.mediaStore.getCurrentAndCollectFlow() - val categories by Storage.stores.categoryStore.getCurrentAndCollectFlow() - val searchStore = Storage.stores.showSearchState - val filtered = media.filter { it.isSeries.toBoolean() } - val availableGenres = filtered.getDistinctGenres() - val availableCategories = filtered.getDistinctCategories(categories) - val initialState = runBlocking { searchStore.get() ?: SearchState() } - val mediaSearch = MediaSearch(searchStore, initialState, availableGenres, availableCategories) - barWithListComp(mediaSearch, initialState, filtered, moe.styx.common.compose.settings["shows-list", false]) - } -} \ No newline at end of file diff --git a/src/main/kotlin/moe/styx/views/anime/tabs/FavouritesListView.kt b/src/main/kotlin/moe/styx/views/anime/tabs/FavouritesListView.kt deleted file mode 100644 index 3995cf4..0000000 --- a/src/main/kotlin/moe/styx/views/anime/tabs/FavouritesListView.kt +++ /dev/null @@ -1,31 +0,0 @@ -package moe.styx.views.anime.tabs - -import androidx.compose.foundation.layout.Column -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Star -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import kotlinx.coroutines.runBlocking -import moe.styx.common.compose.components.search.MediaSearch -import moe.styx.common.compose.extensions.SimpleTab -import moe.styx.common.compose.files.Storage -import moe.styx.common.compose.files.getCurrentAndCollectFlow -import moe.styx.common.compose.utils.SearchState -import moe.styx.common.extension.eqI -import moe.styx.views.barWithListComp - -class FavouritesListView : SimpleTab("Favourites", Icons.Default.Star) { - - @Composable - override fun Content() { - val media by Storage.stores.mediaStore.getCurrentAndCollectFlow() - Column { - val favourites by Storage.stores.favouriteStore.getCurrentAndCollectFlow() - val searchStore = Storage.stores.favSearchState - val filtered = media.filter { m -> favourites.find { m.GUID eqI it.mediaID } != null } - val initialState = runBlocking { searchStore.get() ?: SearchState() } - val mediaSearch = MediaSearch(searchStore, initialState, emptyList(), emptyList(), true) - barWithListComp(mediaSearch, initialState, filtered, false, true, favourites) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/moe/styx/views/anime/tabs/MediaListView.kt b/src/main/kotlin/moe/styx/views/anime/tabs/MediaListView.kt new file mode 100644 index 0000000..7837e83 --- /dev/null +++ b/src/main/kotlin/moe/styx/views/anime/tabs/MediaListView.kt @@ -0,0 +1,84 @@ +package moe.styx.views.anime.tabs + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Movie +import androidx.compose.material.icons.filled.Star +import androidx.compose.material.icons.filled.Tv +import androidx.compose.runtime.* +import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.navigator.tab.Tab +import cafe.adriel.voyager.navigator.tab.TabOptions +import com.russhwolf.settings.get +import kotlinx.coroutines.runBlocking +import moe.styx.common.compose.components.search.MediaSearch +import moe.styx.common.compose.extensions.createTabOptions +import moe.styx.common.compose.extensions.getDistinctCategories +import moe.styx.common.compose.extensions.getDistinctGenres +import moe.styx.common.compose.files.Stores +import moe.styx.common.compose.settings +import moe.styx.common.compose.utils.LocalGlobalNavigator +import moe.styx.common.compose.utils.SearchState +import moe.styx.common.compose.viewmodels.ListPosViewModel +import moe.styx.common.compose.viewmodels.MainDataViewModel +import moe.styx.common.extension.eqI +import moe.styx.common.extension.toBoolean +import moe.styx.views.barWithListComp + +class MediaListView(private val movies: Boolean = false, private val favourites: Boolean = false) : Tab { + override val options: TabOptions + @Composable + get() { + return if (favourites) + createTabOptions("Favourites", Icons.Default.Star) + else if (movies) + createTabOptions("Movies", Icons.Default.Movie) + else + createTabOptions("Shows", Icons.Default.Tv) + } + + override val key: ScreenKey + get() { + return if (favourites) + "favourites-view" + else if (movies) + "movies-view" + else + "shows-view" + } + + @Composable + override fun Content() { + val nav = LocalGlobalNavigator.current + val sm = nav.rememberNavigatorScreenModel("main-vm") { MainDataViewModel() } + val storage by sm.storageFlow.collectAsState() + val searchStore = if (favourites) Stores.favSearchState else if (movies) Stores.movieSearchState else Stores.showSearchState + + val key = if (favourites) "favourites" else if (movies) "movies" else "shows" + val useList = if (favourites) false else settings["$key-list", false] + val listPosModel = nav.rememberNavigatorScreenModel("$key-pos-$useList") { ListPosViewModel() } + + val filtered = remember(storage) { + storage.mediaList.let { mediaList -> + if (favourites) + mediaList.filter { m -> storage.favouritesList.find { it.mediaID eqI m.GUID } != null } + else + if (movies) mediaList.filter { !it.isSeries.toBoolean() } else mediaList.filter { it.isSeries.toBoolean() } + } + } + val availableGenres = remember(storage) { filtered.getDistinctGenres() } + val availableCategories = remember(storage) { filtered.getDistinctCategories(storage.categoryList) } + val initialState = runBlocking { searchStore.get() ?: SearchState() } + val mediaSearch = MediaSearch(searchStore, initialState, availableGenres, availableCategories, favourites) + barWithListComp( + mediaSearch, + initialState, + storage, + filtered, + useList, + listPosModel, + favourites, + if (favourites) storage.favouritesList else emptyList() + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/moe/styx/views/anime/tabs/MovieListView.kt b/src/main/kotlin/moe/styx/views/anime/tabs/MovieListView.kt deleted file mode 100644 index adba4c6..0000000 --- a/src/main/kotlin/moe/styx/views/anime/tabs/MovieListView.kt +++ /dev/null @@ -1,33 +0,0 @@ -package moe.styx.views.anime.tabs - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Movie -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import com.russhwolf.settings.get -import kotlinx.coroutines.runBlocking -import moe.styx.common.compose.components.search.MediaSearch -import moe.styx.common.compose.extensions.SimpleTab -import moe.styx.common.compose.extensions.getDistinctCategories -import moe.styx.common.compose.extensions.getDistinctGenres -import moe.styx.common.compose.files.Storage -import moe.styx.common.compose.files.getCurrentAndCollectFlow -import moe.styx.common.compose.utils.SearchState -import moe.styx.common.extension.toBoolean -import moe.styx.views.barWithListComp - -class MovieListView : SimpleTab("Movies", Icons.Default.Movie) { - - @Composable - override fun Content() { - val media by Storage.stores.mediaStore.getCurrentAndCollectFlow() - val categories by Storage.stores.categoryStore.getCurrentAndCollectFlow() - val searchStore = Storage.stores.movieSearchState - val filtered = media.filter { !it.isSeries.toBoolean() } - val availableGenres = filtered.getDistinctGenres() - val availableCategories = filtered.getDistinctCategories(categories) - val initialState = runBlocking { searchStore.get() ?: SearchState() } - val mediaSearch = MediaSearch(searchStore, initialState, availableGenres, availableCategories) - barWithListComp(mediaSearch, initialState, filtered, moe.styx.common.compose.settings["movies-list", false]) - } -} \ No newline at end of file diff --git a/src/main/kotlin/moe/styx/views/anime/tabs/ScheduleView.kt b/src/main/kotlin/moe/styx/views/anime/tabs/ScheduleView.kt index 6535019..e7b22be 100644 --- a/src/main/kotlin/moe/styx/views/anime/tabs/ScheduleView.kt +++ b/src/main/kotlin/moe/styx/views/anime/tabs/ScheduleView.kt @@ -1,14 +1,11 @@ package moe.styx.views.anime.tabs -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CalendarViewWeek import androidx.compose.runtime.Composable -import moe.styx.common.compose.components.schedule.ScheduleDay +import moe.styx.common.compose.components.schedule.ScheduleViewComponent import moe.styx.common.compose.extensions.SimpleTab import moe.styx.common.compose.utils.LocalGlobalNavigator -import moe.styx.common.data.ScheduleWeekday import moe.styx.logic.utils.pushMediaView class ScheduleView : SimpleTab("Schedule", Icons.Default.CalendarViewWeek) { @@ -16,11 +13,8 @@ class ScheduleView : SimpleTab("Schedule", Icons.Default.CalendarViewWeek) { @Composable override fun Content() { val nav = LocalGlobalNavigator.current - val days = ScheduleWeekday.entries.toTypedArray() - LazyColumn { - items(items = days, itemContent = { day -> - ScheduleDay(day) { nav.pushMediaView(it) } - }) + ScheduleViewComponent { + nav.pushMediaView(it, false) } } } \ No newline at end of file diff --git a/src/main/kotlin/moe/styx/views/login/LoginView.kt b/src/main/kotlin/moe/styx/views/login/LoginView.kt index e0866da..90e5f97 100644 --- a/src/main/kotlin/moe/styx/views/login/LoginView.kt +++ b/src/main/kotlin/moe/styx/views/login/LoginView.kt @@ -12,13 +12,13 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.screen.Screen import kotlinx.coroutines.delay -import moe.styx.Styx__.BuildConfig +import moe.styx.Styx_2.BuildConfig import moe.styx.common.compose.components.buttons.IconButtonWithTooltip import moe.styx.common.compose.http.checkLogin import moe.styx.common.compose.http.generateCode import moe.styx.common.compose.http.isLoggedIn import moe.styx.common.compose.utils.LocalGlobalNavigator -import moe.styx.views.other.LoadingView +import moe.styx.views.anime.AnimeOverview import java.awt.Desktop import java.awt.Toolkit import java.awt.datatransfer.StringSelection @@ -39,7 +39,7 @@ class LoginView() : Screen { return@let checkLogin(creationResponse!!.GUID, true) } if (log != null) { - nav.push(LoadingView()) + nav.replace(AnimeOverview()) break } diff --git a/src/main/kotlin/moe/styx/views/login/OfflineView.kt b/src/main/kotlin/moe/styx/views/login/OfflineView.kt index d870924..bca3c0c 100644 --- a/src/main/kotlin/moe/styx/views/login/OfflineView.kt +++ b/src/main/kotlin/moe/styx/views/login/OfflineView.kt @@ -12,7 +12,7 @@ import cafe.adriel.voyager.core.screen.Screen import moe.styx.common.compose.components.layout.MainScaffold import moe.styx.common.compose.utils.LocalGlobalNavigator import moe.styx.common.compose.utils.ServerStatus -import moe.styx.views.other.LoadingView +import moe.styx.views.anime.AnimeOverview class OfflineView : Screen { @@ -26,7 +26,7 @@ class OfflineView : Screen { Text(ServerStatus.getLastKnownText(), style = MaterialTheme.typography.headlineSmall) Text("Feel free to keep using Styx with the data you have from your last use.", Modifier.padding(0.dp, 15.dp).weight(1f)) - Button({ nav.replaceAll(LoadingView()) }) { + Button({ nav.replaceAll(AnimeOverview()) }) { Text("OK") } } diff --git a/src/main/kotlin/moe/styx/views/other/FontSizeView.kt b/src/main/kotlin/moe/styx/views/other/FontSizeView.kt index 0ae7925..33bdd69 100644 --- a/src/main/kotlin/moe/styx/views/other/FontSizeView.kt +++ b/src/main/kotlin/moe/styx/views/other/FontSizeView.kt @@ -1,10 +1,12 @@ package moe.styx.views.other -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.screen.Screen +import moe.styx.Main import moe.styx.common.compose.components.layout.MainScaffold class FontSizeView : Screen { @@ -13,6 +15,18 @@ class FontSizeView : Screen { override fun Content() { MainScaffold(title = "Font Sizes") { Column { + Row { + Button({ + Main.densityScale.value -= 0.25f + }) { + Text("Down the scale") + } + Button({ + Main.densityScale.value += 0.25f + }) { + Text("Up the scale") + } + } Text("Display Large", style = MaterialTheme.typography.displayLarge) Text("Display Medium", style = MaterialTheme.typography.displayMedium) Text("Display Small", style = MaterialTheme.typography.displaySmall) @@ -28,6 +42,42 @@ class FontSizeView : Screen { Text("Label Large", style = MaterialTheme.typography.labelLarge) Text("Label Medium", style = MaterialTheme.typography.labelMedium) Text("Label Small", style = MaterialTheme.typography.labelSmall) + HorizontalDivider() + LinearProgressIndicator( + { .5f }, + Modifier.padding(5.dp).fillMaxWidth().height(5.dp), + trackColor = MaterialTheme.colorScheme.onSurface + ) + LinearProgressIndicator( + { .5f }, + Modifier.padding(5.dp).fillMaxWidth().height(5.dp), + trackColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + LinearProgressIndicator( + { .5f }, + Modifier.padding(5.dp).fillMaxWidth().height(5.dp), + trackColor = MaterialTheme.colorScheme.inverseOnSurface + ) + LinearProgressIndicator( + { .5f }, + Modifier.padding(5.dp).fillMaxWidth().height(5.dp), + trackColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp) + ) + LinearProgressIndicator( + { .5f }, + Modifier.padding(5.dp).fillMaxWidth().height(5.dp), + trackColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + ) + LinearProgressIndicator( + { .5f }, + Modifier.padding(5.dp).fillMaxWidth().height(5.dp), + trackColor = MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp) + ) + LinearProgressIndicator( + { .5f }, + Modifier.padding(5.dp).fillMaxWidth().height(5.dp), + trackColor = MaterialTheme.colorScheme.surfaceColorAtElevation(15.dp) + ) } } } diff --git a/src/main/kotlin/moe/styx/views/other/LoadingView.kt b/src/main/kotlin/moe/styx/views/other/LoadingView.kt deleted file mode 100644 index 234a6c2..0000000 --- a/src/main/kotlin/moe/styx/views/other/LoadingView.kt +++ /dev/null @@ -1,88 +0,0 @@ -package moe.styx.views.other - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.screen.Screen -import kotlinx.coroutines.delay -import moe.styx.Styx__.BuildConfig -import moe.styx.common.compose.components.layout.MainScaffold -import moe.styx.common.compose.files.Storage -import moe.styx.common.compose.utils.LocalGlobalNavigator -import moe.styx.common.compose.utils.ServerStatus -import moe.styx.common.isWindows -import moe.styx.logic.utils.MpvUtils -import moe.styx.logic.utils.downloadNewInstaller -import moe.styx.logic.utils.isUpToDate -import moe.styx.views.anime.AnimeOverview -import java.awt.Desktop -import java.net.URI - -class LoadingView : Screen { - - @Composable - override fun Content() { - val nav = LocalGlobalNavigator.current - val progress = Storage.loadingProgress.collectAsState() - - if (ServerStatus.lastKnown != ServerStatus.UNKNOWN && !isUpToDate()) { - if (isWindows) - downloadNewInstaller() - OutdatedVersion() - return - } - - LaunchedEffect(Unit) { - delay(1000) - Storage.loadData() - MpvUtils.checkVersionAndDownload() - nav.replaceAll(AnimeOverview()) - } - - MainScaffold(title = "Loading", addPopButton = false) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Column(modifier = Modifier.padding(10.dp).align(Alignment.TopCenter)) { - Text( - text = progress.value, - modifier = Modifier.align(Alignment.CenterHorizontally).padding(0.dp, 25.dp) - ) - CircularProgressIndicator( - modifier = Modifier.align(Alignment.CenterHorizontally).padding(0.dp, 15.dp).fillMaxSize(.4F) - ) - } - } - } - } -} - -@Composable -fun OutdatedVersion() { - MainScaffold(title = "Outdated", addPopButton = false) { - Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) { - var modifier = Modifier.padding(10.dp) - if (!isWindows) - modifier = modifier.weight(1f) - Text( - "This version of Styx is outdated.", - modifier, - style = MaterialTheme.typography.headlineMedium - ) - if (isWindows) - Text( - "Attempting to download the new version automatically. Please wait a bit. If nothing happens, feel free to do it manually below.", - Modifier.weight(1f).padding(16.dp) - ) - Button({ - if (Desktop.isDesktopSupported()) - Desktop.getDesktop().browse(URI(BuildConfig.SITE_URL)) - }) { - Text("Open ${BuildConfig.SITE}") - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/moe/styx/views/other/OutdatedView.kt b/src/main/kotlin/moe/styx/views/other/OutdatedView.kt new file mode 100644 index 0000000..252eaa5 --- /dev/null +++ b/src/main/kotlin/moe/styx/views/other/OutdatedView.kt @@ -0,0 +1,115 @@ +package moe.styx.views.other + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.screen.Screen +import com.dokar.sonner.* +import kotlinx.coroutines.delay +import moe.styx.Styx_2.BuildConfig +import moe.styx.common.compose.components.layout.MainScaffold +import moe.styx.common.compose.http.Endpoints +import moe.styx.common.compose.http.login +import moe.styx.common.compose.utils.LocalToaster +import moe.styx.common.http.DownloadResult +import moe.styx.common.http.downloadFileStream +import moe.styx.common.isWindows +import moe.styx.common.util.launchThreaded +import moe.styx.logic.Files +import moe.styx.logic.runner.openURI +import okio.Path.Companion.toPath +import java.awt.Desktop +import java.io.File +import kotlin.system.exitProcess + +class OutdatedView(private val requestedVersion: String? = null) : Screen { + + @Composable + override fun Content() { + val shouldBeDownloading = remember { mutableStateOf(false) } + + MainScaffold(title = "Outdated", addPopButton = requestedVersion != null) { + Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) { + Text( + if (requestedVersion == null) "This version of Styx is outdated." else "Download $requestedVersion", + Modifier.padding(10.dp).weight(1f), + style = MaterialTheme.typography.headlineMedium + ) + DownloadButtons(shouldBeDownloading) + Button({ + openURI("${BuildConfig.SITE_URL}/user") + }, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary), modifier = Modifier.padding(10.dp)) { + Text("Open ${BuildConfig.SITE}") + } + } + } + } + + + @Suppress("NAME_SHADOWING") + @Composable + fun ColumnScope.DownloadButtons(shouldBeDownloading: MutableState) { + val toaster = LocalToaster.current + var shouldBeDownloading by shouldBeDownloading + Row(Modifier.weight(1f)) { + if (isWindows) { + Button({ + shouldBeDownloading = true + runDownload("win", toaster) { shouldBeDownloading = false } + }, enabled = !shouldBeDownloading) { + Text("Download and open installer") + } + } else { + Button({ + shouldBeDownloading = true + runDownload("rpm", toaster) { shouldBeDownloading = false } + }, enabled = !shouldBeDownloading, modifier = Modifier.padding(12.dp)) { + Text("RPM") + } + Button({ + shouldBeDownloading = true + runDownload("deb", toaster) { shouldBeDownloading = false } + }, enabled = !shouldBeDownloading, modifier = Modifier.padding(12.dp)) { + Text("DEB") + } + } + } + } + + private fun runDownload(platform: String, toaster: ToasterState, onDone: () -> Unit) = launchThreaded { + val outFile = File(Files.getDataDir().parentFile, "Installer." + if (isWindows) "msi" else platform) + val result = downloadFileStream( + Endpoints.DOWNLOAD_BUILD_BASE.url() + "/$platform" + (if (requestedVersion != null) "/$requestedVersion" else "") + "?token=${login?.accessToken}", + outFile.absolutePath.toPath() + ) + if (result !in arrayOf(DownloadResult.OK, DownloadResult.AbortExists)) { + toaster.show( + Toast( + "Failed to download installer! Please check the logs.", + type = ToastType.Error, + duration = ToasterDefaults.DurationLong + ) + ) + onDone() + return@launchThreaded + } + onDone() + if (isWindows) { + if (Desktop.isDesktopSupported() && outFile.exists() && outFile.length() > 100) { + delay(1500L) + Desktop.getDesktop().open(outFile.parentFile) + delay(500L) + Desktop.getDesktop().open(outFile) + delay(1500L) + exitProcess(0) + } + } else { + openURI(outFile.parentFile.absolutePath) + delay(1500L) + exitProcess(0) + } + } +} diff --git a/src/main/kotlin/moe/styx/views/settings/MpvConfigView.kt b/src/main/kotlin/moe/styx/views/settings/MpvConfigView.kt index 47f5d1f..5426646 100644 --- a/src/main/kotlin/moe/styx/views/settings/MpvConfigView.kt +++ b/src/main/kotlin/moe/styx/views/settings/MpvConfigView.kt @@ -2,6 +2,7 @@ package moe.styx.views.settings import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* import androidx.compose.runtime.* @@ -12,21 +13,49 @@ import com.russhwolf.settings.get import com.russhwolf.settings.set import kotlinx.serialization.encodeToString import moe.styx.common.compose.components.layout.MainScaffold +import moe.styx.common.compose.components.misc.ExpandableSettings import moe.styx.common.compose.components.misc.Toggles import moe.styx.common.compose.components.misc.Toggles.settingsContainer import moe.styx.common.compose.settings import moe.styx.common.compose.utils.* import moe.styx.common.isWindows import moe.styx.common.json +import moe.styx.logic.Files import moe.styx.logic.utils.generateNewConfig +import java.io.File class MpvConfigView : Screen { @Composable override fun Content() { var preferences by remember { mutableStateOf(MpvPreferences.getOrDefault()) } + var tipsExpanded by remember { mutableStateOf(false) } MainScaffold(title = "Mpv Configuration") { Column { Column(Modifier.padding(8.dp).fillMaxWidth().weight(1f).verticalScroll(rememberScrollState())) { + ExpandableSettings("MPV Tips and tricks", tipsExpanded, { tipsExpanded = !tipsExpanded }) { + SelectionContainer { + Text( + """ + Here are some possibly useful keybinds: + + CTRL+R Attempts to reload the video, may be useful if the API is having issues and stuff starts buffering. + + SHIFT+C Tries to Auto-Crop the video. Useful if the actual content is 21:9 but has black bars in the file itself. + + SHIFT+W Opens the Recording/Clip-Maker Menu. These clips are just dumped in your user folder. + + H Toggle debanding on the fly. + + You can also step frame by frame with SHIFT + Arrow Keys. + + You can also create a custom config that will be persisted through mpv updates by creating a file at: + ${File(Files.getAppDir(), "custom-mpv.conf").absolutePath} + """.trimIndent(), + modifier = Modifier.padding(8.dp, 4.dp), + style = MaterialTheme.typography.bodyMedium + ) + } + } Column(Modifier.settingsContainer()) { Text("General", Modifier.padding(10.dp, 7.dp), style = MaterialTheme.typography.titleLarge) Toggles.ContainerSwitch( diff --git a/src/main/kotlin/moe/styx/views/settings/SettingsTab.kt b/src/main/kotlin/moe/styx/views/settings/SettingsTab.kt new file mode 100644 index 0000000..e7fad2e --- /dev/null +++ b/src/main/kotlin/moe/styx/views/settings/SettingsTab.kt @@ -0,0 +1,148 @@ +package moe.styx.views.settings + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.Navigator +import com.dokar.sonner.Toast +import com.dokar.sonner.ToastType +import com.dokar.sonner.ToasterDefaults +import com.russhwolf.settings.get +import com.russhwolf.settings.set +import moe.styx.common.compose.components.layout.MainScaffold +import moe.styx.common.compose.components.misc.ExpandableSettings +import moe.styx.common.compose.components.misc.ServerSelection +import moe.styx.common.compose.components.misc.Toggles +import moe.styx.common.compose.components.misc.Toggles.settingsContainer +import moe.styx.common.compose.extensions.SimpleTab +import moe.styx.common.compose.http.login +import moe.styx.common.compose.settings +import moe.styx.common.compose.utils.LocalGlobalNavigator +import moe.styx.common.compose.utils.LocalToaster +import moe.styx.common.isWindows +import moe.styx.common.util.Log +import moe.styx.components.misc.MpvVersionAndDownload +import moe.styx.logic.Files +import moe.styx.logic.runner.openURI +import moe.styx.views.settings.sub.AppearanceSettings +import moe.styx.views.settings.sub.DiscordSettings +import moe.styx.views.settings.sub.MetadataSettings +import java.awt.Desktop +import java.io.File + +class SettingsTab : SimpleTab("Settings", Icons.Default.Settings) { + + @Composable + override fun Content() { + SettingsViewComponent() + } +} + +class SettingsView : Screen { + + @Composable + override fun Content() { + MainScaffold(Modifier.fillMaxSize(), "Settings") { + SettingsViewComponent() + } + } +} + +class SettingsViewModel : ScreenModel { + var appearanceExpanded by mutableStateOf(true) + var metadataExpanded by mutableStateOf(true) + var discordExpanded by mutableStateOf(false) + var systemExpanded by mutableStateOf(false) +} + +@Composable +fun SettingsViewComponent() { + val nav = LocalGlobalNavigator.current + val vm = nav.rememberNavigatorScreenModel("settings-vm") { SettingsViewModel() } + val scrollState = rememberScrollState(0) + val toaster = LocalToaster.current + Column(Modifier.fillMaxSize().padding(5.dp)) { + Column(Modifier.padding(5.dp).weight(1F).verticalScroll(scrollState, true)) { + ExpandableSettings( + "Appearance Options", + vm.appearanceExpanded, + { vm.appearanceExpanded = !vm.appearanceExpanded }, + withContainer = false + ) { + AppearanceSettings() + } + ExpandableSettings("Metadata Options", vm.metadataExpanded, { vm.metadataExpanded = !vm.metadataExpanded }, withContainer = false) { + MetadataSettings() + } + ExpandableSettings("Discord", vm.discordExpanded, { vm.discordExpanded = !vm.discordExpanded }, withContainer = false) { + DiscordSettings() + } + ExpandableSettings("System", vm.systemExpanded, { vm.systemExpanded = !vm.systemExpanded }, withContainer = false) { + ServerSelection() + Toggles.ContainerSwitch( + "Enable Debug Logs", + description = "Please enable this and try to reproduce your issue if you want to report a bug to me!", + value = settings["enable-debug-logs", false] + ) { + settings["enable-debug-logs"] = it + Log.debugEnabled = it + } + Button({ + val logFolder = File(Files.getAppDir(), "Logs") + if (!isWindows) { + openURI(logFolder.absolutePath) + return@Button + } + runCatching { + if (Desktop.isDesktopSupported()) { + Desktop.getDesktop().open(logFolder) + } + return@Button + }.onFailure { + Log.e(exception = it) + }.getOrNull() + toaster.show( + Toast( + "Could not open log folder!\nPlease check it yourself at: ${logFolder.absolutePath}", + type = ToastType.Error, + duration = ToasterDefaults.DurationLong + ) + ) + }, Modifier.padding(8.dp, 5.dp)) { + Text("Open Log Folder") + } + Spacer(Modifier.height(5.dp)) + } + Column(Modifier.settingsContainer()) { + Text("MPV Options", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(10.dp, 7.dp)) + Button({ nav.push(MpvConfigView()) }, Modifier.padding(8.dp, 4.dp)) { + Text("Open Mpv Configuration") + } + MpvVersionAndDownload() + } + } + HorizontalDivider(Modifier.padding(5.dp), thickness = 2.dp) + LoggedInComponent(nav) + } +} + + +@Composable +fun LoggedInComponent(nav: Navigator) { + val primaryColor = MaterialTheme.colorScheme.primary + if (login != null) { + Text( + "Logged in as: ${login!!.name}", + Modifier.padding(10.dp) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/moe/styx/views/settings/SettingsView.kt b/src/main/kotlin/moe/styx/views/settings/SettingsView.kt deleted file mode 100644 index e8e7a5b..0000000 --- a/src/main/kotlin/moe/styx/views/settings/SettingsView.kt +++ /dev/null @@ -1,146 +0,0 @@ -package moe.styx.views.settings - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import cafe.adriel.voyager.navigator.Navigator -import com.russhwolf.settings.get -import com.russhwolf.settings.set -import moe.styx.Main.isUiModeDark -import moe.styx.common.compose.components.misc.Toggles -import moe.styx.common.compose.components.misc.Toggles.settingsContainer -import moe.styx.common.compose.extensions.SimpleTab -import moe.styx.common.compose.http.isLoggedIn -import moe.styx.common.compose.http.login -import moe.styx.common.compose.settings -import moe.styx.common.compose.utils.LocalGlobalNavigator -import moe.styx.common.compose.utils.ServerStatus -import moe.styx.components.misc.MpvVersionAndDownload -import moe.styx.logic.DiscordRPC -import moe.styx.views.login.LoginView -import moe.styx.views.login.OfflineView -import moe.styx.views.other.LoadingView - -class SettingsView : SimpleTab("Settings", Icons.Default.Settings) { - - @Composable - override fun Content() { - val nav = LocalGlobalNavigator.current - var darkMode by remember { isUiModeDark } - val scrollState = rememberScrollState(0) - Column(Modifier.fillMaxSize().padding(5.dp)) { - Column(Modifier.padding(5.dp).weight(1F).verticalScroll(scrollState, true)) { - Column(Modifier.settingsContainer()) { - Text("Layout Options", modifier = Modifier.padding(10.dp, 7.dp), style = MaterialTheme.typography.titleLarge) - Toggles.ContainerSwitch("Darkmode", value = darkMode) { darkMode = it } - Toggles.ContainerSwitch("Show names by default", value = settings["display-names", false]) { settings["display-names"] = it } - Toggles.ContainerSwitch( - "Show episode summaries", - value = settings["display-ep-synopsis", false] - ) { settings["display-ep-synopsis"] = it } - Toggles.ContainerSwitch( - "Prefer german titles and summaries", - value = settings["prefer-german-metadata", false] - ) { settings["prefer-german-metadata"] = it } - Row(Modifier.fillMaxWidth()) { - Toggles.ContainerSwitch( - "Use list for shows", - modifier = Modifier.weight(1f), - value = settings["shows-list", false], - paddingValues = Toggles.rowStartPadding - ) { settings["shows-list"] = it } - Toggles.ContainerSwitch( - "Use list for movies", - modifier = Modifier.weight(1f), - value = settings["movies-list", false], - paddingValues = Toggles.rowEndPadding - ) { settings["movies-list"] = it } - } - Toggles.ContainerSwitch( - "Sort episodes ascendingly", - value = settings["episode-asc", false], - paddingValues = Toggles.colEndPadding - ) { settings["episode-asc"] = it } - } - Column(Modifier.settingsContainer()) { - Text("Discord", modifier = Modifier.padding(10.dp, 7.dp), style = MaterialTheme.typography.titleLarge) - - Row(Modifier.fillMaxWidth().height(IntrinsicSize.Max)) { - Toggles.ContainerSwitch( - "Enable RPC", - modifier = Modifier.weight(1f).fillMaxHeight(), - value = settings["discord-rpc", true], - paddingValues = Toggles.rowStartPadding - ) { - settings["discord-rpc"] = it - if (it && !DiscordRPC.isStarted()) - DiscordRPC.start() - else if (!it && DiscordRPC.isStarted()) { - DiscordRPC.clearActivity() - } - } - - Toggles.ContainerSwitch( - "Show RPC when idle", - "Disabling this means the discord status will only show while you're watching something.", - value = settings["discord-rpc-idle", true], modifier = Modifier.weight(1f), paddingValues = Toggles.rowEndPadding - ) { settings["discord-rpc-idle"] = it } - } - Spacer(Modifier.height(5.dp)) - } - Column(Modifier.settingsContainer()) { - Text("MPV Options", style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(10.dp, 7.dp)) - Button({ nav.push(MpvConfigView()) }, Modifier.padding(8.dp, 4.dp)) { - Text("Open Mpv Configuration") - } - MpvVersionAndDownload() - } - } - HorizontalDivider(Modifier.padding(5.dp), thickness = 2.dp) - LoggedInComponent(nav) - } - } -} - - -@Composable -fun LoggedInComponent(nav: Navigator) { - val primaryColor = MaterialTheme.colorScheme.primary - if (login != null) { - Text( - "Logged in as: ${login!!.name}", - Modifier.padding(10.dp) - ) - } else { - Text("You're not logged in right now.", Modifier.padding(10.dp).drawBehind { - val strokeWidthPx = 1.dp.toPx() - val verticalOffset = size.height - 1.sp.toPx() - drawLine( - color = primaryColor, - strokeWidth = strokeWidthPx, - start = Offset(0f, verticalOffset), - end = Offset(size.width, verticalOffset) - ) - }.clickable { - val view = if (isLoggedIn()) - LoadingView() - else { - if (ServerStatus.lastKnown !in listOf(ServerStatus.ONLINE, ServerStatus.UNAUTHORIZED)) - OfflineView() - else - LoginView() - } - nav.replaceAll(view) - }) - } -} \ No newline at end of file diff --git a/src/main/kotlin/moe/styx/views/settings/sub/AppearanceSettings.kt b/src/main/kotlin/moe/styx/views/settings/sub/AppearanceSettings.kt new file mode 100644 index 0000000..3498165 --- /dev/null +++ b/src/main/kotlin/moe/styx/views/settings/sub/AppearanceSettings.kt @@ -0,0 +1,112 @@ +package moe.styx.views.settings.sub + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Remove +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.russhwolf.settings.get +import com.russhwolf.settings.set +import moe.styx.Main.densityScale +import moe.styx.Main.isUiModeDark +import moe.styx.Main.useMonoFont +import moe.styx.common.compose.components.AppShapes +import moe.styx.common.compose.components.buttons.IconButtonWithTooltip +import moe.styx.common.compose.components.misc.Toggles +import moe.styx.common.compose.settings + +@Composable +fun AppearanceSettings() { + var darkMode by remember { isUiModeDark } + var monoFont by remember { useMonoFont } + + Text( + "Note that these may not fully apply until you go in and out of an anime screen.\nThis is a bug in a 3rd party library and will be fixed in a later update.", + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(10.dp, 5.dp) + ) + + Row(Modifier.fillMaxWidth()) { + Toggles.ContainerSwitch( + "Darkmode", + modifier = Modifier.weight(1f), + value = darkMode, + paddingValues = Toggles.rowStartPadding + ) { darkMode = it.also { settings["darkmode"] = it } } + + Toggles.ContainerSwitch( + "Mono Font", + modifier = Modifier.weight(1f), + value = monoFont, + paddingValues = Toggles.rowEndPadding + ) { monoFont = it.also { settings["mono-font"] = it } } + } + WindowScaleSetting() + Toggles.ContainerSwitch("Show names by default", value = settings["display-names", false]) { settings["display-names"] = it } + Spacer(Modifier.height(5.dp)) +} + +@Composable +fun WindowScaleSetting(modifier: Modifier = Modifier) { + var densityScale by remember { densityScale } + Row( + modifier.padding(8.dp, 4.dp).clip(AppShapes.large).background(MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)), + verticalAlignment = Alignment.CenterVertically + ) { + Column(Modifier.padding(10.dp).weight(1f), horizontalAlignment = Alignment.Start) { + Text("Window Scale: $densityScale", style = MaterialTheme.typography.bodyLarge) + } + Row(Modifier.padding(10.dp), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically) { + IconButtonWithTooltip(Icons.Outlined.Remove, "Decrease Scale") { + val newScale = densityScale - 0.25f + settings["density-scale"] = newScale + densityScale = newScale + } + IconButtonWithTooltip(Icons.Outlined.Add, "Increase Scale") { + val newScale = densityScale + 0.25f + settings["density-scale"] = newScale + densityScale = newScale + } + } + } +} + +@Composable +fun MetadataSettings() { + Toggles.ContainerSwitch( + "Show episode summaries", + value = settings["display-ep-synopsis", false] + ) { settings["display-ep-synopsis"] = it } + Toggles.ContainerSwitch( + "Prefer german titles and summaries", + value = settings["prefer-german-metadata", false] + ) { settings["prefer-german-metadata"] = it } + Row(Modifier.fillMaxWidth()) { + Toggles.ContainerSwitch( + "Use list for shows", + modifier = Modifier.weight(1f), + value = settings["shows-list", false], + paddingValues = Toggles.rowStartPadding + ) { settings["shows-list"] = it } + Toggles.ContainerSwitch( + "Use list for movies", + modifier = Modifier.weight(1f), + value = settings["movies-list", false], + paddingValues = Toggles.rowEndPadding + ) { settings["movies-list"] = it } + } + Toggles.ContainerSwitch( + "Sort episodes ascendingly", + value = settings["episode-asc", false], + paddingValues = Toggles.colEndPadding + ) { settings["episode-asc"] = it } + Spacer(Modifier.height(5.dp)) +} \ No newline at end of file diff --git a/src/main/kotlin/moe/styx/views/settings/sub/DiscordSettings.kt b/src/main/kotlin/moe/styx/views/settings/sub/DiscordSettings.kt new file mode 100644 index 0000000..aeb1894 --- /dev/null +++ b/src/main/kotlin/moe/styx/views/settings/sub/DiscordSettings.kt @@ -0,0 +1,37 @@ +package moe.styx.views.settings.sub + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.russhwolf.settings.get +import com.russhwolf.settings.set +import moe.styx.common.compose.components.misc.Toggles +import moe.styx.common.compose.settings +import moe.styx.logic.DiscordRPC + +@Composable +fun DiscordSettings(modifier: Modifier = Modifier) { + Row(Modifier.fillMaxWidth().height(IntrinsicSize.Max)) { + Toggles.ContainerSwitch( + "Enable RPC", + modifier = Modifier.weight(1f).fillMaxHeight(), + value = settings["discord-rpc", true], + paddingValues = Toggles.rowStartPadding + ) { + settings["discord-rpc"] = it + if (it && !DiscordRPC.isStarted()) + DiscordRPC.start() + else if (!it && DiscordRPC.isStarted()) { + DiscordRPC.clearActivity() + } + } + + Toggles.ContainerSwitch( + "Show RPC when idle", + "Disabling this means the discord status will only show while you're watching something.", + value = settings["discord-rpc-idle", true], modifier = Modifier.weight(1f), paddingValues = Toggles.rowEndPadding + ) { settings["discord-rpc-idle"] = it } + } + Spacer(Modifier.height(5.dp)) +} \ No newline at end of file diff --git a/src/main/resources/fonts/JetBrainsMono-Bold.ttf b/src/main/resources/fonts/JetBrainsMono-Bold.ttf new file mode 100644 index 0000000..1926c80 Binary files /dev/null and b/src/main/resources/fonts/JetBrainsMono-Bold.ttf differ diff --git a/src/main/resources/fonts/JetBrainsMono-BoldItalic.ttf b/src/main/resources/fonts/JetBrainsMono-BoldItalic.ttf new file mode 100644 index 0000000..a447751 Binary files /dev/null and b/src/main/resources/fonts/JetBrainsMono-BoldItalic.ttf differ diff --git a/src/main/resources/fonts/JetBrainsMono-Italic.ttf b/src/main/resources/fonts/JetBrainsMono-Italic.ttf new file mode 100644 index 0000000..8cf794a Binary files /dev/null and b/src/main/resources/fonts/JetBrainsMono-Italic.ttf differ diff --git a/src/main/resources/fonts/JetBrainsMono-Light.ttf b/src/main/resources/fonts/JetBrainsMono-Light.ttf new file mode 100644 index 0000000..9d5d8a5 Binary files /dev/null and b/src/main/resources/fonts/JetBrainsMono-Light.ttf differ diff --git a/src/main/resources/fonts/JetBrainsMono-LightItalic.ttf b/src/main/resources/fonts/JetBrainsMono-LightItalic.ttf new file mode 100644 index 0000000..4c91d3e Binary files /dev/null and b/src/main/resources/fonts/JetBrainsMono-LightItalic.ttf differ diff --git a/src/main/resources/fonts/JetBrainsMono-Medium.ttf b/src/main/resources/fonts/JetBrainsMono-Medium.ttf new file mode 100644 index 0000000..ad71d92 Binary files /dev/null and b/src/main/resources/fonts/JetBrainsMono-Medium.ttf differ diff --git a/src/main/resources/fonts/JetBrainsMono-MediumItalic.ttf b/src/main/resources/fonts/JetBrainsMono-MediumItalic.ttf new file mode 100644 index 0000000..4c96cc5 Binary files /dev/null and b/src/main/resources/fonts/JetBrainsMono-MediumItalic.ttf differ diff --git a/src/main/resources/fonts/JetBrainsMono-Regular.ttf b/src/main/resources/fonts/JetBrainsMono-Regular.ttf new file mode 100644 index 0000000..436c982 Binary files /dev/null and b/src/main/resources/fonts/JetBrainsMono-Regular.ttf differ diff --git a/src/main/resources/fonts/JetBrainsMono-SemiBold.ttf b/src/main/resources/fonts/JetBrainsMono-SemiBold.ttf new file mode 100644 index 0000000..b00a648 Binary files /dev/null and b/src/main/resources/fonts/JetBrainsMono-SemiBold.ttf differ diff --git a/src/main/resources/fonts/JetBrainsMono-SemiBoldItalic.ttf b/src/main/resources/fonts/JetBrainsMono-SemiBoldItalic.ttf new file mode 100644 index 0000000..5b6c9a8 Binary files /dev/null and b/src/main/resources/fonts/JetBrainsMono-SemiBoldItalic.ttf differ diff --git a/src/main/resources/fonts/OpenSans-ExtraBold.ttf b/src/main/resources/fonts/OpenSans-ExtraBold.ttf deleted file mode 100644 index 4eb3393..0000000 Binary files a/src/main/resources/fonts/OpenSans-ExtraBold.ttf and /dev/null differ diff --git a/src/main/resources/fonts/OpenSans-ExtraBoldItalic.ttf b/src/main/resources/fonts/OpenSans-ExtraBoldItalic.ttf deleted file mode 100644 index 75789b4..0000000 Binary files a/src/main/resources/fonts/OpenSans-ExtraBoldItalic.ttf and /dev/null differ diff --git a/src/main/resources/fonts/OpenSans-SemiBold.ttf b/src/main/resources/fonts/OpenSans-SemiBold.ttf deleted file mode 100644 index e5ab464..0000000 Binary files a/src/main/resources/fonts/OpenSans-SemiBold.ttf and /dev/null differ diff --git a/src/main/resources/fonts/OpenSans-SemiBoldItalic.ttf b/src/main/resources/fonts/OpenSans-SemiBoldItalic.ttf deleted file mode 100644 index cd23e15..0000000 Binary files a/src/main/resources/fonts/OpenSans-SemiBoldItalic.ttf and /dev/null differ