Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a95a3d433 | ||
|
|
f48a327a33 | ||
|
|
860274aa41 | ||
|
|
37f8b4645d | ||
|
|
88196f0f51 | ||
|
|
0a835699e0 | ||
|
|
d0d94c30fa |
No files matched your search
@@ -1 +0,0 @@
|
|||||||
{"sessionId":"db412f81-3e15-4e3f-9010-4a026dced776","pid":79175,"procStart":"Tue Jul 21 20:39:41 2026","acquiredAt":1784666538953}
|
|
||||||
@@ -15,7 +15,7 @@ fun main() = application {
|
|||||||
Window(
|
Window(
|
||||||
onCloseRequest = ::exitApplication,
|
onCloseRequest = ::exitApplication,
|
||||||
title = "InTono",
|
title = "InTono",
|
||||||
state = rememberWindowState(width = 1024.dp, height = 768.dp),
|
state = rememberWindowState(width = 1024.dp, height = 1268.dp),
|
||||||
) {
|
) {
|
||||||
NoteMusicaliTheme {
|
NoteMusicaliTheme {
|
||||||
Surface(
|
Surface(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import com.notemusicali.scores.extractMusicXmlFromBytes
|
|||||||
@Composable
|
@Composable
|
||||||
actual fun rememberFilePickerLauncher(
|
actual fun rememberFilePickerLauncher(
|
||||||
mimeTypes: List<String>,
|
mimeTypes: List<String>,
|
||||||
onFileContent: (String) -> Unit,
|
onFileContent: (fileName: String, content: String) -> Unit,
|
||||||
): () -> Unit {
|
): () -> Unit {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val launcher = rememberLauncherForActivityResult(
|
val launcher = rememberLauncherForActivityResult(
|
||||||
@@ -22,7 +22,7 @@ actual fun rememberFilePickerLauncher(
|
|||||||
val bytes = stream.readBytes()
|
val bytes = stream.readBytes()
|
||||||
val content = extractMusicXmlFromBytes(bytes)
|
val content = extractMusicXmlFromBytes(bytes)
|
||||||
if (content != null) {
|
if (content != null) {
|
||||||
onFileContent(content)
|
onFileContent(displayNameOf(context, it), content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
@@ -33,3 +33,14 @@ actual fun rememberFilePickerLauncher(
|
|||||||
|
|
||||||
return { launcher.launch(mimeTypes.toTypedArray()) }
|
return { launcher.launch(mimeTypes.toTypedArray()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun displayNameOf(context: android.content.Context, uri: Uri): String {
|
||||||
|
return try {
|
||||||
|
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||||
|
val idx = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
|
||||||
|
if (idx >= 0 && cursor.moveToFirst()) cursor.getString(idx) else null
|
||||||
|
} ?: (uri.lastPathSegment ?: "Import")
|
||||||
|
} catch (_: Exception) {
|
||||||
|
uri.lastPathSegment ?: "Import"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -234,4 +234,6 @@
|
|||||||
<string name="info_website">djluza.com</string>
|
<string name="info_website">djluza.com</string>
|
||||||
<string name="info_privacy_policy">Privacy Policy</string>
|
<string name="info_privacy_policy">Privacy Policy</string>
|
||||||
<string name="info_privacy_url">https://djluza.com/intono/privacy</string>
|
<string name="info_privacy_url">https://djluza.com/intono/privacy</string>
|
||||||
|
<string name="imported_exercises">Imported</string>
|
||||||
|
<string name="delete_imported">Delete imported score</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -234,4 +234,6 @@
|
|||||||
<string name="info_website">djluza.com</string>
|
<string name="info_website">djluza.com</string>
|
||||||
<string name="info_privacy_policy">Privacy Policy</string>
|
<string name="info_privacy_policy">Privacy Policy</string>
|
||||||
<string name="info_privacy_url">https://djluza.com/intono/privacy</string>
|
<string name="info_privacy_url">https://djluza.com/intono/privacy</string>
|
||||||
|
<string name="imported_exercises">Importati</string>
|
||||||
|
<string name="delete_imported">Elimina spartito importato</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.notemusicali.exercises
|
package com.notemusicali.exercises
|
||||||
|
|
||||||
import com.notemusicali.music.MusicalNote
|
import com.notemusicali.music.MusicalNote
|
||||||
|
import com.notemusicali.music.NoteDuration
|
||||||
import com.notemusicali.music.NoteSequence
|
import com.notemusicali.music.NoteSequence
|
||||||
|
|
||||||
data class Exercise(
|
data class Exercise(
|
||||||
@@ -125,5 +126,195 @@ object ExerciseRepository {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Exercise(
|
||||||
|
id = 6,
|
||||||
|
title = "Studio op.84 n.10 — Dancla",
|
||||||
|
description = "Studio per violino in Sol maggiore",
|
||||||
|
level = 4,
|
||||||
|
sequence = NoteSequence(
|
||||||
|
name = "Studio op.84 n.10 — Dancla",
|
||||||
|
notes = listOf(
|
||||||
|
MusicalNote.fromMidi(55, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(59, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(67, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(71, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(59, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(67, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(71, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(72, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(76, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(67, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(71, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(79, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(83, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(55, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(59, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(67, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(71, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(59, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(67, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(71, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(72, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(76, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(67, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(71, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(79, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(66, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(78, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(81, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(81, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(79, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(76, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(73, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(64, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(73, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(76, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(81, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(81, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(78, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(66, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(67, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(71, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(76, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(79, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(83, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(66, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(78, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(81, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(81, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(79, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(76, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(73, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(66, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(66, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(67, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(70, NoteDuration.EIGHTH, preferFlat = true),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(67, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(70, NoteDuration.EIGHTH, preferFlat = true),
|
||||||
|
MusicalNote.fromMidi(70, NoteDuration.EIGHTH, preferFlat = true),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(79, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(79, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(78, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(78, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(81, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(81, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(86, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(66, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(70, NoteDuration.EIGHTH, preferFlat = true),
|
||||||
|
MusicalNote.fromMidi(67, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(70, NoteDuration.EIGHTH, preferFlat = true),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(55, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(58, NoteDuration.EIGHTH, preferFlat = true),
|
||||||
|
MusicalNote.fromMidi(58, NoteDuration.EIGHTH, preferFlat = true),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(67, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(67, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(66, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(66, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(71, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(79, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(83, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(86, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(86, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(84, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(81, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(78, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(78, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(81, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(86, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(83, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(79, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(71, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(67, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(60, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(64, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(72, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(76, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(59, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(67, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(71, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(57, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(66, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(69, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(74, NoteDuration.HALF),
|
||||||
|
MusicalNote.fromMidi(67, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(59, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(62, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(55, NoteDuration.QUARTER),
|
||||||
|
),
|
||||||
|
beats = 4,
|
||||||
|
beatType = 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.notemusicali.exercises
|
||||||
|
|
||||||
|
import com.notemusicali.music.MusicalNote
|
||||||
|
import com.notemusicali.music.NoteDuration
|
||||||
|
import com.notemusicali.music.NoteSequence
|
||||||
|
import com.notemusicali.util.currentTimeMillis
|
||||||
|
import com.russhwolf.settings.Settings
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class StoredNote(
|
||||||
|
val midi: Int,
|
||||||
|
val duration: NoteDuration = NoteDuration.QUARTER,
|
||||||
|
val flat: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ImportedExercise(
|
||||||
|
val id: String,
|
||||||
|
val title: String,
|
||||||
|
val notes: List<StoredNote>,
|
||||||
|
val beats: Int = 4,
|
||||||
|
val beatType: Int = 4,
|
||||||
|
) {
|
||||||
|
fun toNoteSequence(): NoteSequence = NoteSequence(
|
||||||
|
name = title,
|
||||||
|
notes = notes.map { MusicalNote.fromMidi(it.midi, it.duration, preferFlat = it.flat) },
|
||||||
|
beats = beats,
|
||||||
|
beatType = beatType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Persistenza degli spartiti importati dall'utente (JSON su multiplatform-settings). */
|
||||||
|
internal class ImportedExerciseStore(private val settings: Settings) {
|
||||||
|
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
fun getAll(): List<ImportedExercise> {
|
||||||
|
val raw = settings.getStringOrNull(KEY) ?: return emptyList()
|
||||||
|
return try {
|
||||||
|
json.decodeFromString<List<ImportedExercise>>(raw)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun add(title: String, sequence: NoteSequence, nowMs: Long = currentTimeMillis()): ImportedExercise {
|
||||||
|
val exercise = ImportedExercise(
|
||||||
|
id = nowMs.toString(),
|
||||||
|
title = title,
|
||||||
|
notes = sequence.notes.map { StoredNote(it.midiNumber, it.duration, it.preferFlat) },
|
||||||
|
beats = sequence.beats,
|
||||||
|
beatType = sequence.beatType,
|
||||||
|
)
|
||||||
|
val updated = (listOf(exercise) + getAll()).take(MAX_IMPORTED)
|
||||||
|
settings.putString(KEY, json.encodeToString(updated))
|
||||||
|
return exercise
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(id: String) {
|
||||||
|
val remaining = getAll().filterNot { it.id == id }
|
||||||
|
settings.putString(KEY, json.encodeToString(remaining))
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val KEY = "imported_exercises"
|
||||||
|
const val MAX_IMPORTED = 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Facade con lo storage reale dell'app. */
|
||||||
|
object ImportedExerciseRepository {
|
||||||
|
private val store by lazy { ImportedExerciseStore(Settings()) }
|
||||||
|
|
||||||
|
fun getAll(): List<ImportedExercise> = store.getAll()
|
||||||
|
fun add(title: String, sequence: NoteSequence): ImportedExercise = store.add(title, sequence)
|
||||||
|
fun remove(id: String) = store.remove(id)
|
||||||
|
}
|
||||||
@@ -5,10 +5,10 @@ import androidx.compose.runtime.Composable
|
|||||||
/**
|
/**
|
||||||
* Platform-specific file picker launcher.
|
* Platform-specific file picker launcher.
|
||||||
* Returns a lambda that, when invoked, opens the file picker.
|
* Returns a lambda that, when invoked, opens the file picker.
|
||||||
* [onFileContent] is called with the file content as a String.
|
* [onFileContent] is called with the display name of the file and its content.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
expect fun rememberFilePickerLauncher(
|
expect fun rememberFilePickerLauncher(
|
||||||
mimeTypes: List<String>,
|
mimeTypes: List<String>,
|
||||||
onFileContent: (String) -> Unit,
|
onFileContent: (fileName: String, content: String) -> Unit,
|
||||||
): () -> Unit
|
): () -> Unit
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.notemusicali.ui.components
|
package com.notemusicali.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -13,6 +14,7 @@ import androidx.compose.ui.geometry.Offset
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.rememberTextMeasurer
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.notemusicali.music.MusicalNote
|
import com.notemusicali.music.MusicalNote
|
||||||
|
|
||||||
@@ -29,6 +31,8 @@ fun StaffFullView(
|
|||||||
lightTheme: Boolean = false,
|
lightTheme: Boolean = false,
|
||||||
beats: Int = 4,
|
beats: Int = 4,
|
||||||
beatType: Int = 4,
|
beatType: Int = 4,
|
||||||
|
// Altezza massima visibile: oltre, lo spartito scorre da solo seguendo la nota corrente
|
||||||
|
maxVisibleHeight: Dp = 640.dp,
|
||||||
) {
|
) {
|
||||||
if (notes.isEmpty()) return
|
if (notes.isEmpty()) return
|
||||||
|
|
||||||
@@ -65,10 +69,23 @@ fun StaffFullView(
|
|||||||
scrollState.animateScrollTo(targetY)
|
scrollState.animateScrollTo(targetY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Con un vincolo di altezza dal contenitore (es. weight in Pratica) il
|
||||||
|
// viewport si adatta alla finestra; altrimenti vale il tetto di default.
|
||||||
|
// L'altezza si aggancia a righi interi per non tagliare un rigo a metà.
|
||||||
|
val heightBound = if (this.maxHeight != Dp.Infinity) this.maxHeight else maxVisibleHeight
|
||||||
|
val stride = systemHeightDp + systemGapDp
|
||||||
|
val wholeSystems = ((heightBound + systemGapDp) / stride).toInt().coerceAtLeast(1)
|
||||||
|
val snappedBound = stride * wholeSystems - systemGapDp
|
||||||
|
val viewportHeightDp = if (totalHeightDp < snappedBound) totalHeightDp else snappedBound
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(viewportHeightDp)
|
||||||
|
.verticalScroll(scrollState),
|
||||||
|
) {
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.verticalScroll(scrollState)
|
|
||||||
.height(totalHeightDp),
|
.height(totalHeightDp),
|
||||||
) {
|
) {
|
||||||
val lineColor = if (lightTheme) Color.Black else Color.White.copy(alpha = 0.4f)
|
val lineColor = if (lightTheme) Color.Black else Color.White.copy(alpha = 0.4f)
|
||||||
@@ -196,3 +213,4 @@ fun StaffFullView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -27,8 +27,16 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import com.notemusicali.exercises.Exercise
|
import com.notemusicali.exercises.Exercise
|
||||||
import com.notemusicali.exercises.ExerciseRepository
|
import com.notemusicali.exercises.ExerciseRepository
|
||||||
|
import com.notemusicali.exercises.ImportedExercise
|
||||||
|
import com.notemusicali.exercises.ImportedExerciseRepository
|
||||||
import notemusicali.shared.generated.resources.Res
|
import notemusicali.shared.generated.resources.Res
|
||||||
import notemusicali.shared.generated.resources.*
|
import notemusicali.shared.generated.resources.*
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
@@ -46,6 +54,8 @@ fun ExercisesScreen(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onExerciseSelected: (NoteSequence) -> Unit,
|
onExerciseSelected: (NoteSequence) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
var imported by remember { mutableStateOf(ImportedExerciseRepository.getAll()) }
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.TopCenter,
|
contentAlignment = Alignment.TopCenter,
|
||||||
@@ -70,6 +80,28 @@ fun ExercisesScreen(
|
|||||||
onClick = { onExerciseSelected(exercise.sequence) },
|
onClick = { onExerciseSelected(exercise.sequence) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imported.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.imported_exercises),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color.White.copy(alpha = 0.5f),
|
||||||
|
modifier = Modifier.padding(top = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(imported, key = { it.id }) { entry ->
|
||||||
|
ImportedExerciseCard(
|
||||||
|
entry = entry,
|
||||||
|
onClick = { onExerciseSelected(entry.toNoteSequence()) },
|
||||||
|
onDelete = {
|
||||||
|
ImportedExerciseRepository.remove(entry.id)
|
||||||
|
imported = ImportedExerciseRepository.getAll()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,3 +165,49 @@ private fun StarRating(level: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ImportedExerciseCard(
|
||||||
|
entry: ImportedExercise,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
) {
|
||||||
|
GradientCard(
|
||||||
|
gradient = CardGradients.scoreFile,
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = entry.title,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = Color.White,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
IconButton(onClick = onDelete) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Delete,
|
||||||
|
contentDescription = stringResource(Res.string.delete_imported),
|
||||||
|
tint = Color.White.copy(alpha = 0.6f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
StaffPreview(
|
||||||
|
notes = entry.toNoteSequence().notes,
|
||||||
|
beats = entry.beats,
|
||||||
|
beatType = entry.beatType,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.n_notes, entry.notes.size),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = Color.White.copy(alpha = 0.5f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@@ -231,7 +233,13 @@ fun ManualInputScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (notes.isNotEmpty()) {
|
if (notes.isNotEmpty()) {
|
||||||
|
val notesRowState = rememberLazyListState()
|
||||||
|
LaunchedEffect(notes.size) {
|
||||||
|
// Segui sempre l'ultima nota inserita
|
||||||
|
notesRowState.animateScrollToItem(notes.size - 1)
|
||||||
|
}
|
||||||
LazyRow(
|
LazyRow(
|
||||||
|
state = notesRowState,
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
@@ -274,7 +274,11 @@ fun PracticeScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentMode == StaffMode.FULL) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
} else {
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
// Staff display based on mode
|
// Staff display based on mode
|
||||||
val staffBgMod = if (lightStaff) {
|
val staffBgMod = if (lightStaff) {
|
||||||
@@ -310,7 +314,8 @@ fun PracticeScreen(
|
|||||||
StaffFullView(
|
StaffFullView(
|
||||||
notes = state.sequence.notes,
|
notes = state.sequence.notes,
|
||||||
currentIndex = state.currentIndex,
|
currentIndex = state.currentIndex,
|
||||||
modifier = staffBgMod,
|
// Lo spartito si prende lo spazio residuo: cresce con la finestra
|
||||||
|
modifier = Modifier.weight(1f, fill = false).then(staffBgMod),
|
||||||
lightTheme = lightStaff,
|
lightTheme = lightStaff,
|
||||||
beats = state.sequence.beats,
|
beats = state.sequence.beats,
|
||||||
beatType = state.sequence.beatType,
|
beatType = state.sequence.beatType,
|
||||||
@@ -389,12 +394,16 @@ fun PracticeScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentMode == StaffMode.FULL) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
} else {
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CompletedView(
|
private fun CompletedView(
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.notemusicali.exercises.ImportedExerciseRepository
|
||||||
import com.notemusicali.music.MusicXmlParser
|
import com.notemusicali.music.MusicXmlParser
|
||||||
import com.notemusicali.music.NoteSequence
|
import com.notemusicali.music.NoteSequence
|
||||||
import com.notemusicali.ui.components.BackTopBar
|
import com.notemusicali.ui.components.BackTopBar
|
||||||
@@ -47,10 +48,17 @@ fun PracticeSetupScreen(
|
|||||||
) {
|
) {
|
||||||
val filePickerLauncher = rememberFilePickerLauncher(
|
val filePickerLauncher = rememberFilePickerLauncher(
|
||||||
mimeTypes = listOf("text/xml", "application/xml", "*/*"),
|
mimeTypes = listOf("text/xml", "application/xml", "*/*"),
|
||||||
onFileContent = { content ->
|
onFileContent = { fileName, content ->
|
||||||
try {
|
try {
|
||||||
val sequence = MusicXmlParser.parse(content, "MusicXML Import")
|
val title = fileName
|
||||||
|
.removeSuffix(".musicxml").removeSuffix(".music.xml")
|
||||||
|
.removeSuffix(".xml").removeSuffix(".mxl")
|
||||||
|
.replace('_', ' ')
|
||||||
|
.ifBlank { "MusicXML Import" }
|
||||||
|
val sequence = MusicXmlParser.parse(content, title)
|
||||||
if (sequence.notes.isNotEmpty()) {
|
if (sequence.notes.isNotEmpty()) {
|
||||||
|
// Lo spartito importato resta disponibile tra gli esercizi
|
||||||
|
ImportedExerciseRepository.add(title, sequence)
|
||||||
onMusicXmlLoaded(sequence)
|
onMusicXmlLoaded(sequence)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.notemusicali.exercises
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class ExerciseRepositoryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dancla study op84 n10 is available as exercise`() {
|
||||||
|
val exercise = ExerciseRepository.all.firstOrNull { it.title.contains("Dancla") }
|
||||||
|
assertNotNull(exercise, "lo studio di Dancla deve essere tra gli esercizi")
|
||||||
|
assertTrue(exercise.sequence.notes.size > 100, "atteso lo studio completo, trovate ${exercise.sequence.notes.size} note")
|
||||||
|
assertEquals(4, exercise.sequence.beats)
|
||||||
|
assertEquals(4, exercise.sequence.beatType)
|
||||||
|
// Studio per violino: tutte le note dentro l'estensione del violino (G3..E7)
|
||||||
|
assertTrue(exercise.sequence.notes.all { it.midiNumber in 55..100 })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `exercise ids are unique`() {
|
||||||
|
val ids = ExerciseRepository.all.map { it.id }
|
||||||
|
assertEquals(ids.size, ids.toSet().size)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.notemusicali.exercises
|
||||||
|
|
||||||
|
import com.notemusicali.music.MusicalNote
|
||||||
|
import com.notemusicali.music.NoteDuration
|
||||||
|
import com.notemusicali.music.NoteSequence
|
||||||
|
import com.russhwolf.settings.MapSettings
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class ImportedExerciseStoreTest {
|
||||||
|
|
||||||
|
private fun sequence() = NoteSequence(
|
||||||
|
name = "Test",
|
||||||
|
notes = listOf(
|
||||||
|
MusicalNote.fromMidi(60, NoteDuration.EIGHTH),
|
||||||
|
MusicalNote.fromMidi(70, NoteDuration.HALF, preferFlat = true),
|
||||||
|
),
|
||||||
|
beats = 3,
|
||||||
|
beatType = 4,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `added exercise round trips with notes durations flats and meter`() {
|
||||||
|
val store = ImportedExerciseStore(MapSettings())
|
||||||
|
store.add("Il mio spartito", sequence(), nowMs = 1000L)
|
||||||
|
|
||||||
|
val loaded = store.getAll().single()
|
||||||
|
assertEquals("Il mio spartito", loaded.title)
|
||||||
|
val seq = loaded.toNoteSequence()
|
||||||
|
assertEquals(2, seq.notes.size)
|
||||||
|
assertEquals(60, seq.notes[0].midiNumber)
|
||||||
|
assertEquals(NoteDuration.EIGHTH, seq.notes[0].duration)
|
||||||
|
assertEquals("SIb", seq.notes[1].displayName)
|
||||||
|
assertEquals(NoteDuration.HALF, seq.notes[1].duration)
|
||||||
|
assertEquals(3, seq.beats)
|
||||||
|
assertEquals(4, seq.beatType)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `newest import comes first`() {
|
||||||
|
val store = ImportedExerciseStore(MapSettings())
|
||||||
|
store.add("Primo", sequence(), nowMs = 1L)
|
||||||
|
store.add("Secondo", sequence(), nowMs = 2L)
|
||||||
|
assertEquals(listOf("Secondo", "Primo"), store.getAll().map { it.title })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `remove deletes only the requested exercise`() {
|
||||||
|
val store = ImportedExerciseStore(MapSettings())
|
||||||
|
val a = store.add("A", sequence(), nowMs = 1L)
|
||||||
|
store.add("B", sequence(), nowMs = 2L)
|
||||||
|
store.remove(a.id)
|
||||||
|
assertEquals(listOf("B"), store.getAll().map { it.title })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty storage yields empty list`() {
|
||||||
|
assertTrue(ImportedExerciseStore(MapSettings()).getAll().isEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,31 +5,41 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import com.notemusicali.scores.extractMusicXmlFromBytes
|
import com.notemusicali.scores.extractMusicXmlFromBytes
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
import javax.swing.JFileChooser
|
import javax.swing.JFileChooser
|
||||||
import javax.swing.filechooser.FileNameExtensionFilter
|
import javax.swing.filechooser.FileNameExtensionFilter
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun rememberFilePickerLauncher(
|
actual fun rememberFilePickerLauncher(
|
||||||
mimeTypes: List<String>,
|
mimeTypes: List<String>,
|
||||||
onFileContent: (String) -> Unit,
|
onFileContent: (fileName: String, content: String) -> Unit,
|
||||||
): () -> Unit {
|
): () -> Unit {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
return {
|
return {
|
||||||
scope.launch(Dispatchers.Default) {
|
// I dialoghi Swing vivono solo sull'Event Dispatch Thread; mostrarli da
|
||||||
|
// un thread di background inchioda l'app su macOS
|
||||||
|
scope.launch(Dispatchers.Swing) {
|
||||||
val chooser = JFileChooser().apply {
|
val chooser = JFileChooser().apply {
|
||||||
fileFilter = FileNameExtensionFilter("MusicXML files", "xml", "musicxml", "mxl")
|
fileFilter = FileNameExtensionFilter("MusicXML files", "xml", "musicxml", "mxl")
|
||||||
dialogTitle = "Seleziona file MusicXML"
|
dialogTitle = "Seleziona file MusicXML"
|
||||||
}
|
}
|
||||||
val result = chooser.showOpenDialog(null)
|
val parent = KeyboardFocusManager.getCurrentKeyboardFocusManager().activeWindow
|
||||||
|
val result = chooser.showOpenDialog(parent)
|
||||||
if (result == JFileChooser.APPROVE_OPTION) {
|
if (result == JFileChooser.APPROVE_OPTION) {
|
||||||
|
val file = chooser.selectedFile
|
||||||
|
// Lettura e unzip fuori dall'EDT
|
||||||
|
val content = withContext(Dispatchers.Default) {
|
||||||
try {
|
try {
|
||||||
val bytes = chooser.selectedFile.readBytes()
|
extractMusicXmlFromBytes(file.readBytes())
|
||||||
val content = extractMusicXmlFromBytes(bytes)
|
|
||||||
if (content != null) {
|
|
||||||
onFileContent(content)
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// File read failed
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Callback di nuovo sull'EDT: aggiorna stato Compose e naviga
|
||||||
|
if (content != null) {
|
||||||
|
onFileContent(file.name, content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -1,6 +1,7 @@
|
|||||||
package com.notemusicali.ui.components
|
package com.notemusicali.ui.components
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
import javax.swing.JFileChooser
|
import javax.swing.JFileChooser
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -12,7 +13,9 @@ actual fun rememberFolderPickerLauncher(
|
|||||||
fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||||
dialogTitle = "Scegli cartella spartiti"
|
dialogTitle = "Scegli cartella spartiti"
|
||||||
}
|
}
|
||||||
if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
|
// Parent = finestra attiva, così il dialogo non finisce dietro l'app
|
||||||
|
val parent = KeyboardFocusManager.getCurrentKeyboardFocusManager().activeWindow
|
||||||
|
if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) {
|
||||||
val file = chooser.selectedFile
|
val file = chooser.selectedFile
|
||||||
onFolderSelected(file.absolutePath, file.name)
|
onFolderSelected(file.absolutePath, file.name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.notemusicali.scan.PlatformImage
|
import com.notemusicali.scan.PlatformImage
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import java.awt.KeyboardFocusManager
|
||||||
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import javax.imageio.ImageIO
|
import javax.imageio.ImageIO
|
||||||
@@ -45,9 +47,17 @@ actual fun CameraCapture(
|
|||||||
)
|
)
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch(Dispatchers.Default) {
|
// Dialogo sull'EDT (Swing), decodifica dell'immagine su Default
|
||||||
val image = pickImageFile() ?: return@launch
|
scope.launch(Dispatchers.Swing) {
|
||||||
withContext(Dispatchers.Main) { onImageCaptured(image) }
|
val file = pickImageFile() ?: return@launch
|
||||||
|
val image = withContext(Dispatchers.Default) {
|
||||||
|
try {
|
||||||
|
ImageIO.read(file)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} ?: return@launch
|
||||||
|
onImageCaptured(image)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
@@ -61,16 +71,13 @@ actual fun CameraCapture(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pickImageFile(): PlatformImage? {
|
private fun pickImageFile(): java.io.File? {
|
||||||
val chooser = JFileChooser().apply {
|
val chooser = JFileChooser().apply {
|
||||||
dialogTitle = "Scegli l'immagine dello spartito"
|
dialogTitle = "Scegli l'immagine dello spartito"
|
||||||
fileFilter = FileNameExtensionFilter("Immagini (PNG, JPEG)", "png", "jpg", "jpeg")
|
fileFilter = FileNameExtensionFilter("Immagini (PNG, JPEG)", "png", "jpg", "jpeg")
|
||||||
isMultiSelectionEnabled = false
|
isMultiSelectionEnabled = false
|
||||||
}
|
}
|
||||||
if (chooser.showOpenDialog(null) != JFileChooser.APPROVE_OPTION) return null
|
val parent = KeyboardFocusManager.getCurrentKeyboardFocusManager().activeWindow
|
||||||
return try {
|
if (chooser.showOpenDialog(parent) != JFileChooser.APPROVE_OPTION) return null
|
||||||
ImageIO.read(chooser.selectedFile)
|
return chooser.selectedFile
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -22,14 +22,14 @@ private var retainedFileDelegate: FilePickerDelegate? = null
|
|||||||
@Composable
|
@Composable
|
||||||
actual fun rememberFilePickerLauncher(
|
actual fun rememberFilePickerLauncher(
|
||||||
mimeTypes: List<String>,
|
mimeTypes: List<String>,
|
||||||
onFileContent: (String) -> Unit,
|
onFileContent: (fileName: String, content: String) -> Unit,
|
||||||
): () -> Unit {
|
): () -> Unit {
|
||||||
return {
|
return {
|
||||||
presentFilePicker(onFileContent)
|
presentFilePicker(onFileContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun presentFilePicker(onFileContent: (String) -> Unit) {
|
private fun presentFilePicker(onFileContent: (fileName: String, content: String) -> Unit) {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
val rootVC = UIApplication.sharedApplication.keyWindow?.rootViewController ?: return
|
val rootVC = UIApplication.sharedApplication.keyWindow?.rootViewController ?: return
|
||||||
val topVC = topPresentedViewController(rootVC)
|
val topVC = topPresentedViewController(rootVC)
|
||||||
@@ -56,7 +56,7 @@ private fun topPresentedViewController(vc: UIViewController): UIViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class FilePickerDelegate(
|
private class FilePickerDelegate(
|
||||||
private val onFileContent: (String) -> Unit,
|
private val onFileContent: (fileName: String, content: String) -> Unit,
|
||||||
) : NSObject(), UIDocumentPickerDelegateProtocol {
|
) : NSObject(), UIDocumentPickerDelegateProtocol {
|
||||||
|
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
@@ -71,7 +71,7 @@ private class FilePickerDelegate(
|
|||||||
val data = NSData.dataWithContentsOfURL(url) ?: return
|
val data = NSData.dataWithContentsOfURL(url) ?: return
|
||||||
val bytes = data.toByteArray()
|
val bytes = data.toByteArray()
|
||||||
val content = extractMusicXmlFromBytes(bytes) ?: return
|
val content = extractMusicXmlFromBytes(bytes) ?: return
|
||||||
onFileContent(content)
|
onFileContent(url.lastPathComponent ?: "Import", content)
|
||||||
} finally {
|
} finally {
|
||||||
if (accessing) url.stopAccessingSecurityScopedResource()
|
if (accessing) url.stopAccessingSecurityScopedResource()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user