diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 52001fd..ae813b6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,15 +13,15 @@ jobs:
- name: Checkout the code
uses: actions/checkout@v4
- name: Set up a specific Java version
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: "temurin" # OR adopt OR microsoft OR...
- java-version: "21"
+ java-version: "17"
- name: Run all unit tests
run: ./gradlew test --stacktrace
- name: Upload test reports
if: always() # Run even if the previous steps failed
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: tests-report
path: build/reports/tests/
diff --git a/.idea/runConfigurations/Tests__All_.xml b/.idea/runConfigurations/Tests__All_.xml
index 93f7fcd..c173dd9 100644
--- a/.idea/runConfigurations/Tests__All_.xml
+++ b/.idea/runConfigurations/Tests__All_.xml
@@ -17,8 +17,7 @@
@@ -29,4 +28,4 @@
false
-
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index b5d6fbb..60a414c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -18,8 +18,8 @@ plugins {
val appRawFilesPath = rootDir.toPath() / "raw"
val appResourcesPath = rootDir.toPath() / "asset"
val vlcDirectoryName = "vlc"
-val isVlcFull = System.getenv("fullVlc").toBooleanLenient() ?: false
-val shouldMinifyVlc = System.getenv("minifyVlc").toBooleanLenient() ?: true
+val isVlcFull = System.getenv("fullVlc").toBooleanLenient() == true
+val shouldMinifyVlc = System.getenv("minifyVlc").toBooleanLenient() == true
val releaseDate: LocalDate = LocalDate.of(2024, 7, 6)
group = "ir.mahozad"
@@ -36,7 +36,7 @@ sourceSets {
// Makes the uiTestImplementation configuration extend from testImplementation,
// which means that all the declared dependencies of the test code (and transitively the main as well)
// also become dependencies of this new configuration
-val uiTestImplementation by configurations.getting {
+val uiTestImplementation: Configuration by configurations.getting {
extendsFrom(configurations.testImplementation.get())
}
val uiTest = task("uiTest") {
@@ -160,6 +160,8 @@ tasks.register(
"plugins/access/libfilesystem_plugin.dll",
// Along with audio_output/libmmdevice_plugin.dll normalizes audio loudness
"plugins/audio_filter/libnormvol_plugin.dll",
+ // Needed for MP3 (or audio) files with single (mono) channel
+ "plugins/audio_filter/libtrivial_channel_mixer_plugin.dll",
"plugins/audio_filter/libscaletempo_pitch_plugin.dll",
"plugins/audio_filter/libscaletempo_plugin.dll",
"plugins/audio_output/libdirectsound_plugin.dll",
@@ -209,13 +211,13 @@ tasks.register(
*/
buildConfig {
packageName("${project.group}.${project.name.lowercase()}")
- buildConfigField("String", "APP_NAME", """"${project.name}"""")
- buildConfigField("String", "APP_VERSION", """"${project.version}"""")
- buildConfigField("String", "VLC_DIRECTORY_NAME", """"$vlcDirectoryName"""")
+ buildConfigField(name = "APP_NAME", value = project.name)
+ buildConfigField(name = "APP_VERSION", value = "${project.version}")
+ buildConfigField(name = "VLC_DIRECTORY_NAME", value = vlcDirectoryName)
buildConfigField(
- "java.time.LocalDate",
- "APP_RELEASE_DATE",
- """LocalDate.of(${releaseDate.year}, ${releaseDate.monthValue}, ${releaseDate.dayOfMonth})"""
+ name = "APP_RELEASE_DATE",
+ type = "java.time.LocalDate",
+ expression = """LocalDate.parse("$releaseDate")"""
)
}
@@ -305,12 +307,20 @@ compose.desktop {
* It seems that the Windows OS does not respect the GIF speed and no-loop settings and
* plays the animation at a low frame rate. See https://stackoverflow.com/q/25382400
*/
- jvmArgs += "-splash:app/resources/splash-screen.gif"
+ jvmArgs += "-splash:${'$'}APPDIR/resources/splash-screen.gif"
nativeDistributions {
- // java.naming is to prevent app crash when running the app exe
- // (introduced with ch.qos.logback:logback-classic version 1.4.5)
- // jdk.unsupported is for showing the display image when running the app exe
- modules("java.naming", "jdk.unsupported")
+ modules(
+ // For preventing app crash when running the app exe
+ // (introduced in ch.qos.logback:logback-classic version 1.4.5)
+ "java.naming",
+ // For showing the display image when running the app exe
+ // (introduced in app version 1.5.0 by reimplementing the media player)
+ "jdk.unsupported",
+ // For playing local files when running the app exe (introduced in CMP version 1.6.0)
+ "jdk.accessibility",
+ // For launching the app using "Open with" on a file (required by Apache Tika)
+ "java.sql"
+ )
targetFormats(Dmg, Msi, Exe, Deb)
packageVersion = "${project.version}.0.0"
packageName = project.name
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 937a711..d267178 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,26 +1,26 @@
[versions]
-gradle = "8.6"
-jvm = "21"
+gradle = "8.8"
+jvm = "17"
kotlin = "1.9.22"
proguard = "7.4.2"
-compose-multiplatform = "1.5.12"
-buildConfig = "4.1.2"
-buildDownload = "5.5.0"
+compose-multiplatform = "1.6.0"
+buildConfig = "5.3.5"
+buildDownload = "5.6.0"
kotlin-coroutines = "1.8.0"
-logback = "1.4.11"
-log4jToSlf4j = "2.21.1"
-kotlinLogging = "5.1.0"
-ffmpeg = "5.1.2-1.5.8"
-vlc = "3.0.20"
-vlcj = "4.8.2"
-upx = "4.2.1"
-wavySlider="1.0.0"
+logback = "1.5.5"
+log4jToSlf4j = "2.23.1"
+kotlinLogging = "6.0.9"
+ffmpeg = "6.1.1-1.5.10"
+vlc = "3.0.21"
+vlcj = "4.8.3"
+upx = "4.2.3"
+wavySlider="1.3.0"
persianDateTime = "4.2.1"
-junit5 = "5.10.0"
-assertj = "3.24.2"
-mockk = "1.13.8"
-tika = "2.9.1"
-jna = "5.13.0"
+junit5 = "5.10.2"
+assertj = "3.25.3"
+mockk = "1.13.10"
+tika = "2.9.2"
+jna = "5.14.0"
jaudioTagger = "3.0.1"
image-comparison = "4.4.0"
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index d64cd49..e644113 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 4981abd..dd82863 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip
networkTimeout=60000
validateDistributionUrl=false
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index 1aa94a4..b740cf1 100644
--- a/gradlew
+++ b/gradlew
@@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
diff --git a/raw/icons.svg b/raw/icons.svg
index 4402c8a..34be266 100644
--- a/raw/icons.svg
+++ b/raw/icons.svg
@@ -2,19 +2,19 @@
+ x="0"
+ y="0"
+ xlink:href="#path1-96"
+ id="use2"
+ transform="matrix(-1,0,0,1,62,0)" />
diff --git a/src/main/kotlin/ir/mahozad/cutcon/Defaults.kt b/src/main/kotlin/ir/mahozad/cutcon/Defaults.kt
index 0fd84d1..9b7d0c5 100644
--- a/src/main/kotlin/ir/mahozad/cutcon/Defaults.kt
+++ b/src/main/kotlin/ir/mahozad/cutcon/Defaults.kt
@@ -11,11 +11,12 @@ import ir.mahozad.cutcon.component.SystemDateTime
import ir.mahozad.cutcon.localization.Language
import ir.mahozad.cutcon.localization.LanguageEn
import ir.mahozad.cutcon.model.*
-import java.net.URL
+import java.net.URI
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.div
import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
@@ -23,15 +24,14 @@ const val DISPLAY_WIDTH = 600 // 16:9 aspect ratio
const val DISPLAY_HEIGHT = 338 // 16:9 aspect ratio
const val DISPLAY_WIDTH_MINI = 356 // 16:9 aspect ratio
const val DISPLAY_HEIGHT_MINI = 200 // 16:9 aspect ratio
-const val WINDOW_WIDTH_WITH_PANEL = DISPLAY_WIDTH + 384
-const val WINDOW_WIDTH_NO_PANEL = DISPLAY_WIDTH + 16
+const val SIDE_PANEL_WIDTH = 360
+const val WINDOW_WIDTH_WITH_PANEL = DISPLAY_WIDTH + SIDE_PANEL_WIDTH + 3 * /* Paddings */ 8
+const val WINDOW_WIDTH_NO_PANEL = DISPLAY_WIDTH + 2 * /* Paddings */ 8
const val WINDOW_HEIGHT_REGULAR = DISPLAY_HEIGHT + (4 /* Rows of components */ * 48) + 34
const val WINDOW_WIDTH_MINI = 372
const val WINDOW_HEIGHT_MINI = 314
-val defaultAudioImage by lazy {
- decodeImage(path = assetsPath / "cover.svg", mimeType = "image/svg+xml")
-}
+val defaultAudioImage by lazy { decodeImage(assetsPath / "cover.svg") }
/**
* Because VLC has a problem that finishes the media when seeking to 1.0f,
@@ -52,7 +52,7 @@ val defaultQuality = Quality.MEDIUM
val defaultLanguage: Language = LanguageEn
val defaultCalendar = Calendar.GREGORIAN
val defaultTheme = Theme.LIGHT
-val defaultTooltipDelay = 1.seconds
+val defaultTooltipDelay = 600.milliseconds
val defaultDateTimeCheckingPeriod = 3.seconds
val defaultFontSize = 13.sp
val defaultIconSize = 24.dp
@@ -73,7 +73,7 @@ val defaultDurationConverter = DefaultDurationConverter
val defaultTimeStampString get() = defaultDurationConverter.format(defaultTimeStamp, numberOfParts = 1)
val defaultScreenshotSaveDirectory = (System.getProperty("user.home") ?: error("Could not get user home directory"))
.let(::Path) / "Pictures" / "Screenshots" / BuildConfig.APP_NAME
-val defaultAspectRatio = AspectRatio.W16H9
+val defaultAspectRatio = AspectRatio.SOURCE
val defaultCoverOptions = CoverOptions(
path = null,
scale = 1f,
@@ -88,8 +88,7 @@ val defaultIntroOptions = IntroOptions(
val defaultIsResumed = true
val defaultAudioVolume = 1f
val defaultIsAudioMuted = false
-val defaultMediaUrl = URL("file://")
-val defaultSeek = 0f
+val defaultMediaUrl = URI("file://localhost").toURL()
val defaultSpeed = Speed.NORMAL
val defaultFastSpeed = Speed.FAST2_0
val defaultClipToLoop: Clip? = null
diff --git a/src/main/kotlin/ir/mahozad/cutcon/Main.kt b/src/main/kotlin/ir/mahozad/cutcon/Main.kt
index 377cfbc..dc31084 100644
--- a/src/main/kotlin/ir/mahozad/cutcon/Main.kt
+++ b/src/main/kotlin/ir/mahozad/cutcon/Main.kt
@@ -5,6 +5,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.*
import io.github.oshai.kotlinlogging.KotlinLogging.logger
import ir.mahozad.cutcon.component.*
+import ir.mahozad.cutcon.component.DefaultMediaPlayer
import ir.mahozad.cutcon.converter.DefaultConverterFactory
import ir.mahozad.cutcon.localization.Messages
import ir.mahozad.cutcon.ui.MainWindow
@@ -43,7 +44,7 @@ val assetsPath = System
val viewModel = MainViewModel(
urlMaker = DefaultUrlMaker,
- settings = Preferences.userRoot(),
+ settings = Preferences.userNodeForPackage({}::class.java),
dispatcher = Dispatchers.IO,
mediaPlayer = DefaultMediaPlayer(),
dateTimeChecker = DefaultDateTimeChecker(Dispatchers.IO),
@@ -52,9 +53,9 @@ val viewModel = MainViewModel(
)
@OptIn(ExperimentalComposeUiApi::class)
-fun main() {
+fun main(vararg args: String) {
logger.info { "Application started" }
- initialize()
+ initialize(args.getOrNull(0))
application(exitProcessOnExit = false /* See exceptionHandler */) {
CompositionLocalProvider(
LocalWindowExceptionHandlerFactory provides exceptionHandler
@@ -66,10 +67,12 @@ fun main() {
logger.info { "Application finished" }
}
-private fun initialize() {
+private fun initialize(commandLinePath: String?) {
viewModel.startUrlMaker()
viewModel.startDateTimeChecker()
viewModel.startMediaProgressListener()
+ // For when launching the app via "Open with" or command line
+ commandLinePath?.let(::Path)?.run(viewModel::setSourceToLocal)
}
// This is also called when force closing/halting/killing the app process
diff --git a/src/main/kotlin/ir/mahozad/cutcon/MainViewModel.kt b/src/main/kotlin/ir/mahozad/cutcon/MainViewModel.kt
index 621d7a2..8af5be3 100644
--- a/src/main/kotlin/ir/mahozad/cutcon/MainViewModel.kt
+++ b/src/main/kotlin/ir/mahozad/cutcon/MainViewModel.kt
@@ -3,13 +3,13 @@ package ir.mahozad.cutcon
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.input.key.*
-import androidx.compose.ui.res.loadImageBitmap
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowPosition
import io.github.oshai.kotlinlogging.KotlinLogging.logger
import ir.mahozad.cutcon.component.*
+import ir.mahozad.cutcon.component.MediaPlayer
import ir.mahozad.cutcon.converter.ConverterFactory
import ir.mahozad.cutcon.localization.Language
import ir.mahozad.cutcon.model.*
@@ -27,7 +27,6 @@ import ir.mahozad.cutcon.ui.widget.COVER_PREVIEW_SIZE
import ir.mahozad.cutcon.ui.widget.INTRO_PREVIEW_SIZE
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
-import org.jaudiotagger.audio.AudioFileIO
import java.awt.GraphicsEnvironment
import java.awt.Rectangle
import java.nio.file.Path
@@ -60,14 +59,17 @@ class MainViewModel(
data class Failure(val throwable: Throwable) : ConversionStatus
}
+ data class WindowWidth(val value: Int, val isAnimated: Boolean)
+
private val logger = logger(name = MainViewModel::class.simpleName ?: "")
private val coroutineScope = CoroutineScope(dispatcher)
private var conversionJob: Job? = null
private val _isAppExitConfirmDialogDisplayed = MutableStateFlow(false)
private val _isChangelogDialogDisplayed = MutableStateFlow(
- settings[PREF_LAST_SHOWN_CHANGELOG_VERSION, null].let {
- compareVersionStrings(BuildConfig.APP_VERSION, it) == VersionComparisonResult.NEWER
- }
+ // settings[PREF_LAST_SHOWN_CHANGELOG_VERSION, null].let {
+ // compareVersionStrings(BuildConfig.APP_VERSION, it) == VersionComparisonResult.NEWER
+ // }
+ false
)
private val _language = MutableStateFlow(
settings[PREF_LANGUAGE, null]
@@ -120,6 +122,7 @@ class MainViewModel(
private val _source = MutableStateFlow