From 0add6de7161ba18005feff3821a3ebe79f238570 Mon Sep 17 00:00:00 2001 From: luciano Date: Wed, 22 Jul 2026 10:38:51 +0200 Subject: [PATCH] fix(ci): il pattern gitignore 'scores/' escludeva anche i sorgenti 'scores/' senza slash iniziale ignorava ogni directory 'scores' a qualsiasi livello: mancavano dal repo il package com.notemusicali.scores, ScoresScreen e i 12 spartiti bundled. Ancorato a '/scores/' (solo la libreria alla radice) e aggiunti i file mancanti. Co-Authored-By: Claude Fable 5 --- .gitignore | 6 +- .../notemusicali/scores/MxlReader.android.kt | 25 ++ .../scores/ScoreProvider.android.kt | 92 +++++++ .../scores/ScoreProviderContext.kt | 14 + .../files/scores/bella_ciao.mxl | Bin 0 -> 10960 bytes .../files/scores/canone_pachelbel.mxl | Bin 0 -> 6687 bytes .../files/scores/carol_of_the_bells.mxl | Bin 0 -> 3480 bytes .../files/scores/fur_elise.mxl | Bin 0 -> 3618 bytes .../files/scores/greensleeves.mxl | Bin 0 -> 4918 bytes .../files/scores/gymnopedie_satie.mxl | Bin 0 -> 4866 bytes .../files/scores/happy_birthday.mxl | Bin 0 -> 2274 bytes .../files/scores/lago_dei_cigni.mxl | Bin 0 -> 5706 bytes .../files/scores/minuetto_bach.mxl | Bin 0 -> 4745 bytes .../files/scores/notturno_chopin.mxl | Bin 0 -> 7734 bytes .../files/scores/ode_to_joy.mxl | Bin 0 -> 3209 bytes .../files/scores/preludio_bach.mxl | Bin 0 -> 7726 bytes .../com/notemusicali/scores/BundledScores.kt | 47 ++++ .../com/notemusicali/scores/MxlReader.kt | 3 + .../com/notemusicali/scores/ScoreEntry.kt | 7 + .../com/notemusicali/scores/ScoreProvider.kt | 8 + .../notemusicali/ui/scores/ScoresScreen.kt | 249 ++++++++++++++++++ .../notemusicali/scores/BundledScoresTest.kt | 43 +++ .../com/notemusicali/scores/MxlReaderTest.kt | 47 ++++ .../notemusicali/scores/MxlReader.desktop.kt | 23 ++ .../scores/ScoreProvider.desktop.kt | 64 +++++ .../com/notemusicali/scores/MxlReader.ios.kt | 124 +++++++++ .../notemusicali/scores/ScoreProvider.ios.kt | 141 ++++++++++ 27 files changed, 890 insertions(+), 3 deletions(-) create mode 100644 shared/src/androidMain/kotlin/com/notemusicali/scores/MxlReader.android.kt create mode 100644 shared/src/androidMain/kotlin/com/notemusicali/scores/ScoreProvider.android.kt create mode 100644 shared/src/androidMain/kotlin/com/notemusicali/scores/ScoreProviderContext.kt create mode 100644 shared/src/commonMain/composeResources/files/scores/bella_ciao.mxl create mode 100644 shared/src/commonMain/composeResources/files/scores/canone_pachelbel.mxl create mode 100644 shared/src/commonMain/composeResources/files/scores/carol_of_the_bells.mxl create mode 100644 shared/src/commonMain/composeResources/files/scores/fur_elise.mxl create mode 100644 shared/src/commonMain/composeResources/files/scores/greensleeves.mxl create mode 100644 shared/src/commonMain/composeResources/files/scores/gymnopedie_satie.mxl create mode 100644 shared/src/commonMain/composeResources/files/scores/happy_birthday.mxl create mode 100644 shared/src/commonMain/composeResources/files/scores/lago_dei_cigni.mxl create mode 100644 shared/src/commonMain/composeResources/files/scores/minuetto_bach.mxl create mode 100644 shared/src/commonMain/composeResources/files/scores/notturno_chopin.mxl create mode 100644 shared/src/commonMain/composeResources/files/scores/ode_to_joy.mxl create mode 100644 shared/src/commonMain/composeResources/files/scores/preludio_bach.mxl create mode 100644 shared/src/commonMain/kotlin/com/notemusicali/scores/BundledScores.kt create mode 100644 shared/src/commonMain/kotlin/com/notemusicali/scores/MxlReader.kt create mode 100644 shared/src/commonMain/kotlin/com/notemusicali/scores/ScoreEntry.kt create mode 100644 shared/src/commonMain/kotlin/com/notemusicali/scores/ScoreProvider.kt create mode 100644 shared/src/commonMain/kotlin/com/notemusicali/ui/scores/ScoresScreen.kt create mode 100644 shared/src/commonTest/kotlin/com/notemusicali/scores/BundledScoresTest.kt create mode 100644 shared/src/commonTest/kotlin/com/notemusicali/scores/MxlReaderTest.kt create mode 100644 shared/src/desktopMain/kotlin/com/notemusicali/scores/MxlReader.desktop.kt create mode 100644 shared/src/desktopMain/kotlin/com/notemusicali/scores/ScoreProvider.desktop.kt create mode 100644 shared/src/iosMain/kotlin/com/notemusicali/scores/MxlReader.ios.kt create mode 100644 shared/src/iosMain/kotlin/com/notemusicali/scores/ScoreProvider.ios.kt diff --git a/.gitignore b/.gitignore index 18d29d0..9887ff7 100644 --- a/.gitignore +++ b/.gitignore @@ -22,9 +22,9 @@ iosApp/build/ video/node_modules/ video/out/ -# Score library (256MB of .mxl, not source code — the curated bundled set -# lives in shared/src/commonMain/composeResources/files/scores/) -scores/ +# Score library at the repo root (256MB of .mxl, not source code — the curated +# bundled set lives in shared/src/commonMain/composeResources/files/scores/) +/scores/ # Binary release artifacts *.apk diff --git a/shared/src/androidMain/kotlin/com/notemusicali/scores/MxlReader.android.kt b/shared/src/androidMain/kotlin/com/notemusicali/scores/MxlReader.android.kt new file mode 100644 index 0000000..cc82328 --- /dev/null +++ b/shared/src/androidMain/kotlin/com/notemusicali/scores/MxlReader.android.kt @@ -0,0 +1,25 @@ +package com.notemusicali.scores + +import java.util.zip.ZipInputStream + +actual fun extractMusicXmlFromBytes(bytes: ByteArray): String? { + // Check for ZIP magic bytes (PK\x03\x04) = MXL file + if (bytes.size >= 4 && bytes[0] == 0x50.toByte() && bytes[1] == 0x4B.toByte()) { + return try { + ZipInputStream(bytes.inputStream()).use { zis -> + var entry = zis.nextEntry + while (entry != null) { + if (entry.name.endsWith(".xml") && !entry.name.startsWith("META-INF")) { + return@use zis.bufferedReader().readText() + } + entry = zis.nextEntry + } + null + } + } catch (_: Exception) { + null + } + } + // Plain XML + return bytes.decodeToString() +} diff --git a/shared/src/androidMain/kotlin/com/notemusicali/scores/ScoreProvider.android.kt b/shared/src/androidMain/kotlin/com/notemusicali/scores/ScoreProvider.android.kt new file mode 100644 index 0000000..2eb8013 --- /dev/null +++ b/shared/src/androidMain/kotlin/com/notemusicali/scores/ScoreProvider.android.kt @@ -0,0 +1,92 @@ +package com.notemusicali.scores + +import android.net.Uri +import android.provider.DocumentsContract + +actual object ScoreProvider { + actual fun listScores(relativePath: String): List { + val baseDir = ScoreProviderContext.scoresDir ?: return emptyList() + val dir = if (relativePath.isEmpty()) baseDir + else java.io.File(baseDir, relativePath) + if (!dir.exists() || !dir.isDirectory) return emptyList() + + return dir.listFiles() + ?.filter { it.isDirectory || it.extension.lowercase() in listOf("mxl", "xml", "musicxml") } + ?.sortedWith(compareBy { !it.isDirectory }.thenBy { it.name.lowercase() }) + ?.map { file -> + ScoreEntry( + name = if (file.isDirectory) file.name else file.nameWithoutExtension, + path = if (relativePath.isEmpty()) file.name + else "$relativePath/${file.name}", + isDirectory = file.isDirectory, + ) + } + ?: emptyList() + } + + actual fun readScoreBytes(path: String): ByteArray? { + val baseDir = ScoreProviderContext.scoresDir ?: return null + val file = java.io.File(baseDir, path) + if (!file.exists()) return null + return try { + file.readBytes() + } catch (_: Exception) { + null + } + } + + actual fun listFromUri(treeUri: String, documentId: String): List { + val context = ScoreProviderContext.appContext ?: return emptyList() + val treeUriParsed = Uri.parse(treeUri) + val parentDocId = if (documentId.isEmpty()) { + DocumentsContract.getTreeDocumentId(treeUriParsed) + } else { + documentId + } + val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUriParsed, parentDocId) + val results = mutableListOf() + try { + context.contentResolver.query( + childrenUri, + arrayOf( + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_MIME_TYPE, + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + ), + null, null, null, + )?.use { cursor -> + val nameCol = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME) + val mimeCol = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE) + val idCol = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID) + while (cursor.moveToNext()) { + val name = cursor.getString(nameCol) ?: continue + val mime = cursor.getString(mimeCol) ?: "" + val docId = cursor.getString(idCol) ?: continue + val isDir = mime == DocumentsContract.Document.MIME_TYPE_DIR + val ext = name.substringAfterLast(".", "").lowercase() + if (isDir || ext in listOf("mxl", "xml", "musicxml")) { + results.add( + ScoreEntry( + name = if (isDir) name else name.substringBeforeLast("."), + path = docId, + isDirectory = isDir, + ) + ) + } + } + } + } catch (_: Exception) {} + return results.sortedWith(compareBy { !it.isDirectory }.thenBy { it.name.lowercase() }) + } + + actual fun readFromUri(treeUri: String, documentId: String): ByteArray? { + val context = ScoreProviderContext.appContext ?: return null + val treeUriParsed = Uri.parse(treeUri) + val docUri = DocumentsContract.buildDocumentUriUsingTree(treeUriParsed, documentId) + return try { + context.contentResolver.openInputStream(docUri)?.use { it.readBytes() } + } catch (_: Exception) { + null + } + } +} diff --git a/shared/src/androidMain/kotlin/com/notemusicali/scores/ScoreProviderContext.kt b/shared/src/androidMain/kotlin/com/notemusicali/scores/ScoreProviderContext.kt new file mode 100644 index 0000000..c7d34dc --- /dev/null +++ b/shared/src/androidMain/kotlin/com/notemusicali/scores/ScoreProviderContext.kt @@ -0,0 +1,14 @@ +package com.notemusicali.scores + +import android.content.Context +import java.io.File + +object ScoreProviderContext { + var scoresDir: File? = null + var appContext: Context? = null + + fun init(context: Context, externalFilesDir: File?) { + appContext = context.applicationContext + scoresDir = externalFilesDir?.let { File(it, "scores") } + } +} diff --git a/shared/src/commonMain/composeResources/files/scores/bella_ciao.mxl b/shared/src/commonMain/composeResources/files/scores/bella_ciao.mxl new file mode 100644 index 0000000000000000000000000000000000000000..332356ab6434c7ab6088118ed484bc9fa0a3e05f GIT binary patch literal 10960 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 literal 0 HcmV?d00001 diff --git a/shared/src/commonMain/composeResources/files/scores/canone_pachelbel.mxl b/shared/src/commonMain/composeResources/files/scores/canone_pachelbel.mxl new file mode 100644 index 0000000000000000000000000000000000000000..22269111a5d84b05fd43ebe9baead6e434073958 GIT binary patch literal 6687 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 literal 0 HcmV?d00001 diff --git a/shared/src/commonMain/composeResources/files/scores/greensleeves.mxl b/shared/src/commonMain/composeResources/files/scores/greensleeves.mxl new file mode 100644 index 0000000000000000000000000000000000000000..d67c11fbc66afb333e20b1bc8f2780be409dbfda GIT binary patch literal 4918 zcmZ{ocQ71ox5rnsl~tnmMRZn+B_x(039BwviymclD+nP(i{6QvMDK*yDA8+lt3@wa zL|Y+xk2k-2@4PejoqL}%XU;kE%$)iD^_?>xggPM+9RL6X0#fN=I$v9Tn;ZcEK>FWg z_?wzfkcuL3t*2tvZmynI4z9K@M15SGF`ujYhIItgMTAC%uo(S&ladHs)nWA!?fxP4 zdi~oJ?i00$n)=lm_y|@QJ~X5*QnRXys1_W+iopkm;36dZ31_z*3T0&*i6eQL>(qMs z&!3d~KAL#P85KBiWVM|$cce4)c}%5-2!Qxs>L3$rIX|cYfD9D^0NG!49@cIzZ2uAT z%gh9mFHIAGt+ISI=U?5hTqmc+n~{MmOIj?|)UCaYhhzGzYZEW2p=&)2vD!*n|(s#ru?M;IiX+!H~)FQz87_HazZlIz~o4%I?l-37osscO)>f|#(cPWf7Ade<_m#O z)bm_N-3Z*gU$X*t%*G3bx-%%A-NuwE$<4e&p0s6#5yTv#nMJSIzMIIK7mQ+)kps*3 ze}^=yPG#|gl*(Pg{;=@7?IQ(2DLtom=gsn1jcbOM6Cie~W5XA$y{?^Su z^~vp6oQ9#7SDr8JVwTWRvhw%&VLtewMliK$e$ktR>z=$bphD6hNn4kOf#}t9%4@I1 znk;CHhw;Io9L2^w+x6Wrw-N<&PUDcHZI$_Qr@J4TkDX)>4o4Z8yBFO^>$qEX6sVIVp6sr2sOQ?y;3lE#Ewt4bl zxF;%TonpM&XDM7vwT0VV*F^%EF}AgaKdD?;Y|4u*ani_n<;kttOH!r@tzn|o|Wo+!0t_2(W8s>iY}QeYfz!Z zRaL*qiIj6w2R})OZF#%XJtnB$NB<~`JaH3vcJgNVkrPcDQku>>g6v{AfQCKsEgcER znpmbr2?G*(GL1grJzBLXV{=11xjL77TZJ3RdmTxONG;gqs4(1@hhh#rqbm*1r~0*7 zN-=TRQj+R=vcirj^_eQp_e3S^Ak}8|PQEXjg3d#fyqgyKwKW1?jLzN3^D_(#zO>mz zxs0#z9#4lqzr4oXJrUm6-B#3TSK#)g;PS?V z)DcD`*kS)AEL@Nw9+|FR!NOKi7RMgXMLrh8?qgN);5>9} z?$lTa)Z*^4=WH^+>X6U0biOC)9q-ME{N?Fygy?G>K2icyO`=3bfm}}oL@WvfQ3`n+ zxSM`bV1SaT$-ctzbJNAIbF)ibwE^;mO~&~z-|h)aY6_ZKvJ1N0ee>BHwcanBQaS=r z->*@Yk<(nUp-4Q3OB{qT4<%?P_7x0gFiHPL&aDGj7geo7kw?Pa-kx%mHg$fLD+6Q-!9MO2&Hk~s?}&rk~chS*1G||`Q|{erhS4L`|dz&n>8b< zs6%=&m$ah&uxDfeLKM}8Xeb#I*Gz0&wIpH;n) zubWn=6z+BP?b|efIl78m`(Q+8+uP&S%z_`kTdxVVvSJ?N4Ua09p5`u2u@`}@Y!1oT zcJ$F3n7X;=NY@LdRrnNETJhTWv|CG6>gXvkA}b}pWFN-8Dw@z*O4%fkT+qNL-KQeL zAhsV5x1;<7!fr>{BKE1T`gZ0GqUqzwyA?-uJ}Wom4{t6g|L{I1sE5nfH6)IzCoDZ2E& zJx>=l$W<-C%cVz}%2jPI=_Odb*;idMtJxq1+`s4!T=ierZ)Qh;9xPwf|Tm=;t#A(MNTV)Xn-mQ<X|GNHHf+wGbA%yoYwVu74C-5|f90bH?^5R3@97i_ko3bLR2 z?Y^cjVb;v!;zj%23+Z||E23kjpk&Q<-}Y1m|F01d!7&wJR?8h6k~WshK!je$mG5Xwae5xBg;RaHGR%*-uuDdJevt}sm&1`P4%_% zR5Z)>fhYNG(aMfX;t;hcFOm1&GM4-~nrA*@VvpLJS;VmbQPM-#03?cF;EM_ zD8s`JwR`_Aj-P&KPE~p}S0>OnZrSYd&F|!1+@BUuZYcqi{P9I-5q_6RlbwQX>pVkk zFN7t$o6;e_Kkt~5ois(jRQZu9-|_Qz1wpzZpWTZLJh;T6fkgR*XjMl+4&{n7bE3h% zuqgWFZ*xz7pT!L2b5c?(Ub{dCvwvA_OjH~33fm#1C{SjBlF!UX24a7Uy7YdGWx_m{ z9UzY5O&4T^Z_Ww&SSg$-t81WN#Z8jQ*j5vF7vAH8m@8r9&`#-0m9C?>;DOin%&{XN zPVGHG%Jt@WFDB%AUd~}f`gvSGzXC9omjFt{xGq(IlU8B*GpT+kl@e=d%)&avsa4$} zOqV8&1Agm!=J%q;K5Kj`kX49i{P{)mQk>-qZuU^XBsP%(;uxfp+u!f%Ganq5auujI zRG;(p6SuB8TJHd=FBUDC*xH(rhg7>w)prE6{Kt%0~95dNtrY( zJuc0Ohc%imm9#f^_~UEiDc>b+;AGoR!oVu>*7OH=bcIZDBx?EO_=g|rX*z$H3iE!p z_f-dk4el}Gi1~z#QI$VE!Wp$}c%2~X-59q!tRFr<1aqxFXlK95bf7R%K`oI_P97(L zk;0fFKW_J;<22OSFLp!O>GC2<_p-1`-C{@b_;~ceOlT-2`eac`NU5`FjLNYrHiY1G zox?dR+kKWHh8lRHA-&pH!V-2g~ANe+)3}JNxw%Xiwt;5B)oF7k`V!px%iq|t; zml8q8q0GO+LA9-{yUdU9@lm6r`(ai%8*nN!%oXpXNf!IPf2|)lCWo4RUYn^z1LdJw ziA+XSF`c1tBmpL;sTYa&+}P+LCZ^xu=CSwE*9lI*oT*@=25 zIm*{2qF5=I55hPg?>)#xH)q~$%t@R_%5=&vJ7z~>XvqynGhk0g8soag%piQS574no zm?v{fI}0njER152Kvh1@S$b+(f_^5|^#fK8BA#-j%&nwY@+9iVCq^zz!%BRM94M)^ zgh*o3Chx1eKB7Pgipd9u1hmB;pS84mNC*vDACLw4Sm=u|o29GgCykMLt$wE)Bld?> z941sqcHFpTwzFLsMB_*_8fLAj-m|UkmkJ>%e0FG(+LdoneUCH%%02Gy(+wK5^8v~s zE8U1VS(rKRp8M)7&&77q!yXTkvVB6cH{YP!ft&m31))J{TG+>;Ke)zeGXld&kEGL_ z%TU(YD^7wxn`L1FRRZ{1!7m#u+o)7sK2kY%hBTN$3&_?`1rk@sya_^~%qWW@b@PlX|Nv=D-`mOYNZb zRrgebx1-~pI32#MeLK;5tv>>S72oaX5u#+|@f6+_>pCO8`{d@ubEcdkFc{N#V&gx} zwjOQDrll>8Be~GTrjRv`lF;eH1_LaaHfa zWx$b)VT?0?AJg1qqY{B!5nj83R@il!2I9|g=v&*H0edg8mmLSTb0K`s7z4+3gZh05 zwtqQ@P=)cc;uSPoVEae8w$KL=>UT<=SgJ)i8n{aWgmiJRm-xULLs5Wo%LgZI#-Wmh zuj{u77maW}^-gP~gde%lTK^biVzjEE08U)D^*Em_RVl1c+bo%QmB)+&ylE8C zEp@EJXg>4$%Sdh;AIXw4rG-)f>He<)i4Sh~sfV_7VA|9v!S))xWRg*(%#ZX8%%m0s zMG9sY7|Ovfg`@uMkFV({?^l;~9y6e4Tz@TnrC#(cGO=PaEf(^XC3mYZXK%YP@bUU$ zh*}+z#(kFxwe2itO*2H@A;LG3L0S?G?<>hC8AyV(HgQfKpl3rd{yfxk(&CrPhl|o7 z&gEIHW;tY93_)a79idXa{TOtOrS2Q)+QZqj45yN;4Rr}-Sw0@sYZnTcIbw~?7w?^5 zJN01Qg0@@K2L~&+7VB-Y>a=HHsLY-yKi}|ugt}Ac9>Pus4dN%<+fl4t{mDgRe+;&N zH=V5AAXU~WHFf^Wy8iJiV*U_M*9Yf^YxL&s0F7Mt0YlGV zO<4{vmGl(+>0W!HKg!+dy!BG@q5%9Hjv}`JD-Ea#E+Dp2o^1_f+5tm6DpR_X?8_^9v-zop(XYzD_3wWywI=W8K7l^)@Rqac(ED7z{J5BWVs0!$f>7#ZOTixb zq=qRER`!0^JSTQ*s-;xMcCvdb?Q|62q0l4rG%CsLVvh7{jsSNXfIh?K+>Z#Q(_vm0 z{0ILKEPpvanJ!fJu-Gz%V!ZjxZL;o3QYTq!07&xa>IR`sa0@{A|6{7ZEa<;pV9-D1 q|Atoox#K@!(tmFN00M(P{%!w_mJsU1B>#Nf`Wt?KYaz`))4u@bu{z!W literal 0 HcmV?d00001 diff --git a/shared/src/commonMain/composeResources/files/scores/gymnopedie_satie.mxl b/shared/src/commonMain/composeResources/files/scores/gymnopedie_satie.mxl new file mode 100644 index 0000000000000000000000000000000000000000..ed2e969464f4fc903793c494308aa417e65b6451 GIT binary patch literal 4866 zcmZ{oXD}RI+r|l2FDr-^td@ustFuWEvD!xTnyem)PLzmVR*SL-LIlw}L3FFG5;c1C z5P}tDS6SWb|9*Mj=b7g@zqx15b?%vat}o}DnM)f^Ldrx$L_|S^7N1kGITV(6xmFq1 z#d2MmF#X3O>d#a}tzDcwEbW|MzPa!1vTUYz5Q2&Ui`oN&N2mlv6hqBsWB`eX#IVFfbk`Og zUx|oINJvS`%Kc+)8!?xNm1glDF@glB)~%i~X=3>c-R(;f2aDBprS>-fp7l4|X;OC% z&PV-v&wutwO5(}BR?)VW-BDzzZsFfWf2$=-ZOkdG*K|;umtR=86>8@Qk+i0g>ILFF z3eS(W=%1*bc%2Vzh-O_~UQ}M4`Y0G{g)np9M;jbtD^GrHxh!2A*YA?|Ya3j3bmte> zuaumgOpk}rf-m#~o}w->)Uf(oUY&;Bqcbg?uXJ2pMx$9(8&p>U_AeEqViBi-LdM~9Qg<(OFMQcCXKOpS-YrK7Z`A&pHJ1x<0ba)usT zPD6yY;>})G&6C{vw6HDzftxOZ3S;wGL+5hz-QKYXtJECz#@FT|OxyC6z2VZjZ_O2& zxE$QR9b_K6{y~$a@8$N_{Y^X58d*-&jbMD6*Z;E0r}9$LkE>b#&fw_V!^Hus$v=zO z@8)IX%X5iKLg!(kN&aLyFMGJV5XoY>$WNcV?>Fvgdop{13}g&_iZ}rS{@KOAxt#F! z-`ZY9#|%hIh?W$GsJy<>+AHYcmktk~we|BN(GPP=LpMe9ez51rnZNGy0!XJ|C)nf2}b zr`QcI4C6(M`AHy60C4xA!d#acmiE;p^gN#2*F<)&@NK_QiQ2f9KEm5w{u1jOZIQ=C zWo6J35y2dYTY?Qw!xX)wtk<}CJ$x0!PM7T^Zl&(-K+n_fk8E`v8j~k>k6|?Ggee<} z1CIjcG(Nm?srpqT7OVW_;DL@)BUPs1;(6}uB_Vqj4!KES*V9xRSRizD=hw430F|l; zyrXcFrk&nb3aM2<&U{s8$@N7aKK*ekiNIf_ues>zZs_~D(^_qgQBsOkJ&*9q zWLUx{W75+>VUD32JwUdklqc*_kbV}W=5+<1-{tYMh)gQkmare86H-y7XzJ3{S{LzI zO?j-^gumZBb0AM&k}T^SlzZFs&yf&gKCW54ya$v0kcdx|#^|z3aCOW*uHZ77Tc_Al za+ywnj`{MJOI7-#Q&d8LEuSYrvsIS9!0#a;fZot*7`9}+5F;f;b@A@}l!Sm(WYIXe zfaAv?x;oasFYsq5<1O3~FO&IkT~@P81pKC$f8Hhsxp^cScMDgzq1VczMu9S&3+Qp? zlzLm{nKF7x-)k!K=37O9s!q)Edo5luS>xdrO4Q=sgKl36vj#?vO+>k**Y22Ko`$Y} zV6*j=PgeeD-n%QjW{-Kv&M#C#(Au&EyQ6K$NBmG$sFlCG8~I~3qYmCw_iTg_kZdi4 zX-OzrQ9Uwd`|lhW*?LA$p-^p|)$DW9X~8B|hPwxXdh8$K`r26rVfQrDa!_O zB_Lnn!76%>P}A$bz1eQ}z}uKAxyASJwn_)87d}^Gpu5~H-?`IY8qyyQa+Tl|Yz7L* z<*Ci6zZa-a5M}vD1VklSO)qDT*N4dv?tC}cd{Zna^ullmp1FdU+Iilr=Min}rDG{P zwbH~AC|8uA_cK85{zKyqw#uwjoNWS|0Oj;)nG;r3zl4eW578>Xa#&XBf0 zGQhZ3D$QANJ!qLx2E-#Gvcz;e;qRXw99N7el5gEjKJ5$UXTljUxxK$xMRC+HF>=-) z7(Oxm;ND?XKQxSv#e`vdn`xWppb^$gtVI70#q>%`lEr~c+tuPr5BvQ7Fm@T-leEBZ z-duV&q>1cL-RqXro?w?Ad7hVv{8oDqSN`yscH+Ig&BfVV+jgJ)h<=NAFK$*vOX~Q5 z-;`tz1j$g}2-G;Q>RhR0A}`W!EaSU1-*lick4S|GFN0tB*p05}U{29GTW`18*N{|@ z@=p!(6R7!p#XrcFR~YdJwhl!}>3O)# zEW_*y%ljrM-K$`_q}+}P`Vj&tfT1cq@tOl)7yUl4v6FlngsMKew^8-o3y|}Hkb=zE zbBD2kF{6v>4NB!YnYg(*vc5|j3AlMwg=WrtO7RN{)vM~;AvSI}w_p&^CcD<=vCNhS zTT!?w^3qaH!0eClX#$69jAI8sRPNE#fL{*4EHlAmq+RA8phGeir+p(b#^C2sPC+i8wdv=Q`XK&H z?c@b0>{n7|V&>w=_ur7t?E#}ud60mF7pE17lVLf?f6$sS-&YD)Xhg%qTKJw9py8d{ z;=vXg9K-1pN2X5ub(wJvX4~YWwTtaYFpi`E*MEuE-PlUnQ0p*iPX53c&g2)i7o1!J z1wXb+2&qgEI@Z-=Mz+C`OOugNOn#Adj(n=pm0bxONq@In>Ax>lrwTgu-N6M@vUxP4 zH#y@sw-0Q{KwA&jRHUJ@z0%=E@ZL5^1}k7}6m)xm`6ikTg302i0MOv1tfG>M8PT9o zj;E+Xp+t}K2;+=2_NOR>*dtw=cS1<|231ev5MN2P+tU3TQ`V+dQ&`s@96c)&y~c43 zMZ>~xbn@2``c5FY9oMbNfbk5jOf6S3zTa{oNc7f+V~bsOa1|GL4zz<^*N(;i{44gM zcHjZ@N9zxR&AViCpr+WM({S>MbqXShH^WzFO+zQ^dWkx#&qr>JfBd+xNU#|HLe(;$ zGGKD6oYMqXqiL_SC$hW~{1&KYfxX4`C0!s)&xySSETJomM7^5bCUBFLtCV z?8#G$HHo5SGc7@x_U9?*t}DA{@jKH6y(pjg)6hd~Yi=KRHU?`S-t=JK7xAhc-aJUz za9T}8TD4|dNk?`hmi96>zH}*1MSsU+;b9v?J=CI1n!_q3`i6x{{ffHH`h5OavUPPn zz_6Zj7`$B%8F zvWCnILuSBf9Yx~hfd!gWsG(IYfM} z=TO(YY0a1!X7O|~fxbXcOZ&yXd*J2x3 zUQw)DNPE||$`E!vKEALUG*>*mJiX-ty=-H^)niFTo?cH7b>%+q=jqt=GEW;=u-Gy3 zJBe5$rEq1mwyw5`_RRgccR&pv01q;eWh$3KK;$(JP&}fAJ?z@Fiur{3=HuR3w8k*! zxJ;Q_`|@L(na{fg-joHx0iGfS_7yzElktcxMGhmrylh4vA892*!OA%kO$^XFIv1wc z1V8xz48LorT*dod%7L0^D+cOaV?j-H(0qEV8v&s@LQuy=ik8!8{J<-D2Pdz;TEpY= zprZoKm!b8aPN?Nm$v$jb&kH4-Ai@n?c>{3=qbaOm);)@z(?4I%VF)MnQL)(is7+W& z5#MrkvA-mHJ}}l-HZA``uOtcft>39;h3lD}A5HnpSxhX+(@M+THgwQr+i7C}XH`JQ zN_EQZ+$~YbAv|4nhRN&9--4$)&NoiupTwD(=B@~^7ruI0-sv5+Pd(wh^>^$3QT8xu zE0yuYV3>qnGs}E@8p?nM5sy)N3C8OF3L^as{qZ1;n7MR+{Fv3Y#wje8gyvJL9JF95 z+fq(YSeRSf*+ZyF`;K%Db>$$*HlAXnCC~Nu;!M`J7V-~R*lL(56|E>t?q>_?_Prn+c4s;|hu_bD*|GzZ zzJwHBf9H3e({TU&ZWti|uAQ``eCB_+t>=n`-x0p+IgfQ>j0o248mh`pGKH=TSYaBI zbOW30#L-)gajL0|VSJa^vv2vK&WWBMz zuci9~MUM~089O&@BV6)zoI-k45Rhq<*-h}!HVif83P_x(F=_#@r?Ly?wu}}?NJ8k% zG-c_tYXOFviuXdl%lwb&epXy4a&*OP``8>&XJ?% za1i~m-l@71;glCIePNILj@zjKNU}BF(#SwK$9+(gI9Vd zk@QoU;*Ft{m$D<<-S~V_u@Jhk*DTjJH3$ZL!aH;%Fkh~)NVzEwG@VkGCdzAUSF7|wDZc2})zCvoO{B9?GGnb}Qen9~(WV%& z%)QRoJZsaZ(G7sk9W7<$CX82m?E7DOc0c_C4g3MsJUVKbv+p7Jg9(wFAxlAbH`CuK zJ4lO_bL@{u--z1FwRL-Ta(p}DHQisgJD5UdvYjPpLmoBLJz9S0OR5`y{juapjmq)w ztEus7YD{+`C1)3IOaow!4`7m3Gm1_mOt@95=Mt7F;1K%3#Or6I#s<+B;uq?-G!c{j zIi6?94etaj3BEq4&-CM_8yKVgkP4t1u=iGE7;7w04l0-GFv^_~;+KFZEe=V15gQk5 z3PV=F#5yreM3Vd-ttRxy_N5q?`2bRH*-fyYAAju`QyL|%A>NINV#eHdA{#U z)p2y@!0(d#=t(aE;vkT*>=<%-fvxO5ydu$t6W<^r`tNDeb(-`4)_=~P{(Z*3$3g!$ aAoqIvui=n3oDA?!|Hd^z|8yDuQU3$~SW`Iw literal 0 HcmV?d00001 diff --git a/shared/src/commonMain/composeResources/files/scores/happy_birthday.mxl b/shared/src/commonMain/composeResources/files/scores/happy_birthday.mxl new file mode 100644 index 0000000000000000000000000000000000000000..c86348d7cb0ab39eefb052026e7d368a2e968d0e GIT binary patch literal 2274 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 Dx+w-j literal 0 HcmV?d00001 diff --git a/shared/src/commonMain/composeResources/files/scores/minuetto_bach.mxl b/shared/src/commonMain/composeResources/files/scores/minuetto_bach.mxl new file mode 100644 index 0000000000000000000000000000000000000000..dd7f1b37b37d8a031a71379b4ee1bb4ceaddd968 GIT binary patch literal 4745 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 SqWygU`RV+-Mj_4L-v0oZRhxzY literal 0 HcmV?d00001 diff --git a/shared/src/commonMain/composeResources/files/scores/ode_to_joy.mxl b/shared/src/commonMain/composeResources/files/scores/ode_to_joy.mxl new file mode 100644 index 0000000000000000000000000000000000000000..166d9f52d1e4f9e684bc681467f56b30a8df9db1 GIT binary patch literal 3209 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 E05;3_TL1t6 literal 0 HcmV?d00001 diff --git a/shared/src/commonMain/composeResources/files/scores/preludio_bach.mxl b/shared/src/commonMain/composeResources/files/scores/preludio_bach.mxl new file mode 100644 index 0000000000000000000000000000000000000000..134e9d1555ca324530b71ced0f6a1c3c598c6780 GIT binary patch literal 7726 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 Ls5+YdpWgoi2$com literal 0 HcmV?d00001 diff --git a/shared/src/commonMain/kotlin/com/notemusicali/scores/BundledScores.kt b/shared/src/commonMain/kotlin/com/notemusicali/scores/BundledScores.kt new file mode 100644 index 0000000..5e452c6 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/notemusicali/scores/BundledScores.kt @@ -0,0 +1,47 @@ +package com.notemusicali.scores + +import notemusicali.shared.generated.resources.Res +import org.jetbrains.compose.resources.ExperimentalResourceApi + +/** + * Curated set of scores shipped inside the app as compose resources, so every + * platform has content available even before the user picks a folder. + */ +object BundledScores { + + private const val PREFIX = "bundled:" + + // Resource file name (under files/scores/) to display name + private val manifest = listOf( + "happy_birthday.mxl" to "Happy Birthday", + "ode_to_joy.mxl" to "Inno alla Gioia — Beethoven", + "fur_elise.mxl" to "Per Elisa — Beethoven", + "minuetto_bach.mxl" to "Minuetto in Sol — Bach", + "preludio_bach.mxl" to "Preludio in Do — Bach", + "canone_pachelbel.mxl" to "Canone in Re — Pachelbel", + "gymnopedie_satie.mxl" to "Gymnopédie n.1 — Satie", + "notturno_chopin.mxl" to "Notturno op.9 n.2 — Chopin", + "lago_dei_cigni.mxl" to "Il Lago dei Cigni — Čajkovskij", + "greensleeves.mxl" to "Greensleeves", + "carol_of_the_bells.mxl" to "Carol of the Bells", + "bella_ciao.mxl" to "Bella Ciao", + ) + + val entries: List = manifest.map { (file, displayName) -> + ScoreEntry(name = displayName, path = PREFIX + file, isDirectory = false) + } + + fun isBundled(path: String): Boolean = path.startsWith(PREFIX) + + @OptIn(ExperimentalResourceApi::class) + suspend fun read(path: String): ByteArray? { + if (!isBundled(path)) return null + val file = path.removePrefix(PREFIX) + if (manifest.none { it.first == file }) return null + return try { + Res.readBytes("files/scores/$file") + } catch (_: Throwable) { + null + } + } +} diff --git a/shared/src/commonMain/kotlin/com/notemusicali/scores/MxlReader.kt b/shared/src/commonMain/kotlin/com/notemusicali/scores/MxlReader.kt new file mode 100644 index 0000000..8e2cbd9 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/notemusicali/scores/MxlReader.kt @@ -0,0 +1,3 @@ +package com.notemusicali.scores + +expect fun extractMusicXmlFromBytes(bytes: ByteArray): String? diff --git a/shared/src/commonMain/kotlin/com/notemusicali/scores/ScoreEntry.kt b/shared/src/commonMain/kotlin/com/notemusicali/scores/ScoreEntry.kt new file mode 100644 index 0000000..286f9f8 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/notemusicali/scores/ScoreEntry.kt @@ -0,0 +1,7 @@ +package com.notemusicali.scores + +data class ScoreEntry( + val name: String, + val path: String, + val isDirectory: Boolean, +) diff --git a/shared/src/commonMain/kotlin/com/notemusicali/scores/ScoreProvider.kt b/shared/src/commonMain/kotlin/com/notemusicali/scores/ScoreProvider.kt new file mode 100644 index 0000000..616c45a --- /dev/null +++ b/shared/src/commonMain/kotlin/com/notemusicali/scores/ScoreProvider.kt @@ -0,0 +1,8 @@ +package com.notemusicali.scores + +expect object ScoreProvider { + fun listScores(relativePath: String = ""): List + fun readScoreBytes(path: String): ByteArray? + fun listFromUri(treeUri: String, documentId: String = ""): List + fun readFromUri(treeUri: String, documentId: String): ByteArray? +} diff --git a/shared/src/commonMain/kotlin/com/notemusicali/ui/scores/ScoresScreen.kt b/shared/src/commonMain/kotlin/com/notemusicali/ui/scores/ScoresScreen.kt new file mode 100644 index 0000000..4918973 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/notemusicali/ui/scores/ScoresScreen.kt @@ -0,0 +1,249 @@ +package com.notemusicali.ui.scores + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Folder +import androidx.compose.material.icons.outlined.MusicNote +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.notemusicali.music.MusicXmlParser +import com.notemusicali.music.NoteSequence +import com.notemusicali.scan.AppSettings +import com.notemusicali.scores.BundledScores +import com.notemusicali.scores.ScoreEntry +import com.notemusicali.scores.ScoreProvider +import com.notemusicali.scores.extractMusicXmlFromBytes +import com.notemusicali.ui.components.BackTopBar +import notemusicali.shared.generated.resources.Res +import notemusicali.shared.generated.resources.* +import org.jetbrains.compose.resources.stringResource +import com.notemusicali.ui.components.GradientCard +import com.notemusicali.ui.components.rememberFolderPickerLauncher +import com.notemusicali.ui.theme.CardGradients +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Composable +fun ScoresScreen( + onBack: () -> Unit, + onScoreSelected: (NoteSequence) -> Unit, +) { + var externalUri by remember { + mutableStateOf(AppSettings.getString("scores_folder_uri")?.ifEmpty { null }) + } + var externalName by remember { + mutableStateOf(AppSettings.getString("scores_folder_name") ?: "") + } + var pathStack by remember { mutableStateOf(listOf()) } + val currentPath = pathStack.lastOrNull() ?: "" + + val entries = remember(externalUri, currentPath) { + if (externalUri != null) { + ScoreProvider.listFromUri(externalUri!!, currentPath) + } else { + // App source: curated bundled scores first, then any file the user + // dropped in the app's scores directory + val bundled = if (currentPath.isEmpty()) BundledScores.entries else emptyList() + bundled + ScoreProvider.listScores(currentPath) + } + } + + var loadingPath by remember { mutableStateOf(null) } + val scope = rememberCoroutineScope() + + val folderPicker = rememberFolderPickerLauncher { uri, name -> + externalUri = uri + externalName = name + pathStack = emptyList() + AppSettings.putString("scores_folder_uri", uri) + AppSettings.putString("scores_folder_name", name) + } + + val scoresTitle = stringResource(Res.string.scores_title) + val title = if (pathStack.isEmpty()) { + if (externalUri != null) externalName.ifEmpty { scoresTitle } else scoresTitle + } else { + val last = pathStack.last() + last.substringAfterLast("/").substringAfterLast(":").ifEmpty { last } + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter, + ) { + Column(modifier = Modifier.widthIn(max = 600.dp).fillMaxHeight()) { + BackTopBar( + title = title, + onBack = { + if (pathStack.isNotEmpty()) { + pathStack = pathStack.dropLast(1) + } else { + onBack() + } + }, + contentColor = MaterialTheme.colorScheme.primary, + ) + + // Source selector + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = if (externalUri != null) stringResource(Res.string.folder_named, externalName) else stringResource(Res.string.folder_app), + fontSize = 12.sp, + color = Color.White.copy(alpha = 0.4f), + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + TextButton(onClick = { folderPicker() }) { + Icon( + Icons.Outlined.Folder, + contentDescription = stringResource(Res.string.change_folder), + modifier = Modifier.size(16.dp), + tint = Color.White.copy(alpha = 0.6f), + ) + Spacer(modifier = Modifier.width(4.dp)) + Text(stringResource(Res.string.change), fontSize = 12.sp, color = Color.White.copy(alpha = 0.6f)) + } + if (externalUri != null) { + TextButton(onClick = { + externalUri = null + externalName = "" + pathStack = emptyList() + AppSettings.putString("scores_folder_uri", "") + AppSettings.putString("scores_folder_name", "") + }) { + Text("Reset", fontSize = 12.sp, color = Color.White.copy(alpha = 0.6f)) + } + } + } + + if (entries.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(Res.string.no_scores_found), + fontSize = 16.sp, + color = Color.White.copy(alpha = 0.5f), + textAlign = TextAlign.Center, + ) + } + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(vertical = 16.dp), + ) { + items(entries) { entry -> + ScoreEntryCard( + entry = entry, + isLoading = loadingPath == entry.path, + onClick = { + if (entry.isDirectory) { + pathStack = pathStack + entry.path + } else if (loadingPath == null) { + loadingPath = entry.path + scope.launch { + val sequence = withContext(Dispatchers.Default) { + val bytes = when { + BundledScores.isBundled(entry.path) -> BundledScores.read(entry.path) + externalUri != null -> ScoreProvider.readFromUri(externalUri!!, entry.path) + else -> ScoreProvider.readScoreBytes(entry.path) + } + bytes ?: return@withContext null + val xml = extractMusicXmlFromBytes(bytes) + ?: return@withContext null + val seq = MusicXmlParser.parse(xml, entry.name) + if (seq.notes.isNotEmpty()) seq else null + } + loadingPath = null + if (sequence != null) { + onScoreSelected(sequence) + } + } + } + }, + ) + } + } + } + } + } +} + +@Composable +private fun ScoreEntryCard( + entry: ScoreEntry, + isLoading: Boolean, + onClick: () -> Unit, +) { + GradientCard( + gradient = if (entry.isDirectory) CardGradients.scores else CardGradients.scoreFile, + onClick = onClick, + modifier = Modifier.fillMaxWidth(), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(28.dp), + color = Color.White.copy(alpha = 0.8f), + strokeWidth = 2.dp, + ) + } else { + Icon( + imageVector = if (entry.isDirectory) Icons.Outlined.Folder else Icons.Outlined.MusicNote, + contentDescription = if (entry.isDirectory) stringResource(Res.string.folder) else stringResource(Res.string.score_file), + modifier = Modifier.size(28.dp), + tint = Color.White.copy(alpha = 0.7f), + ) + } + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = entry.name, + fontSize = 16.sp, + fontWeight = if (entry.isDirectory) FontWeight.SemiBold else FontWeight.Normal, + color = Color.White, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } +} diff --git a/shared/src/commonTest/kotlin/com/notemusicali/scores/BundledScoresTest.kt b/shared/src/commonTest/kotlin/com/notemusicali/scores/BundledScoresTest.kt new file mode 100644 index 0000000..2e387d1 --- /dev/null +++ b/shared/src/commonTest/kotlin/com/notemusicali/scores/BundledScoresTest.kt @@ -0,0 +1,43 @@ +package com.notemusicali.scores + +import com.notemusicali.music.MusicXmlParser +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class BundledScoresTest { + + @Test + fun `manifest lists bundled scores as non-directory entries with bundled prefix`() { + val entries = BundledScores.entries + assertTrue(entries.size >= 10, "expected a curated set of scores, got ${entries.size}") + entries.forEach { entry -> + assertTrue(BundledScores.isBundled(entry.path), "path ${entry.path} must carry the bundled prefix") + assertTrue(!entry.isDirectory, "bundled scores are plain files") + assertTrue(entry.name.isNotBlank() && !entry.name.contains('_'), "display name should be human readable, got ${entry.name}") + } + } + + @Test + fun `non-bundled paths are not recognized as bundled`() { + assertTrue(!BundledScores.isBundled("/storage/scores/foo.mxl")) + assertTrue(!BundledScores.isBundled("")) + } + + @Test + fun `bundled score can be read and parsed into notes`() = runTest { + val entry = BundledScores.entries.first() + val bytes = BundledScores.read(entry.path) + assertNotNull(bytes, "bundled score ${entry.path} must be readable") + val xml = extractMusicXmlFromBytes(bytes) + assertNotNull(xml, "bundled score must contain MusicXML") + val sequence = MusicXmlParser.parse(xml, entry.name) + assertTrue(sequence.notes.isNotEmpty(), "parsed sequence must contain notes") + } + + @Test + fun `reading an unknown bundled path returns null`() = runTest { + assertTrue(BundledScores.read("bundled:does_not_exist.mxl") == null) + } +} diff --git a/shared/src/commonTest/kotlin/com/notemusicali/scores/MxlReaderTest.kt b/shared/src/commonTest/kotlin/com/notemusicali/scores/MxlReaderTest.kt new file mode 100644 index 0000000..e2b6cfd --- /dev/null +++ b/shared/src/commonTest/kotlin/com/notemusicali/scores/MxlReaderTest.kt @@ -0,0 +1,47 @@ +package com.notemusicali.scores + +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +@OptIn(ExperimentalEncodingApi::class) +class MxlReaderTest { + + // Both archives contain META-INF/container.xml plus score.xml (a minimal + // score-partwise document with one C4 quarter note). + private val storedMxl = Base64.decode( + "UEsDBBQAAAAAAAAAIVxZbzWxZAAAAGQAAAAWAAAATUVUQS1JTkYvY29udGFpbmVyLnhtbDw/eG1sIHZlcnNpb249IjEuMCI/Pjxjb250YWluZXI+PHJvb3RmaWxlcz48cm9vdGZpbGUgZnVsbC1wYXRoPSJzY29yZS54bWwiLz48L3Jvb3RmaWxlcz48L2NvbnRhaW5lcj5QSwMEFAAAAAAAAAAhXK2hgKfKAAAAygAAAAkAAABzY29yZS54bWw8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCI/PjxzY29yZS1wYXJ0d2lzZT48cGFydCBpZD0iUDEiPjxtZWFzdXJlIG51bWJlcj0iMSI+PG5vdGU+PHBpdGNoPjxzdGVwPkM8L3N0ZXA+PG9jdGF2ZT40PC9vY3RhdmU+PC9waXRjaD48dHlwZT5xdWFydGVyPC90eXBlPjwvbm90ZT48L21lYXN1cmU+PC9wYXJ0Pjwvc2NvcmUtcGFydHdpc2U+UEsBAhQDFAAAAAAAAAAhXFlvNbFkAAAAZAAAABYAAAAAAAAAAAAAAIABAAAAAE1FVEEtSU5GL2NvbnRhaW5lci54bWxQSwECFAMUAAAAAAAAACFcraGAp8oAAADKAAAACQAAAAAAAAAAAAAAgAGYAAAAc2NvcmUueG1sUEsFBgAAAAACAAIAewAAAIkBAAAAAA==", + ) + + private val deflatedMxl = Base64.decode( + "UEsDBBQAAAAIAAAAIVxZbzWxTAAAAGQAAAAWAAAATUVUQS1JTkYvY29udGFpbmVyLnhtbLOxr8jNUShLLSrOzM+zVTLUM1Cyt7NJzs8rSczMSy2ysynKzy9Jy8xJLUYwFdJKc3J0CxJLMmyVipPzi1L1gGYo6dvZ6CMp1keYAQBQSwMEFAAAAAgAAAAhXK2hgKeQAAAAygAAAAkAAABzY29yZS54bWxdjjsOAjEMRK8SpV/CShQUjrdAoqaAA4SsBZHIh3wWuD3Jkopqxvb42TC97YMtFJPxTvJxs+WMnPazcTfJL+fjsOcTQtI+0hBUzC+TCKE5ZmbJTyNHsKRSicRcsVeKlVJ7zueWM1nf63qmgAcQq4LXWS2EOxDdgei5/AmEz1LhFEGsFYgfSfQjLVznVf5++gJQSwECFAMUAAAACAAAACFcWW81sUwAAABkAAAAFgAAAAAAAAAAAAAAgAEAAAAATUVUQS1JTkYvY29udGFpbmVyLnhtbFBLAQIUAxQAAAAIAAAAIVytoYCnkAAAAMoAAAAJAAAAAAAAAAAAAACAAYAAAABzY29yZS54bWxQSwUGAAAAAAIAAgB7AAAANwEAAAAA", + ) + + @Test + fun `extracts score xml from stored mxl archive skipping META-INF`() { + val xml = extractMusicXmlFromBytes(storedMxl) + assertTrue(xml != null && xml.contains(""), "expected score XML, got: $xml") + assertTrue(!xml!!.contains(""), "META-INF/container.xml must be skipped") + } + + @Test + fun `extracts score xml from deflate-compressed mxl archive`() { + val xml = extractMusicXmlFromBytes(deflatedMxl) + assertTrue(xml != null && xml.contains("4"), "expected score XML, got: $xml") + } + + @Test + fun `passes plain xml through unchanged`() { + val plain = "" + assertEquals(plain, extractMusicXmlFromBytes(plain.encodeToByteArray())) + } + + @Test + fun `returns null for corrupt zip data`() { + val corrupt = byteArrayOf(0x50, 0x4B, 0x03, 0x04, 1, 2, 3, 4, 5) + assertNull(extractMusicXmlFromBytes(corrupt)) + } +} diff --git a/shared/src/desktopMain/kotlin/com/notemusicali/scores/MxlReader.desktop.kt b/shared/src/desktopMain/kotlin/com/notemusicali/scores/MxlReader.desktop.kt new file mode 100644 index 0000000..29d3b6e --- /dev/null +++ b/shared/src/desktopMain/kotlin/com/notemusicali/scores/MxlReader.desktop.kt @@ -0,0 +1,23 @@ +package com.notemusicali.scores + +import java.util.zip.ZipInputStream + +actual fun extractMusicXmlFromBytes(bytes: ByteArray): String? { + if (bytes.size >= 4 && bytes[0] == 0x50.toByte() && bytes[1] == 0x4B.toByte()) { + return try { + ZipInputStream(bytes.inputStream()).use { zis -> + var entry = zis.nextEntry + while (entry != null) { + if (entry.name.endsWith(".xml") && !entry.name.startsWith("META-INF")) { + return@use zis.bufferedReader().readText() + } + entry = zis.nextEntry + } + null + } + } catch (_: Exception) { + null + } + } + return bytes.decodeToString() +} diff --git a/shared/src/desktopMain/kotlin/com/notemusicali/scores/ScoreProvider.desktop.kt b/shared/src/desktopMain/kotlin/com/notemusicali/scores/ScoreProvider.desktop.kt new file mode 100644 index 0000000..ef29902 --- /dev/null +++ b/shared/src/desktopMain/kotlin/com/notemusicali/scores/ScoreProvider.desktop.kt @@ -0,0 +1,64 @@ +package com.notemusicali.scores + +actual object ScoreProvider { + private val scoresDir: java.io.File? by lazy { + val userHome = System.getProperty("user.home") + val dir = java.io.File(userHome, "NoteMusicali/scores") + if (dir.exists()) dir else null + } + + actual fun listScores(relativePath: String): List { + val baseDir = scoresDir ?: return emptyList() + val dir = if (relativePath.isEmpty()) baseDir + else java.io.File(baseDir, relativePath) + if (!dir.exists() || !dir.isDirectory) return emptyList() + + return dir.listFiles() + ?.filter { it.isDirectory || it.extension.lowercase() in listOf("mxl", "xml", "musicxml") } + ?.sortedWith(compareBy { !it.isDirectory }.thenBy { it.name.lowercase() }) + ?.map { file -> + ScoreEntry( + name = if (file.isDirectory) file.name else file.nameWithoutExtension, + path = if (relativePath.isEmpty()) file.name + else "$relativePath/${file.name}", + isDirectory = file.isDirectory, + ) + } + ?: emptyList() + } + + actual fun readScoreBytes(path: String): ByteArray? { + val baseDir = scoresDir ?: return null + val file = java.io.File(baseDir, path) + if (!file.exists()) return null + return try { + file.readBytes() + } catch (_: Exception) { + null + } + } + + actual fun listFromUri(treeUri: String, documentId: String): List { + val baseDir = java.io.File(treeUri) + val dir = if (documentId.isEmpty()) baseDir else java.io.File(baseDir, documentId) + if (!dir.exists() || !dir.isDirectory) return emptyList() + + return dir.listFiles() + ?.filter { it.isDirectory || it.extension.lowercase() in listOf("mxl", "xml", "musicxml") } + ?.sortedWith(compareBy { !it.isDirectory }.thenBy { it.name.lowercase() }) + ?.map { file -> + ScoreEntry( + name = if (file.isDirectory) file.name else file.nameWithoutExtension, + path = if (documentId.isEmpty()) file.name else "$documentId/${file.name}", + isDirectory = file.isDirectory, + ) + } + ?: emptyList() + } + + actual fun readFromUri(treeUri: String, documentId: String): ByteArray? { + val file = java.io.File(treeUri, documentId) + if (!file.exists()) return null + return try { file.readBytes() } catch (_: Exception) { null } + } +} diff --git a/shared/src/iosMain/kotlin/com/notemusicali/scores/MxlReader.ios.kt b/shared/src/iosMain/kotlin/com/notemusicali/scores/MxlReader.ios.kt new file mode 100644 index 0000000..5fdb79a --- /dev/null +++ b/shared/src/iosMain/kotlin/com/notemusicali/scores/MxlReader.ios.kt @@ -0,0 +1,124 @@ +package com.notemusicali.scores + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.sizeOf +import kotlinx.cinterop.toKString +import kotlinx.cinterop.usePinned +import platform.zlib.Z_FINISH +import platform.zlib.Z_OK +import platform.zlib.Z_STREAM_END +import platform.zlib.inflate +import platform.zlib.inflateEnd +import platform.zlib.inflateInit2_ +import platform.zlib.z_stream +import platform.zlib.zlibVersion + +actual fun extractMusicXmlFromBytes(bytes: ByteArray): String? { + if (bytes.size >= 4 && bytes[0] == 0x50.toByte() && bytes[1] == 0x4B.toByte()) { + return try { + extractFromZip(bytes) + } catch (_: Throwable) { + null + } + } + return bytes.decodeToString() +} + +private fun readU16(b: ByteArray, off: Int): Int = + (b[off].toInt() and 0xFF) or ((b[off + 1].toInt() and 0xFF) shl 8) + +private fun readU32(b: ByteArray, off: Int): Long = + (b[off].toLong() and 0xFF) or + ((b[off + 1].toLong() and 0xFF) shl 8) or + ((b[off + 2].toLong() and 0xFF) shl 16) or + ((b[off + 3].toLong() and 0xFF) shl 24) + +private const val EOCD_SIGNATURE = 0x06054b50L +private const val CENTRAL_DIR_SIGNATURE = 0x02014b50L +private const val EOCD_MIN_SIZE = 22 +private const val LOCAL_HEADER_SIZE = 30 +private const val METHOD_STORED = 0 +private const val METHOD_DEFLATED = 8 + +/** + * Minimal ZIP reader driven by the central directory: returns the content of the + * first .xml entry outside META-INF, mirroring the JVM implementations. + */ +private fun extractFromZip(bytes: ByteArray): String? { + // The End Of Central Directory record sits at the end of the file, possibly + // followed by a comment of up to 65535 bytes. + var eocd = -1 + var i = bytes.size - EOCD_MIN_SIZE + val stop = maxOf(0, bytes.size - EOCD_MIN_SIZE - 0xFFFF) + while (i >= stop) { + if (readU32(bytes, i) == EOCD_SIGNATURE) { + eocd = i + break + } + i-- + } + if (eocd < 0) return null + + val entryCount = readU16(bytes, eocd + 10) + var offset = readU32(bytes, eocd + 16).toInt() + + repeat(entryCount) { + if (offset + 46 > bytes.size || readU32(bytes, offset) != CENTRAL_DIR_SIGNATURE) return null + val method = readU16(bytes, offset + 10) + val compressedSize = readU32(bytes, offset + 20).toInt() + val uncompressedSize = readU32(bytes, offset + 24).toInt() + val nameLen = readU16(bytes, offset + 28) + val extraLen = readU16(bytes, offset + 30) + val commentLen = readU16(bytes, offset + 32) + val localHeaderOffset = readU32(bytes, offset + 42).toInt() + val name = bytes.decodeToString(offset + 46, offset + 46 + nameLen) + offset += 46 + nameLen + extraLen + commentLen + + if (name.endsWith(".xml") && !name.startsWith("META-INF")) { + // Name/extra field lengths in the local header can differ from the + // central directory ones, so the data offset must be re-derived here. + val lhNameLen = readU16(bytes, localHeaderOffset + 26) + val lhExtraLen = readU16(bytes, localHeaderOffset + 28) + val dataStart = localHeaderOffset + LOCAL_HEADER_SIZE + lhNameLen + lhExtraLen + if (dataStart + compressedSize > bytes.size) return null + val data = bytes.copyOfRange(dataStart, dataStart + compressedSize) + return when (method) { + METHOD_STORED -> data.decodeToString() + METHOD_DEFLATED -> inflateRaw(data, uncompressedSize)?.decodeToString() + else -> null + } + } + } + return null +} + +@OptIn(ExperimentalForeignApi::class) +private fun inflateRaw(compressed: ByteArray, uncompressedSize: Int): ByteArray? { + if (uncompressedSize <= 0 || compressed.isEmpty()) return null + val output = ByteArray(uncompressedSize) + return memScoped { + val stream = alloc() + // Negative windowBits selects raw deflate (no zlib header), as used by ZIP + val init = inflateInit2_(stream.ptr, -15, zlibVersion()?.toKString(), sizeOf().toInt()) + if (init != Z_OK) return@memScoped null + try { + compressed.usePinned { inPin -> + output.usePinned { outPin -> + stream.next_in = inPin.addressOf(0).reinterpret() + stream.avail_in = compressed.size.toUInt() + stream.next_out = outPin.addressOf(0).reinterpret() + stream.avail_out = uncompressedSize.toUInt() + if (inflate(stream.ptr, Z_FINISH) != Z_STREAM_END) return@memScoped null + } + } + output + } finally { + inflateEnd(stream.ptr) + } + } +} diff --git a/shared/src/iosMain/kotlin/com/notemusicali/scores/ScoreProvider.ios.kt b/shared/src/iosMain/kotlin/com/notemusicali/scores/ScoreProvider.ios.kt new file mode 100644 index 0000000..d1b0bc9 --- /dev/null +++ b/shared/src/iosMain/kotlin/com/notemusicali/scores/ScoreProvider.ios.kt @@ -0,0 +1,141 @@ +package com.notemusicali.scores + +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.ObjCObjectVar +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.usePinned +import platform.Foundation.NSData +import platform.Foundation.NSError +import platform.Foundation.NSFileManager +import platform.Foundation.NSFileType +import platform.Foundation.NSFileTypeDirectory +import platform.Foundation.NSURL +import platform.Foundation.dataWithBytes +import platform.Foundation.dataWithContentsOfFile +import platform.posix.memcpy +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +actual object ScoreProvider { + + private val allowedExtensions = listOf("mxl", "xml", "musicxml") + + actual fun listScores(relativePath: String): List { + return emptyList() + } + + actual fun readScoreBytes(path: String): ByteArray? { + return null + } + + @OptIn(ExperimentalForeignApi::class) + actual fun listFromUri(treeUri: String, documentId: String): List { + val rootUrl = resolveBookmark(treeUri) ?: return emptyList() + val accessing = rootUrl.startAccessingSecurityScopedResource() + try { + val rootPath = rootUrl.path ?: return emptyList() + val dirPath = if (documentId.isEmpty()) rootPath else "$rootPath/$documentId" + + val fm = NSFileManager.defaultManager + val contents = fm.contentsOfDirectoryAtPath(dirPath, error = null) ?: return emptyList() + + val results = mutableListOf() + for (item in contents) { + val name = item as? String ?: continue + if (name.startsWith(".")) continue + + val fullPath = "$dirPath/$name" + val isDir = isDirectoryAtPath(fullPath) + val ext = name.substringAfterLast(".", "").lowercase() + + if (isDir || ext in allowedExtensions) { + val relativePath = if (documentId.isEmpty()) name else "$documentId/$name" + results.add( + ScoreEntry( + name = if (isDir) name else name.substringBeforeLast("."), + path = relativePath, + isDirectory = isDir, + ) + ) + } + } + return results.sortedWith( + compareBy { !it.isDirectory }.thenBy { it.name.lowercase() } + ) + } finally { + if (accessing) rootUrl.stopAccessingSecurityScopedResource() + } + } + + @OptIn(ExperimentalForeignApi::class) + actual fun readFromUri(treeUri: String, documentId: String): ByteArray? { + val rootUrl = resolveBookmark(treeUri) ?: return null + val accessing = rootUrl.startAccessingSecurityScopedResource() + try { + val rootPath = rootUrl.path ?: return null + val filePath = "$rootPath/$documentId" + val data = NSData.dataWithContentsOfFile(filePath) ?: return null + return data.toByteArray() + } finally { + if (accessing) rootUrl.stopAccessingSecurityScopedResource() + } + } + + @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class, ExperimentalEncodingApi::class) + private fun resolveBookmark(treeUri: String): NSURL? { + if (!treeUri.startsWith("bookmark:")) return null + val base64 = treeUri.removePrefix("bookmark:") + val bytes = try { + Base64.decode(base64) + } catch (_: Exception) { + return null + } + val bookmarkData = bytes.toNSData() + return try { + memScoped { + val errorPtr = alloc>() + NSURL( + byResolvingBookmarkData = bookmarkData, + options = 0u, + relativeToURL = null, + bookmarkDataIsStale = null, + error = errorPtr.ptr, + ) + } + } catch (_: Exception) { + null + } + } + + @OptIn(ExperimentalForeignApi::class) + private fun isDirectoryAtPath(path: String): Boolean { + val attrs = NSFileManager.defaultManager.attributesOfItemAtPath(path, error = null) + ?: return false + return attrs[NSFileType] == NSFileTypeDirectory + } + + @OptIn(ExperimentalForeignApi::class) + private fun NSData.toByteArray(): ByteArray { + val size = length.toInt() + if (size == 0) return ByteArray(0) + val src = this.bytes ?: return ByteArray(0) + return ByteArray(size).apply { + usePinned { pinned -> + memcpy(pinned.addressOf(0), src, this@toByteArray.length) + } + } + } + + @OptIn(ExperimentalForeignApi::class) + private fun ByteArray.toNSData(): NSData { + if (isEmpty()) return NSData() + return usePinned { pinned -> + NSData.dataWithBytes(pinned.addressOf(0), size.toULong()) + ?: NSData() + } + } +}