├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── composeApp ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── com │ │ │ └── moriafly │ │ │ └── salt │ │ │ └── audiotag │ │ │ ├── MainActivity.kt │ │ │ └── util │ │ │ └── DialogUtil.android.kt │ └── res │ │ ├── drawable │ │ └── ic_launcher_foreground.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-zh │ │ └── strings.xml │ │ └── values │ │ ├── strings.xml │ │ └── themes.xml │ ├── commonMain │ ├── composeResources │ │ └── drawable │ │ │ └── compose-multiplatform.xml │ └── kotlin │ │ └── com │ │ └── moriafly │ │ └── salt │ │ └── audiotag │ │ ├── MainActivityContent.kt │ │ ├── ui │ │ ├── icon │ │ │ ├── More.kt │ │ │ ├── SaltAudioTagIcons.kt │ │ │ └── Wrench.kt │ │ ├── navigation │ │ │ ├── AppNavigation.kt │ │ │ └── ScreenRoute.kt │ │ ├── screen │ │ │ ├── audiotag │ │ │ │ ├── AudioTagScreen.kt │ │ │ │ ├── AudioTagUiState.kt │ │ │ │ ├── AudioTagViewModel.kt │ │ │ │ ├── MetadataItem.kt │ │ │ │ ├── MetadataItemPopup.kt │ │ │ │ └── StreaminfoPanel.kt │ │ │ ├── basic │ │ │ │ └── BasicScreen.kt │ │ │ └── main │ │ │ │ └── MainScreen.kt │ │ └── theme │ │ │ └── AppTheme.kt │ │ └── util │ │ └── DialogUtil.kt │ ├── desktopMain │ └── kotlin │ │ └── com │ │ └── moriafly │ │ └── salt │ │ └── audiotag │ │ ├── Main.kt │ │ └── util │ │ └── DialogUtil.desktop.kt │ └── iosMain │ └── kotlin │ └── com │ └── moriafly │ └── salt │ └── audiotag │ └── MainViewController.kt ├── core ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── moriafly │ │ └── salt │ │ └── audiotag │ │ └── SaltAudioTag.android.kt │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── moriafly │ │ └── salt │ │ └── audiotag │ │ ├── SaltAudioTag.kt │ │ ├── UnstableSaltAudioTagApi.kt │ │ ├── format │ │ └── flac │ │ │ ├── Flac.kt │ │ │ ├── FlacReader.kt │ │ │ ├── FlacWriter.kt │ │ │ └── MetadataBlock.kt │ │ ├── rw │ │ ├── CanWrite.kt │ │ ├── ReadStrategy.kt │ │ ├── Reader.kt │ │ ├── WriteOperation.kt │ │ ├── Writer.kt │ │ └── data │ │ │ ├── AudioTag.kt │ │ │ ├── Metadata.kt │ │ │ ├── Picture.kt │ │ │ └── Streaminfo.kt │ │ └── util │ │ ├── CharsetsUtil.kt │ │ ├── MetadataUtil.kt │ │ ├── PathUtil.kt │ │ ├── SinkUtil.kt │ │ └── SystemFileSystemUtil.kt │ └── commonTest │ └── kotlin │ └── com │ └── moriafly │ └── salt │ └── audiotag │ ├── format │ └── flac │ │ └── FlacTest.kt │ └── util │ └── SystemFileSystemUtilTest.kt ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── iosApp ├── Configuration │ └── Config.xcconfig ├── iosApp.xcodeproj │ └── project.pbxproj └── iosApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── app-icon-1024.png │ └── Contents.json │ ├── ContentView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iOSApp.swift ├── res └── DOCUMENTS.md └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{kt,kts}] 4 | max_line_length = 100 5 | ktlint_code_style = ktlint_official 6 | ktlint_standard = enabled 7 | ktlint_function_naming_ignore_when_annotated_with = Composable 8 | ktlint_standard_trailing-comma-on-call-site = disabled 9 | ktlint_standard_trailing-comma-on-declaration-site = disabled 10 | ktlint_standard_function-signature = disabled 11 | ktlint_standard_multiline-expression-wrapping = disabled 12 | ktlint_standard_chain-method-continuation = disabled 13 | ktlint_compose_compositionlocal-allowlist = disabled 14 | ktlint_standard_function-signature = disabled -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .fleet 3 | .kotlin 4 | .gradle 5 | **/build/ 6 | xcuserdata 7 | !src/**/build/ 8 | local.properties 9 | .idea 10 | .DS_Store 11 | captures 12 | .externalNativeBuild 13 | .cxx 14 | *.xcodeproj/* 15 | !*.xcodeproj/project.pbxproj 16 | !*.xcodeproj/xcshareddata/ 17 | !*.xcodeproj/project.xcworkspace/ 18 | !*.xcworkspace/contents.xcworkspacedata 19 | **/xcshareddata/WorkspaceSettings.xcsettings 20 | composeApp/release 21 | gradle.properties 22 | secret_key.gpg 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | , 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 椒盐音频标签 Salt Audio Tag 2 | 3 | ## 这是什么? 4 | 5 | 开发目标:成为一个跨平台(Android、Windows、Linux 和 macOS)音频标签编辑器。 6 | 7 | 开发状态:早期开发中。 8 | 9 | ## 项目结构 10 | 11 | 基于 Compose Multiplatform 开发,IO 操作使用 kotlinx-io 库。 12 | 13 | - composeApp:App UI 14 | - core:标签 15 | 16 | ## 格式支持 17 | 18 | | 格式 | 流信息 | 文本元数据 | 图片 | 19 | |------|-----|-------|----| 20 | | FLAC | 读 | 读/写 | 读 | 21 | 22 | ## 核心库使用 23 | 24 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.moriafly/salt-audiotag)](https://search.maven.org/search?q=g:io.github.moriafly) 25 | 26 | ## 开源协议 27 | 28 | ``` 29 | Salt Audio Tag 30 | Copyright (C) 2025 Moriafly 31 | 32 | This library is free software; you can redistribute it and/or modify it under the terms of the 33 | GNU Lesser General Public License as published by the Free Software Foundation; either version 34 | 2.1 of the License, or (at your option) any later version. 35 | 36 | This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 37 | even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 38 | Lesser General Public License for more details. 39 | 40 | You should have received a copy of the GNU Lesser General Public License along with this library; 41 | if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 42 | 02110-1301 USA 43 | ``` -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | // this is necessary to avoid the plugins to be loaded multiple times 3 | // in each subproject's classloader 4 | alias(libs.plugins.androidApplication) apply false 5 | alias(libs.plugins.androidLibrary) apply false 6 | alias(libs.plugins.composeMultiplatform) apply false 7 | alias(libs.plugins.composeCompiler) apply false 8 | alias(libs.plugins.kotlinMultiplatform) apply false 9 | alias(libs.plugins.buildkonfig) apply false 10 | } 11 | -------------------------------------------------------------------------------- /composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 2 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | 5 | plugins { 6 | alias(libs.plugins.kotlinMultiplatform) 7 | alias(libs.plugins.androidApplication) 8 | alias(libs.plugins.composeMultiplatform) 9 | alias(libs.plugins.composeCompiler) 10 | } 11 | 12 | kotlin { 13 | androidTarget { 14 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 15 | compilerOptions { 16 | jvmTarget.set(JvmTarget.JVM_21) 17 | } 18 | } 19 | 20 | // listOf( 21 | // iosX64(), 22 | // iosArm64(), 23 | // iosSimulatorArm64() 24 | // ).forEach { iosTarget -> 25 | // iosTarget.binaries.framework { 26 | // baseName = "ComposeApp" 27 | // isStatic = true 28 | // } 29 | // } 30 | 31 | jvm("desktop") 32 | 33 | sourceSets { 34 | val desktopMain by getting 35 | 36 | androidMain.dependencies { 37 | implementation(compose.preview) 38 | implementation(libs.androidx.activity.compose) 39 | } 40 | commonMain.dependencies { 41 | implementation(compose.runtime) 42 | implementation(compose.foundation) 43 | implementation(compose.material) 44 | implementation(compose.ui) 45 | implementation(compose.components.resources) 46 | implementation(compose.components.uiToolingPreview) 47 | implementation(libs.androidx.lifecycle.viewmodel) 48 | implementation(libs.androidx.lifecycle.viewmodel.compose) 49 | implementation(libs.androidx.lifecycle.runtime.compose) 50 | implementation(libs.salt.ui) 51 | implementation(libs.filekit.core) 52 | implementation(libs.filekit.dialogs) 53 | implementation(libs.filekit.dialogs.compose) 54 | implementation(libs.kotlinx.coroutines.core) 55 | implementation(libs.jetbrains.androidx.navigation.compose) 56 | implementation(project(":core")) 57 | } 58 | desktopMain.dependencies { 59 | implementation(compose.desktop.currentOs) 60 | implementation(libs.kotlinx.coroutines.swing) 61 | } 62 | } 63 | } 64 | 65 | android { 66 | namespace = "com.moriafly.salt.audiotag" 67 | compileSdk = libs.versions.android.compileSdk.get().toInt() 68 | 69 | defaultConfig { 70 | applicationId = "com.moriafly.salt.audiotag" 71 | minSdk = libs.versions.android.minSdk.get().toInt() 72 | targetSdk = libs.versions.android.targetSdk.get().toInt() 73 | versionCode = libs.versions.versionCode.get().toInt() 74 | versionName = libs.versions.versionName.get() 75 | } 76 | packaging { 77 | resources { 78 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 79 | } 80 | } 81 | buildTypes { 82 | getByName("release") { 83 | isMinifyEnabled = true 84 | } 85 | } 86 | compileOptions { 87 | sourceCompatibility = JavaVersion.VERSION_21 88 | targetCompatibility = JavaVersion.VERSION_21 89 | } 90 | } 91 | 92 | dependencies { 93 | debugImplementation(compose.uiTooling) 94 | } 95 | 96 | compose.desktop { 97 | application { 98 | mainClass = "com.moriafly.salt.audiotag.MainKt" 99 | 100 | nativeDistributions { 101 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 102 | packageName = "com.moriafly.salt.audiotag" 103 | packageVersion = "1.0.0" 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/moriafly/salt/audiotag/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag 19 | 20 | import android.os.Build 21 | import android.os.Bundle 22 | import androidx.activity.ComponentActivity 23 | import androidx.activity.compose.setContent 24 | import androidx.compose.runtime.LaunchedEffect 25 | import com.moriafly.salt.audiotag.ui.theme.AppTheme 26 | import com.moriafly.salt.ui.SaltTheme 27 | import com.moriafly.salt.ui.UnstableSaltUiApi 28 | import com.moriafly.salt.ui.ext.edgeToEdge 29 | import com.moriafly.salt.ui.util.WindowUtil 30 | 31 | class MainActivity : ComponentActivity() { 32 | @OptIn(UnstableSaltUiApi::class) 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | edgeToEdge() 35 | super.onCreate(savedInstanceState) 36 | 37 | setContent { 38 | AppTheme { 39 | val isDarkTheme = SaltTheme.configs.isDarkTheme 40 | LaunchedEffect(isDarkTheme) { 41 | if (isDarkTheme) { 42 | WindowUtil.setStatusBarForegroundColor(window, WindowUtil.BarColor.White) 43 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 44 | WindowUtil.setNavigationBarForegroundColor( 45 | window, 46 | WindowUtil.BarColor.White 47 | ) 48 | } 49 | } else { 50 | WindowUtil.setStatusBarForegroundColor(window, WindowUtil.BarColor.Black) 51 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 52 | WindowUtil.setNavigationBarForegroundColor( 53 | window, 54 | WindowUtil.BarColor.Black 55 | ) 56 | } 57 | } 58 | } 59 | 60 | MainActivityContent() 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/moriafly/salt/audiotag/util/DialogUtil.android.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.util 19 | 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.ui.platform.LocalConfiguration 22 | import androidx.compose.ui.unit.Dp 23 | import androidx.compose.ui.unit.dp 24 | 25 | @Composable 26 | actual fun getDefaultDialogContentHeight(): Dp = LocalConfiguration.current 27 | .screenHeightDp.dp * 0.67f 28 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 19 | 22 | 25 | 28 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moriafly/SaltAudioTag/eea82a1963ded9edc2535127c4bc3495a7693eb6/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moriafly/SaltAudioTag/eea82a1963ded9edc2535127c4bc3495a7693eb6/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moriafly/SaltAudioTag/eea82a1963ded9edc2535127c4bc3495a7693eb6/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moriafly/SaltAudioTag/eea82a1963ded9edc2535127c4bc3495a7693eb6/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moriafly/SaltAudioTag/eea82a1963ded9edc2535127c4bc3495a7693eb6/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moriafly/SaltAudioTag/eea82a1963ded9edc2535127c4bc3495a7693eb6/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moriafly/SaltAudioTag/eea82a1963ded9edc2535127c4bc3495a7693eb6/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moriafly/SaltAudioTag/eea82a1963ded9edc2535127c4bc3495a7693eb6/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moriafly/SaltAudioTag/eea82a1963ded9edc2535127c4bc3495a7693eb6/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moriafly/SaltAudioTag/eea82a1963ded9edc2535127c4bc3495a7693eb6/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values-zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 椒盐音频标签 4 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Salt Audio Tag 3 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 24 | 30 | 36 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/MainActivityContent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag 19 | 20 | import androidx.compose.foundation.background 21 | import androidx.compose.foundation.layout.Box 22 | import androidx.compose.foundation.layout.fillMaxSize 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.ui.Modifier 25 | import com.moriafly.salt.audiotag.ui.navigation.AppNavigation 26 | import com.moriafly.salt.ui.SaltTheme 27 | 28 | @Composable 29 | fun MainActivityContent() { 30 | Box( 31 | modifier = Modifier 32 | .fillMaxSize() 33 | .background(SaltTheme.colors.background) 34 | ) { 35 | AppNavigation() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/ui/icon/More.kt: -------------------------------------------------------------------------------- 1 | package com.moriafly.salt.audiotag.ui.icon 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | 9 | val SaltAudioTagIcons.More: ImageVector 10 | get() { 11 | if (_More != null) { 12 | return _More!! 13 | } 14 | _More = ImageVector.Builder( 15 | name = "More", 16 | defaultWidth = 24.dp, 17 | defaultHeight = 24.dp, 18 | viewportWidth = 30f, 19 | viewportHeight = 30f 20 | ).apply { 21 | path(fill = SolidColor(Color(0xFF000000))) { 22 | moveTo(5f, 12f) 23 | curveTo(3.343f, 12f, 2f, 13.343f, 2f, 15f) 24 | curveTo(2f, 16.657f, 3.343f, 18f, 5f, 18f) 25 | curveTo(6.657f, 18f, 8f, 16.657f, 8f, 15f) 26 | curveTo(8f, 13.343f, 6.657f, 12f, 5f, 12f) 27 | close() 28 | moveTo(15f, 12f) 29 | curveTo(13.343f, 12f, 12f, 13.343f, 12f, 15f) 30 | curveTo(12f, 16.657f, 13.343f, 18f, 15f, 18f) 31 | curveTo(16.657f, 18f, 18f, 16.657f, 18f, 15f) 32 | curveTo(18f, 13.343f, 16.657f, 12f, 15f, 12f) 33 | close() 34 | moveTo(25f, 12f) 35 | curveTo(23.343f, 12f, 22f, 13.343f, 22f, 15f) 36 | curveTo(22f, 16.657f, 23.343f, 18f, 25f, 18f) 37 | curveTo(26.657f, 18f, 28f, 16.657f, 28f, 15f) 38 | curveTo(28f, 13.343f, 26.657f, 12f, 25f, 12f) 39 | close() 40 | } 41 | }.build() 42 | 43 | return _More!! 44 | } 45 | 46 | @Suppress("ObjectPropertyName") 47 | private var _More: ImageVector? = null 48 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/ui/icon/SaltAudioTagIcons.kt: -------------------------------------------------------------------------------- 1 | package com.moriafly.salt.audiotag.ui.icon 2 | 3 | object SaltAudioTagIcons 4 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/ui/icon/Wrench.kt: -------------------------------------------------------------------------------- 1 | package com.moriafly.salt.audiotag.ui.icon 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.SolidColor 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import androidx.compose.ui.graphics.vector.path 7 | import androidx.compose.ui.unit.dp 8 | 9 | val SaltAudioTagIcons.Wrench: ImageVector 10 | get() { 11 | if (_Wrench != null) { 12 | return _Wrench!! 13 | } 14 | _Wrench = ImageVector.Builder( 15 | name = "Wrench", 16 | defaultWidth = 256.dp, 17 | defaultHeight = 256.dp, 18 | viewportWidth = 48f, 19 | viewportHeight = 48f 20 | ).apply { 21 | path(fill = SolidColor(Color(0xFF000000))) { 22 | moveTo(31.5f, 5f) 23 | curveTo(24.614f, 5f, 19f, 10.614f, 19f, 17.5f) 24 | curveTo(19f, 18.907f, 19.296f, 20.234f, 19.727f, 21.496f) 25 | lineTo(7.611f, 33.611f) 26 | arcTo(1.5f, 1.5f, 0f, isMoreThanHalf = false, isPositiveArc = false, 7.609f, 33.611f) 27 | curveTo(5.474f, 35.747f, 5.475f, 39.254f, 7.611f, 41.389f) 28 | curveTo(8.679f, 42.455f, 10.094f, 43f, 11.5f, 43f) 29 | curveTo(12.906f, 43f, 14.321f, 42.455f, 15.389f, 41.389f) 30 | arcTo(1.5f, 1.5f, 0f, isMoreThanHalf = false, isPositiveArc = false, 15.391f, 41.389f) 31 | lineTo(27.504f, 29.273f) 32 | curveTo(28.766f, 29.704f, 30.094f, 30f, 31.502f, 30f) 33 | curveTo(38.388f, 30f, 44.002f, 24.386f, 44.002f, 17.5f) 34 | curveTo(44.002f, 15.499f, 43.517f, 13.609f, 42.686f, 11.939f) 35 | arcTo(1.5f, 1.5f, 0f, isMoreThanHalf = false, isPositiveArc = false, 40.283f, 11.547f) 36 | lineTo(33.414f, 18.414f) 37 | curveTo(33.018f, 18.81f, 32.514f, 19f, 32f, 19f) 38 | curveTo(31.486f, 19f, 30.982f, 18.81f, 30.586f, 18.414f) 39 | curveTo(29.793f, 17.622f, 29.793f, 16.38f, 30.586f, 15.588f) 40 | arcTo(1.5f, 1.5f, 0f, isMoreThanHalf = false, isPositiveArc = false, 30.586f, 15.586f) 41 | lineTo(37.453f, 8.719f) 42 | arcTo(1.5f, 1.5f, 0f, isMoreThanHalf = false, isPositiveArc = false, 37.061f, 6.316f) 43 | curveTo(35.39f, 5.484f, 33.501f, 5f, 31.5f, 5f) 44 | close() 45 | moveTo(31.5f, 8f) 46 | curveTo(32.249f, 8f, 32.975f, 8.092f, 33.672f, 8.258f) 47 | lineTo(28.465f, 13.465f) 48 | curveTo(26.524f, 15.404f, 26.524f, 18.596f, 28.465f, 20.535f) 49 | curveTo(29.435f, 21.505f, 30.722f, 22f, 32f, 22f) 50 | curveTo(33.278f, 22f, 34.565f, 21.505f, 35.535f, 20.535f) 51 | lineTo(40.744f, 15.328f) 52 | curveTo(40.91f, 16.026f, 41.002f, 16.751f, 41.002f, 17.5f) 53 | curveTo(41.002f, 22.764f, 36.766f, 27f, 31.502f, 27f) 54 | curveTo(30.143f, 27f, 28.86f, 26.711f, 27.689f, 26.197f) 55 | arcTo(1.5f, 1.5f, 0f, isMoreThanHalf = false, isPositiveArc = false, 26.025f, 26.51f) 56 | lineTo(13.268f, 39.268f) 57 | curveTo(12.771f, 39.762f, 12.142f, 40f, 11.5f, 40f) 58 | curveTo(10.858f, 40f, 10.227f, 39.763f, 9.73f, 39.268f) 59 | curveTo(8.742f, 38.28f, 8.744f, 36.721f, 9.732f, 35.732f) 60 | lineTo(22.49f, 22.977f) 61 | arcTo(1.5f, 1.5f, 0f, isMoreThanHalf = false, isPositiveArc = false, 22.803f, 21.313f) 62 | curveTo(22.289f, 20.142f, 22f, 18.858f, 22f, 17.5f) 63 | curveTo(22f, 12.236f, 26.236f, 8f, 31.5f, 8f) 64 | close() 65 | } 66 | }.build() 67 | 68 | return _Wrench!! 69 | } 70 | 71 | @Suppress("ObjectPropertyName") 72 | private var _Wrench: ImageVector? = null 73 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/ui/navigation/AppNavigation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.ui.navigation 19 | 20 | import androidx.compose.animation.AnimatedContentTransitionScope 21 | import androidx.compose.animation.EnterTransition 22 | import androidx.compose.animation.ExitTransition 23 | import androidx.compose.animation.core.tween 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.CompositionLocalProvider 26 | import androidx.compose.runtime.compositionLocalOf 27 | import androidx.navigation.NavBackStackEntry 28 | import androidx.navigation.NavController 29 | import androidx.navigation.compose.NavHost 30 | import androidx.navigation.compose.composable 31 | import androidx.navigation.compose.rememberNavController 32 | import com.moriafly.salt.audiotag.ui.screen.audiotag.AudioTagScreen 33 | import com.moriafly.salt.audiotag.ui.screen.main.MainScreen 34 | 35 | val LocalNavController = compositionLocalOf { 36 | error("LocalNavController is not provided.") 37 | } 38 | 39 | @Composable 40 | fun AppNavigation() { 41 | val navController = rememberNavController() 42 | CompositionLocalProvider( 43 | LocalNavController provides navController 44 | ) { 45 | NavHost( 46 | navController = navController, 47 | startDestination = ScreenRoute.MAIN, 48 | enterTransition = { enterTransition }, 49 | exitTransition = { exitTransition }, 50 | popEnterTransition = { popEnterTransition }, 51 | popExitTransition = { popExitTransition } 52 | ) { 53 | composable(ScreenRoute.MAIN) { MainScreen() } 54 | composable(ScreenRoute.AUDIO_TAG) { AudioTagScreen() } 55 | } 56 | } 57 | } 58 | 59 | private val AnimatedContentTransitionScope.enterTransition: EnterTransition 60 | get() = slideIntoContainer( 61 | towards = AnimatedContentTransitionScope.SlideDirection.Left, 62 | animationSpec = tween(350) 63 | ) 64 | 65 | private val AnimatedContentTransitionScope.exitTransition: ExitTransition 66 | get() = slideOutOfContainer( 67 | towards = AnimatedContentTransitionScope.SlideDirection.Left, 68 | animationSpec = tween(350) 69 | ) 70 | 71 | private val AnimatedContentTransitionScope.popEnterTransition: EnterTransition 72 | get() = slideIntoContainer( 73 | towards = AnimatedContentTransitionScope.SlideDirection.Right, 74 | animationSpec = tween(350) 75 | ) 76 | 77 | private val AnimatedContentTransitionScope.popExitTransition: ExitTransition 78 | get() = slideOutOfContainer( 79 | towards = AnimatedContentTransitionScope.SlideDirection.Right, 80 | animationSpec = tween(350) 81 | ) 82 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/ui/navigation/ScreenRoute.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.ui.navigation 19 | 20 | object ScreenRoute { 21 | const val MAIN = "main" 22 | const val AUDIO_TAG = "audio_tag" 23 | } 24 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/ui/screen/audiotag/AudioTagScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.ui.screen.audiotag 19 | 20 | import androidx.compose.animation.AnimatedContent 21 | import androidx.compose.foundation.background 22 | import androidx.compose.foundation.layout.Box 23 | import androidx.compose.foundation.layout.Column 24 | import androidx.compose.foundation.layout.ColumnScope 25 | import androidx.compose.foundation.layout.Spacer 26 | import androidx.compose.foundation.layout.WindowInsets 27 | import androidx.compose.foundation.layout.fillMaxSize 28 | import androidx.compose.foundation.layout.fillMaxWidth 29 | import androidx.compose.foundation.layout.height 30 | import androidx.compose.foundation.layout.size 31 | import androidx.compose.foundation.layout.windowInsetsBottomHeight 32 | import androidx.compose.foundation.lazy.LazyColumn 33 | import androidx.compose.material.CircularProgressIndicator 34 | import androidx.compose.runtime.Composable 35 | import androidx.compose.runtime.LaunchedEffect 36 | import androidx.compose.runtime.collectAsState 37 | import androidx.compose.runtime.getValue 38 | import androidx.compose.ui.Alignment 39 | import androidx.compose.ui.Modifier 40 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 41 | import androidx.compose.ui.unit.dp 42 | import androidx.lifecycle.viewmodel.compose.viewModel 43 | import com.moriafly.salt.audiotag.ui.navigation.LocalNavController 44 | import com.moriafly.salt.audiotag.ui.screen.basic.BasicScreenColumn 45 | import com.moriafly.salt.ui.BottomBar 46 | import com.moriafly.salt.ui.BottomBarItem 47 | import com.moriafly.salt.ui.Item 48 | import com.moriafly.salt.ui.ItemButton 49 | import com.moriafly.salt.ui.ItemDivider 50 | import com.moriafly.salt.ui.ItemInfo 51 | import com.moriafly.salt.ui.ItemInfoType 52 | import com.moriafly.salt.ui.ItemOuterTitle 53 | import com.moriafly.salt.ui.RoundedColumn 54 | import com.moriafly.salt.ui.SaltTheme 55 | import com.moriafly.salt.ui.Text 56 | import com.moriafly.salt.ui.UnstableSaltUiApi 57 | import com.moriafly.salt.ui.ext.safeMainIgnoringVisibility 58 | import com.moriafly.salt.ui.icons.SaltIcons 59 | import com.moriafly.salt.ui.icons.Success 60 | import com.moriafly.salt.ui.util.SystemUtil 61 | import io.github.vinceglb.filekit.dialogs.FileKitType 62 | import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher 63 | 64 | @Composable 65 | fun AudioTagScreen( 66 | viewModel: AudioTagViewModel = viewModel { AudioTagViewModel() } 67 | ) { 68 | BasicScreenColumn( 69 | title = "音频标签", 70 | autoVerticalScroll = false 71 | ) { 72 | val navController = LocalNavController.current 73 | LaunchedEffect(Unit) { 74 | viewModel.saveResult.collect { 75 | navController.popBackStack() 76 | } 77 | } 78 | 79 | LaunchedEffect(Unit) { 80 | viewModel.readResult.collect { 81 | } 82 | } 83 | 84 | AudioTagScreenContent() 85 | } 86 | } 87 | 88 | @OptIn(UnstableSaltUiApi::class) 89 | @Composable 90 | private fun ColumnScope.AudioTagScreenContent( 91 | viewModel: AudioTagViewModel = viewModel() 92 | ) { 93 | val launcher = rememberFilePickerLauncher( 94 | type = FileKitType.File( 95 | "flac" 96 | ) 97 | ) { platformFile -> 98 | if (platformFile != null) { 99 | viewModel.load(platformFile) 100 | } 101 | } 102 | 103 | LaunchedEffect(viewModel) { 104 | launcher.launch() 105 | } 106 | 107 | val uiState by viewModel.uiState.collectAsState() 108 | val state = uiState.state 109 | val metadataItems = uiState.metadataItemUiStates 110 | 111 | AnimatedContent( 112 | targetState = state, 113 | modifier = Modifier 114 | .weight(1f) 115 | ) { targetState -> 116 | when (targetState) { 117 | AudioTagUiState.State.Idle -> { 118 | IdleContent( 119 | onPickFile = { 120 | launcher.launch() 121 | } 122 | ) 123 | } 124 | 125 | AudioTagUiState.State.Loading -> LoadingContent() 126 | 127 | AudioTagUiState.State.Loaded -> { 128 | LazyColumn( 129 | modifier = Modifier 130 | .weight(1f) 131 | .fillMaxWidth() 132 | ) { 133 | uiState.streaminfo?.let { 134 | item { 135 | StreaminfoPanel(it) 136 | } 137 | } 138 | 139 | item { 140 | ItemOuterTitle(text = "元数据") 141 | RoundedColumn { 142 | metadataItems.forEachIndexed { index, item -> 143 | if (index != 0) { 144 | ItemDivider( 145 | color = SaltTheme.colors.subText.copy(alpha = 0.5f) 146 | ) 147 | } 148 | MetadataItem( 149 | onDelete = { 150 | viewModel.removeMetadata(index) 151 | }, 152 | item = item 153 | ) 154 | } 155 | } 156 | } 157 | 158 | item { 159 | RoundedColumn { 160 | ItemButton( 161 | onClick = { 162 | viewModel.addEmptyMetadata() 163 | }, 164 | text = "添加元数据项" 165 | ) 166 | } 167 | } 168 | } 169 | } 170 | 171 | AudioTagUiState.State.Saving -> SavingContent() 172 | 173 | AudioTagUiState.State.Error -> { 174 | RoundedColumn { 175 | ItemInfo( 176 | text = "读取错误", 177 | infoType = ItemInfoType.Error 178 | ) 179 | } 180 | } 181 | } 182 | } 183 | 184 | BottomBar { 185 | BottomBarItem( 186 | state = true, 187 | onClick = { 188 | viewModel.save() 189 | }, 190 | painter = rememberVectorPainter(SaltIcons.Success), 191 | text = "保存" 192 | ) 193 | } 194 | 195 | if (SystemUtil.os.isAndroid()) { 196 | Spacer( 197 | Modifier 198 | .fillMaxWidth() 199 | .windowInsetsBottomHeight(WindowInsets.safeMainIgnoringVisibility) 200 | .background(SaltTheme.colors.subBackground) 201 | ) 202 | } 203 | } 204 | 205 | @Composable 206 | private fun IdleContent( 207 | onPickFile: () -> Unit 208 | ) { 209 | Column( 210 | modifier = Modifier 211 | .fillMaxSize() 212 | ) { 213 | RoundedColumn { 214 | Item( 215 | onClick = { 216 | onPickFile() 217 | }, 218 | text = "选择音频文件" 219 | ) 220 | } 221 | } 222 | } 223 | 224 | @Composable 225 | private fun LoadingContent() { 226 | Box( 227 | modifier = Modifier 228 | .fillMaxSize(), 229 | contentAlignment = Alignment.Center 230 | ) { 231 | Column( 232 | horizontalAlignment = Alignment.CenterHorizontally 233 | ) { 234 | CircularProgressIndicator( 235 | modifier = Modifier 236 | .size(SaltTheme.dimens.itemIcon), 237 | color = SaltTheme.colors.highlight, 238 | strokeWidth = 2.dp 239 | ) 240 | Spacer(Modifier.height(2.dp)) 241 | Text( 242 | text = "读取中……" 243 | ) 244 | } 245 | } 246 | } 247 | 248 | @Composable 249 | private fun SavingContent() { 250 | Box( 251 | modifier = Modifier 252 | .fillMaxSize(), 253 | contentAlignment = Alignment.Center 254 | ) { 255 | Column( 256 | horizontalAlignment = Alignment.CenterHorizontally 257 | ) { 258 | CircularProgressIndicator( 259 | modifier = Modifier 260 | .size(SaltTheme.dimens.itemIcon), 261 | color = SaltTheme.colors.highlight, 262 | strokeWidth = 2.dp 263 | ) 264 | Spacer(Modifier.height(2.dp)) 265 | Text( 266 | text = "保存中……" 267 | ) 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/ui/screen/audiotag/AudioTagUiState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.ui.screen.audiotag 19 | 20 | import androidx.compose.foundation.text.input.TextFieldState 21 | import com.moriafly.salt.audiotag.rw.data.Streaminfo 22 | 23 | data class AudioTagUiState( 24 | val state: State = State.Idle, 25 | val streaminfo: Streaminfo? = null, 26 | val metadataItemUiStates: List = emptyList() 27 | ) { 28 | enum class State { 29 | Idle, 30 | Loading, 31 | Loaded, 32 | Saving, 33 | Error 34 | } 35 | 36 | data class MetadataItemUiState( 37 | val key: TextFieldState, 38 | val value: TextFieldState 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/ui/screen/audiotag/AudioTagViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.ui.screen.audiotag 19 | 20 | import androidx.compose.foundation.text.input.TextFieldState 21 | import androidx.lifecycle.ViewModel 22 | import androidx.lifecycle.viewModelScope 23 | import com.moriafly.salt.audiotag.SaltAudioTag 24 | import com.moriafly.salt.audiotag.UnstableSaltAudioTagApi 25 | import com.moriafly.salt.audiotag.rw.ReadStrategy 26 | import com.moriafly.salt.audiotag.rw.WriteOperation 27 | import com.moriafly.salt.audiotag.rw.data.Metadata 28 | import com.moriafly.salt.audiotag.util.SystemFileSystemUtil 29 | import io.github.vinceglb.filekit.PlatformFile 30 | import io.github.vinceglb.filekit.extension 31 | import io.github.vinceglb.filekit.sink 32 | import io.github.vinceglb.filekit.source 33 | import kotlinx.coroutines.Dispatchers 34 | import kotlinx.coroutines.Job 35 | import kotlinx.coroutines.flow.MutableSharedFlow 36 | import kotlinx.coroutines.flow.MutableStateFlow 37 | import kotlinx.coroutines.flow.asSharedFlow 38 | import kotlinx.coroutines.flow.asStateFlow 39 | import kotlinx.coroutines.flow.update 40 | import kotlinx.coroutines.launch 41 | import kotlinx.coroutines.sync.Mutex 42 | import kotlinx.coroutines.sync.withLock 43 | import kotlinx.io.buffered 44 | import kotlinx.io.files.SystemFileSystem 45 | 46 | class AudioTagViewModel : ViewModel() { 47 | private val _uiState = MutableStateFlow(AudioTagUiState()) 48 | val uiState = _uiState.asStateFlow() 49 | 50 | private val _readResult = MutableSharedFlow() 51 | val readResult = _readResult.asSharedFlow() 52 | 53 | private val _saveResult = MutableSharedFlow() 54 | val saveResult = _saveResult.asSharedFlow() 55 | 56 | private val mutex = Mutex() 57 | 58 | private var platformFile: PlatformFile? = null 59 | 60 | private var loadJob: Job? = null 61 | private var saveJob: Job? = null 62 | 63 | @OptIn(UnstableSaltAudioTagApi::class) 64 | fun load(platformFile: PlatformFile) { 65 | loadJob?.cancel() 66 | loadJob = viewModelScope.launch(Dispatchers.IO) { 67 | mutex.withLock { 68 | _uiState.update { it.copy(state = AudioTagUiState.State.Loading) } 69 | 70 | this@AudioTagViewModel.platformFile = platformFile 71 | 72 | try { 73 | val audioTagResult = platformFile.source().buffered().use { source -> 74 | SaltAudioTag.read( 75 | source = source, 76 | extension = platformFile.extension, 77 | strategy = ReadStrategy.All 78 | ) 79 | } 80 | 81 | val audioTag = audioTagResult.getOrThrow() 82 | 83 | val metadatas = audioTag.metadatas ?: emptyList() 84 | 85 | _uiState.update { 86 | it.copy( 87 | streaminfo = audioTag.streaminfo, 88 | metadataItemUiStates = metadatas.map { metadata -> 89 | AudioTagUiState.MetadataItemUiState( 90 | key = TextFieldState(metadata.key), 91 | value = TextFieldState(metadata.value) 92 | ) 93 | } 94 | ) 95 | } 96 | 97 | _readResult.emit(true) 98 | _uiState.update { it.copy(state = AudioTagUiState.State.Loaded) } 99 | } catch (e: Exception) { 100 | _readResult.emit(false) 101 | _uiState.update { it.copy(state = AudioTagUiState.State.Error) } 102 | 103 | // TODO 104 | e.printStackTrace() 105 | } 106 | } 107 | } 108 | } 109 | 110 | @OptIn(UnstableSaltAudioTagApi::class) 111 | fun save() { 112 | saveJob?.cancel() 113 | saveJob = viewModelScope.launch(Dispatchers.IO) { 114 | mutex.withLock { 115 | _uiState.update { it.copy(state = AudioTagUiState.State.Saving) } 116 | 117 | val platformFile = this@AudioTagViewModel.platformFile 118 | 119 | if (platformFile == null) { 120 | _saveResult.emit(false) 121 | return@launch 122 | } 123 | 124 | val metadatas = _uiState.value.metadataItemUiStates.map { metadataItemUiState -> 125 | Metadata( 126 | key = metadataItemUiState.key.text.toString(), 127 | value = metadataItemUiState.value.text.toString() 128 | ) 129 | } 130 | 131 | val src = SystemFileSystemUtil.tempFilePath() 132 | val dst = SystemFileSystemUtil.tempFilePath() 133 | try { 134 | platformFile.source().buffered().use { 135 | SystemFileSystemUtil.write(it, src) 136 | } 137 | 138 | SaltAudioTag.write( 139 | src = src, 140 | dst = dst, 141 | extension = platformFile.extension, 142 | WriteOperation.AllMetadata.create(metadatas) 143 | ) 144 | 145 | platformFile.sink().buffered().use { sink -> 146 | SystemFileSystem.source(dst).buffered().use { source -> 147 | sink.transferFrom(source) 148 | } 149 | } 150 | 151 | _saveResult.emit(true) 152 | } catch (e: Exception) { 153 | // Save error 154 | _saveResult.emit(false) 155 | } finally { 156 | SystemFileSystem.delete(src) 157 | SystemFileSystem.delete(dst) 158 | } 159 | } 160 | } 161 | } 162 | 163 | fun addEmptyMetadata() { 164 | _uiState.update { 165 | it.copy( 166 | metadataItemUiStates = 167 | it.metadataItemUiStates + AudioTagUiState.MetadataItemUiState( 168 | key = TextFieldState(), 169 | value = TextFieldState() 170 | ) 171 | ) 172 | } 173 | } 174 | 175 | fun removeMetadata(index: Int) { 176 | _uiState.update { 177 | it.copy( 178 | metadataItemUiStates = 179 | it.metadataItemUiStates.toMutableList().apply { 180 | removeAt(index) 181 | } 182 | ) 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/ui/screen/audiotag/MetadataItem.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.ui.screen.audiotag 19 | 20 | import androidx.compose.foundation.background 21 | import androidx.compose.foundation.interaction.MutableInteractionSource 22 | import androidx.compose.foundation.interaction.collectIsFocusedAsState 23 | import androidx.compose.foundation.layout.Box 24 | import androidx.compose.foundation.layout.Row 25 | import androidx.compose.foundation.layout.Spacer 26 | import androidx.compose.foundation.layout.fillMaxWidth 27 | import androidx.compose.foundation.layout.padding 28 | import androidx.compose.foundation.layout.size 29 | import androidx.compose.foundation.layout.width 30 | import androidx.compose.foundation.text.BasicTextField 31 | import androidx.compose.foundation.text.input.TextFieldState 32 | import androidx.compose.runtime.Composable 33 | import androidx.compose.runtime.remember 34 | import androidx.compose.ui.Alignment 35 | import androidx.compose.ui.Modifier 36 | import androidx.compose.ui.graphics.Color 37 | import androidx.compose.ui.graphics.SolidColor 38 | import androidx.compose.ui.graphics.takeOrElse 39 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 40 | import androidx.compose.ui.text.TextStyle 41 | import androidx.compose.ui.text.font.FontWeight 42 | import androidx.compose.ui.unit.dp 43 | import com.moriafly.salt.audiotag.ui.icon.More 44 | import com.moriafly.salt.audiotag.ui.icon.SaltAudioTagIcons 45 | import com.moriafly.salt.ui.Icon 46 | import com.moriafly.salt.ui.SaltTheme 47 | import com.moriafly.salt.ui.noRippleClickable 48 | import com.moriafly.salt.ui.popup.rememberPopupState 49 | 50 | @Composable 51 | fun MetadataItem( 52 | onDelete: () -> Unit, 53 | item: AudioTagUiState.MetadataItemUiState 54 | ) { 55 | val state = rememberPopupState() 56 | val color = if (state.expend) { 57 | SaltTheme.colors.stroke 58 | } else { 59 | Color.Unspecified 60 | } 61 | Row( 62 | modifier = Modifier 63 | .background(color) 64 | .padding( 65 | horizontal = SaltTheme.dimens.padding 66 | ), 67 | verticalAlignment = Alignment.CenterVertically 68 | ) { 69 | MetadataKeyValue( 70 | key = item.key, 71 | value = item.value, 72 | modifier = Modifier 73 | .weight(1f) 74 | ) 75 | Box { 76 | Icon( 77 | painter = rememberVectorPainter(SaltAudioTagIcons.More), 78 | contentDescription = null, 79 | modifier = Modifier 80 | .size(SaltTheme.dimens.itemIcon) 81 | .noRippleClickable { 82 | state.expend() 83 | } 84 | .padding(4.dp), 85 | tint = SaltTheme.colors.text.copy(alpha = 0.75f) 86 | ) 87 | 88 | MetadataItemPopup( 89 | onDelete = onDelete, 90 | state = state 91 | ) 92 | } 93 | } 94 | } 95 | 96 | @Composable 97 | private fun MetadataKeyValue( 98 | key: TextFieldState, 99 | value: TextFieldState, 100 | modifier: Modifier = Modifier 101 | ) { 102 | Row( 103 | modifier = modifier 104 | ) { 105 | MetadataTextField( 106 | state = key, 107 | modifier = Modifier 108 | .weight(1f) 109 | ) 110 | Spacer(Modifier.width(SaltTheme.dimens.subPadding)) 111 | MetadataTextField( 112 | state = value, 113 | modifier = Modifier 114 | .weight(3f), 115 | textStyle = SaltTheme.textStyles.main.copy( 116 | fontWeight = FontWeight.Bold 117 | ) 118 | ) 119 | } 120 | } 121 | 122 | @Composable 123 | private fun MetadataTextField( 124 | state: TextFieldState, 125 | modifier: Modifier = Modifier, 126 | textStyle: TextStyle = SaltTheme.textStyles.main, 127 | ) { 128 | val interactionSource = remember { MutableInteractionSource() } 129 | val isFocused = interactionSource.collectIsFocusedAsState() 130 | val borderColor = if (isFocused.value) { 131 | SaltTheme.colors.highlight 132 | } else { 133 | SaltTheme.colors.subText.copy(alpha = 0.5f) 134 | } 135 | 136 | val color = textStyle.color.takeOrElse { SaltTheme.colors.text } 137 | 138 | BasicTextField( 139 | state = state, 140 | modifier = modifier, 141 | textStyle = textStyle.copy( 142 | color = color 143 | ), 144 | interactionSource = interactionSource, 145 | cursorBrush = SolidColor(SaltTheme.colors.highlight), 146 | decorator = { innerTextField -> 147 | Box( 148 | modifier = Modifier 149 | .fillMaxWidth() 150 | .padding( 151 | vertical = SaltTheme.dimens.subPadding 152 | ) 153 | ) { 154 | innerTextField() 155 | } 156 | } 157 | ) 158 | } 159 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/ui/screen/audiotag/MetadataItemPopup.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.ui.screen.audiotag 19 | 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.getValue 22 | import androidx.compose.runtime.mutableStateOf 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.setValue 25 | import com.moriafly.salt.ui.UnstableSaltUiApi 26 | import com.moriafly.salt.ui.dialog.YesNoDialog 27 | import com.moriafly.salt.ui.popup.PopupMenu 28 | import com.moriafly.salt.ui.popup.PopupMenuItem 29 | import com.moriafly.salt.ui.popup.PopupState 30 | 31 | @Suppress("DEPRECATION") 32 | @OptIn(UnstableSaltUiApi::class) 33 | @Composable 34 | fun MetadataItemPopup( 35 | onDelete: () -> Unit, 36 | state: PopupState 37 | ) { 38 | var deleteDialog by remember { mutableStateOf(false) } 39 | if (deleteDialog) { 40 | YesNoDialog( 41 | onDismissRequest = { 42 | deleteDialog = false 43 | }, 44 | onConfirm = { 45 | deleteDialog = false 46 | onDelete() 47 | }, 48 | title = "删除此元数据项目", 49 | content = "是否确认删除?" 50 | ) 51 | } 52 | 53 | PopupMenu( 54 | expanded = state.expend, 55 | onDismissRequest = { state.dismiss() } 56 | ) { 57 | PopupMenuItem( 58 | onClick = { 59 | state.dismiss() 60 | deleteDialog = true 61 | }, 62 | text = "删除" 63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/ui/screen/audiotag/StreaminfoPanel.kt: -------------------------------------------------------------------------------- 1 | package com.moriafly.salt.audiotag.ui.screen.audiotag 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import com.moriafly.salt.audiotag.rw.data.Streaminfo 9 | import com.moriafly.salt.audiotag.rw.data.seconds 10 | import com.moriafly.salt.ui.ItemOuterTitle 11 | import com.moriafly.salt.ui.ItemValue 12 | import com.moriafly.salt.ui.RoundedColumn 13 | 14 | @Composable 15 | fun StreaminfoPanel( 16 | streaminfo: Streaminfo 17 | ) { 18 | Column( 19 | modifier = Modifier 20 | .fillMaxWidth() 21 | ) { 22 | ItemOuterTitle(text = "流信息") 23 | RoundedColumn { 24 | Row { 25 | ItemValue( 26 | text = "采样率", 27 | sub = "${streaminfo.sampleRate.toFloat() / 1000} kHz", 28 | modifier = Modifier 29 | .weight(1f) 30 | ) 31 | ItemValue( 32 | text = "声道数", 33 | sub = "${streaminfo.channelCount}", 34 | modifier = Modifier 35 | .weight(1f) 36 | ) 37 | } 38 | Row { 39 | ItemValue( 40 | text = "位深度", 41 | sub = "${streaminfo.bits}", 42 | modifier = Modifier 43 | .weight(1f) 44 | ) 45 | ItemValue( 46 | text = "时长", 47 | sub = "${streaminfo.seconds} 秒", 48 | modifier = Modifier 49 | .weight(1f) 50 | ) 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/ui/screen/basic/BasicScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.ui.screen.basic 19 | 20 | import androidx.compose.foundation.layout.Column 21 | import androidx.compose.foundation.layout.ColumnScope 22 | import androidx.compose.foundation.layout.Spacer 23 | import androidx.compose.foundation.layout.WindowInsets 24 | import androidx.compose.foundation.layout.WindowInsetsSides 25 | import androidx.compose.foundation.layout.fillMaxSize 26 | import androidx.compose.foundation.layout.only 27 | import androidx.compose.foundation.layout.windowInsetsBottomHeight 28 | import androidx.compose.foundation.layout.windowInsetsPadding 29 | import androidx.compose.foundation.rememberScrollState 30 | import androidx.compose.foundation.verticalScroll 31 | import androidx.compose.runtime.Composable 32 | import androidx.compose.ui.Modifier 33 | import com.moriafly.salt.audiotag.ui.navigation.LocalNavController 34 | import com.moriafly.salt.ui.TitleBar 35 | import com.moriafly.salt.ui.UnstableSaltUiApi 36 | import com.moriafly.salt.ui.ext.safeMainIgnoringVisibility 37 | import com.moriafly.salt.ui.thenIf 38 | import com.moriafly.salt.ui.util.SystemUtil 39 | 40 | @OptIn(UnstableSaltUiApi::class) 41 | @Composable 42 | fun BasicScreenColumn( 43 | title: String, 44 | showBackBtn: Boolean = true, 45 | autoVerticalScroll: Boolean = true, 46 | content: @Composable ColumnScope.() -> Unit 47 | ) { 48 | Column( 49 | modifier = Modifier 50 | .thenIf(SystemUtil.os.isAndroid()) { 51 | windowInsetsPadding( 52 | WindowInsets.safeMainIgnoringVisibility.only( 53 | WindowInsetsSides.Top + WindowInsetsSides.Horizontal 54 | ) 55 | ) 56 | } 57 | ) { 58 | val navController = LocalNavController.current 59 | TitleBar( 60 | onBack = { 61 | navController.popBackStack() 62 | }, 63 | text = title, 64 | showBackBtn = showBackBtn 65 | ) 66 | Column( 67 | modifier = Modifier 68 | .fillMaxSize() 69 | .thenIf(autoVerticalScroll) { 70 | verticalScroll(rememberScrollState()) 71 | } 72 | ) { 73 | content() 74 | if (SystemUtil.os.isAndroid() && autoVerticalScroll) { 75 | Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeMainIgnoringVisibility)) 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/ui/screen/main/MainScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.ui.screen.main 19 | 20 | import androidx.compose.runtime.Composable 21 | import com.moriafly.salt.audiotag.SaltAudioTag 22 | import com.moriafly.salt.audiotag.ui.navigation.LocalNavController 23 | import com.moriafly.salt.audiotag.ui.navigation.ScreenRoute 24 | import com.moriafly.salt.audiotag.ui.screen.basic.BasicScreenColumn 25 | import com.moriafly.salt.ui.Item 26 | import com.moriafly.salt.ui.ItemOuterLargeTitle 27 | import com.moriafly.salt.ui.ItemOuterTitle 28 | import com.moriafly.salt.ui.ItemTip 29 | import com.moriafly.salt.ui.RoundedColumn 30 | import com.moriafly.salt.ui.UnstableSaltUiApi 31 | 32 | @OptIn(UnstableSaltUiApi::class) 33 | @Composable 34 | fun MainScreen() { 35 | BasicScreenColumn( 36 | title = "", 37 | showBackBtn = false 38 | ) { 39 | ItemOuterLargeTitle( 40 | text = "椒盐音频标签", 41 | sub = "版本 ${SaltAudioTag.getVersionName()}" 42 | ) 43 | 44 | RoundedColumn { 45 | val navController = LocalNavController.current 46 | Item( 47 | onClick = { 48 | navController.navigate(ScreenRoute.AUDIO_TAG) 49 | }, 50 | text = "FLAC 元数据读写" 51 | ) 52 | } 53 | 54 | ItemOuterTitle(text = "开源协议") 55 | RoundedColumn { 56 | ItemTip(text = LICENSE) 57 | } 58 | } 59 | } 60 | 61 | private val LICENSE = 62 | """ 63 | Salt Audio Tag 64 | Copyright (C) 2025 Moriafly 65 | 66 | This library is free software; you can redistribute it and/or modify it under the terms of the 67 | GNU Lesser General Public License as published by the Free Software Foundation; either version 68 | 2.1 of the License, or (at your option) any later version. 69 | 70 | This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 71 | even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 72 | Lesser General Public License for more details. 73 | 74 | You should have received a copy of the GNU Lesser General Public License along with this library; 75 | if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 76 | 02110-1301 USA 77 | """.trimIndent() 78 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/ui/theme/AppTheme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.ui.theme 19 | 20 | import androidx.compose.foundation.isSystemInDarkTheme 21 | import androidx.compose.material.ripple 22 | import androidx.compose.runtime.Composable 23 | import com.moriafly.salt.ui.SaltConfigs 24 | import com.moriafly.salt.ui.SaltDynamicColors 25 | import com.moriafly.salt.ui.SaltTheme 26 | import com.moriafly.salt.ui.darkSaltColors 27 | import com.moriafly.salt.ui.lightSaltColors 28 | 29 | @Composable 30 | fun AppTheme( 31 | content: @Composable () -> Unit 32 | ) { 33 | SaltTheme( 34 | configs = SaltConfigs( 35 | isDarkTheme = isSystemInDarkTheme(), 36 | indication = ripple() 37 | ), 38 | dynamicColors = SaltDynamicColors( 39 | light = lightSaltColors(), 40 | dark = darkSaltColors() 41 | ), 42 | content = content 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/moriafly/salt/audiotag/util/DialogUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.util 19 | 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.ui.unit.Dp 22 | 23 | @Composable 24 | expect fun getDefaultDialogContentHeight(): Dp 25 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/moriafly/salt/audiotag/Main.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | @file:Suppress("ktlint:standard:filename") 19 | 20 | package com.moriafly.salt.audiotag 21 | 22 | import androidx.compose.ui.window.Window 23 | import androidx.compose.ui.window.application 24 | 25 | fun main() = application { 26 | Window( 27 | onCloseRequest = ::exitApplication, 28 | title = "SaltAudioTag", 29 | ) { 30 | MainActivityContent() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/moriafly/salt/audiotag/util/DialogUtil.desktop.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.util 19 | 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.ui.ExperimentalComposeUiApi 22 | import androidx.compose.ui.platform.LocalDensity 23 | import androidx.compose.ui.platform.LocalWindowInfo 24 | import androidx.compose.ui.unit.Dp 25 | 26 | @OptIn(ExperimentalComposeUiApi::class) 27 | @Composable 28 | actual fun getDefaultDialogContentHeight(): Dp { 29 | val density = LocalDensity.current 30 | val windowInfo = LocalWindowInfo.current 31 | return with(density) { windowInfo.containerSize.height.toDp() * 0.67f } 32 | } 33 | -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/com/moriafly/salt/audiotag/MainViewController.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | @file:Suppress("FunctionName") 19 | 20 | package com.moriafly.salt.audiotag 21 | 22 | import androidx.compose.ui.window.ComposeUIViewController 23 | 24 | @Suppress("ktlint:standard:function-naming") 25 | fun MainViewController() = ComposeUIViewController { 26 | MainActivityContent() 27 | } 28 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.codingfeline.buildkonfig.compiler.FieldSpec.Type 2 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | import com.vanniktech.maven.publish.SonatypeHost 5 | 6 | plugins { 7 | alias(libs.plugins.kotlinMultiplatform) 8 | alias(libs.plugins.androidLibrary) 9 | alias(libs.plugins.buildkonfig) 10 | id("maven-publish") 11 | id("com.vanniktech.maven.publish") version "0.30.0" 12 | } 13 | 14 | mavenPublishing { 15 | // Define coordinates for the published artifact 16 | coordinates( 17 | groupId = "io.github.moriafly", 18 | artifactId = "salt-audiotag", 19 | version = libs.versions.versionName.get() 20 | ) 21 | 22 | // Configure POM metadata for the published artifact 23 | pom { 24 | name.set("Salt Audio Tag") 25 | description.set("Audio Tag") 26 | inceptionYear.set("2025") 27 | url.set("https://github.com/Moriafly/SaltAudioTag") 28 | 29 | licenses { 30 | license { 31 | name.set("GNU Lesser General Public License v2.1") 32 | url.set("https://github.com/Moriafly/SaltAudioTag/blob/main/LICENSE") 33 | } 34 | } 35 | 36 | // Specify developer information 37 | developers { 38 | developer { 39 | id.set("Moriafly") 40 | name.set("Moriafly") 41 | email.set("moriafly@163.com") 42 | } 43 | } 44 | 45 | // Specify SCM information 46 | scm { 47 | url.set("https://github.com/Moriafly/SaltAudioTag") 48 | } 49 | } 50 | 51 | // Configure publishing to Maven Central 52 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 53 | 54 | // Enable GPG signing for all publications 55 | signAllPublications() 56 | } 57 | 58 | kotlin { 59 | compilerOptions { 60 | freeCompilerArgs.addAll( 61 | "-Xexpect-actual-classes", 62 | "-Xcontext-receivers", 63 | "-Xwhen-guards" 64 | ) 65 | } 66 | 67 | androidTarget { 68 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 69 | compilerOptions { 70 | jvmTarget.set(JvmTarget.JVM_11) 71 | } 72 | } 73 | 74 | listOf( 75 | iosX64(), 76 | iosArm64(), 77 | iosSimulatorArm64() 78 | ).forEach { iosTarget -> 79 | iosTarget.binaries.framework { 80 | baseName = "Core" 81 | isStatic = true 82 | } 83 | } 84 | 85 | jvm("desktop") 86 | 87 | sourceSets { 88 | val desktopMain by getting 89 | 90 | androidMain.dependencies { 91 | // implementation(libs.kotlin.test.junit) 92 | } 93 | commonMain.dependencies { 94 | api(libs.kotlinx.io.core) 95 | api(libs.kotlinx.io.bytestring) 96 | } 97 | commonTest.dependencies { 98 | implementation(libs.kotlin.test) 99 | } 100 | desktopMain.dependencies { 101 | implementation(libs.kotlinx.coroutines.swing) 102 | } 103 | } 104 | } 105 | 106 | android { 107 | namespace = "com.moriafly.salt.audiotag" 108 | compileSdk = libs.versions.android.compileSdk.get().toInt() 109 | 110 | defaultConfig { 111 | minSdk = libs.versions.android.minSdk.get().toInt() 112 | targetSdk = libs.versions.android.targetSdk.get().toInt() 113 | } 114 | packaging { 115 | resources { 116 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 117 | } 118 | } 119 | buildTypes { 120 | getByName("release") { 121 | isMinifyEnabled = false 122 | } 123 | } 124 | compileOptions { 125 | sourceCompatibility = JavaVersion.VERSION_11 126 | targetCompatibility = JavaVersion.VERSION_11 127 | } 128 | } 129 | 130 | dependencies { 131 | } 132 | 133 | buildkonfig { 134 | packageName = "com.mroiafly.salt.audiotag" 135 | // objectName = "YourAwesomeConfig" 136 | // exposeObjectWithName = "YourAwesomePublicConfig" 137 | 138 | defaultConfigs { 139 | buildConfigField(Type.STRING, "versionName", libs.versions.versionName.get()) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /core/src/androidMain/kotlin/com/moriafly/salt/audiotag/SaltAudioTag.android.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | @file:Suppress("unused") 19 | 20 | package com.moriafly.salt.audiotag 21 | 22 | import android.content.Context 23 | import android.net.Uri 24 | import com.moriafly.salt.audiotag.rw.ReadStrategy 25 | import com.moriafly.salt.audiotag.rw.data.AudioTag 26 | import kotlinx.io.asSource 27 | import kotlinx.io.buffered 28 | 29 | /** 30 | * Read audio tag from [uri]. 31 | */ 32 | @UnstableSaltAudioTagApi 33 | fun SaltAudioTag.read( 34 | context: Context, 35 | uri: Uri, 36 | extension: String, 37 | strategy: ReadStrategy 38 | ): Result = runCatching { 39 | val inputStream = context.contentResolver.openInputStream(uri) 40 | 41 | require(inputStream != null) { 42 | "Failed to open input stream form $uri" 43 | } 44 | 45 | inputStream.asSource().buffered().use { source -> 46 | read(source, extension, strategy).getOrThrow() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/SaltAudioTag.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag 19 | 20 | import com.moriafly.salt.audiotag.format.flac.FlacReader 21 | import com.moriafly.salt.audiotag.format.flac.FlacWriter 22 | import com.moriafly.salt.audiotag.rw.ReadStrategy 23 | import com.moriafly.salt.audiotag.rw.WriteOperation 24 | import com.moriafly.salt.audiotag.rw.data.AudioTag 25 | import com.mroiafly.salt.audiotag.BuildKonfig 26 | import kotlinx.io.Source 27 | import kotlinx.io.buffered 28 | import kotlinx.io.files.Path 29 | import kotlinx.io.files.SystemFileSystem 30 | 31 | /** 32 | * Salt Audio Tag 33 | * 34 | * @author Moriafly 35 | */ 36 | object SaltAudioTag { 37 | fun getVersionName() = BuildKonfig.versionName 38 | 39 | /** 40 | * Read audio file. 41 | * 42 | * Sample: 43 | * ```kotlin 44 | * source.use { 45 | * val result = SaltAudioTag.read( 46 | * source = it, 47 | * extension = "flac", 48 | * strategy = ReadStrategy.All 49 | * ) 50 | * } 51 | * ``` 52 | * 53 | * @param source Audio file source. 54 | * @param extension Audio file extension. 55 | * @param strategy Read strategy. 56 | */ 57 | @UnstableSaltAudioTagApi 58 | fun read( 59 | source: Source, 60 | extension: String, 61 | strategy: ReadStrategy 62 | ): Result = runCatching { 63 | when (extension) { 64 | "flac" -> FlacReader().read(source, strategy) 65 | else -> throw UnsupportedOperationException("Unsupported file extension $extension") 66 | } 67 | } 68 | 69 | /** 70 | * Read audio file. 71 | * 72 | * @param path Audio file path in SystemFileSystem. 73 | * @param extension Audio file extension. 74 | * @param strategy Read strategy. 75 | */ 76 | @UnstableSaltAudioTagApi 77 | fun read( 78 | path: Path, 79 | extension: String, 80 | strategy: ReadStrategy 81 | ): Result = runCatching { 82 | SystemFileSystem.source(path).buffered().use { source -> 83 | read(source, extension, strategy).getOrThrow() 84 | } 85 | } 86 | 87 | /** 88 | * Write audio file. 89 | * 90 | * @param src Source file path. 91 | * @param dst Destination file path. 92 | * @param extension Extension of the audio file, such as "flac". 93 | * @param operation Write operations. 94 | */ 95 | @UnstableSaltAudioTagApi 96 | fun write( 97 | src: Path, 98 | dst: Path, 99 | extension: String, 100 | vararg operation: WriteOperation 101 | ): Result = runCatching { 102 | when (extension) { 103 | "flac" -> FlacWriter().write(src, dst, *operation) 104 | else -> throw UnsupportedOperationException("Unsupported file extension $extension") 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/UnstableSaltAudioTagApi.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | @file:Suppress("unused") 19 | 20 | package com.moriafly.salt.audiotag 21 | 22 | @RequiresOptIn( 23 | message = "This Salt Audio Tag API is experimental and is likely to change or be removed in " + 24 | "the future.", 25 | level = RequiresOptIn.Level.ERROR 26 | ) 27 | @Retention(AnnotationRetention.BINARY) 28 | annotation class UnstableSaltAudioTagApi 29 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/format/flac/Flac.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | @file:Suppress( 19 | "unused", 20 | "ktlint:standard:filename", 21 | "MemberVisibilityCanBePrivate", 22 | "SpellCheckingInspection" 23 | ) 24 | 25 | package com.moriafly.salt.audiotag.format.flac 26 | 27 | import kotlinx.io.Source 28 | import kotlinx.io.bytestring.ByteString 29 | import kotlinx.io.readByteString 30 | 31 | internal class FlacSignature( 32 | source: Source 33 | ) { 34 | init { 35 | check(source.readByteString(4) == HEADER) { 36 | "Invalid FLAC header" 37 | } 38 | } 39 | 40 | companion object { 41 | /** 42 | * A FLAC bitstream consists of the fLaC (i.e., 0x664C6143) marker at the beginning of the 43 | * stream. 44 | */ 45 | val HEADER = ByteString(0x66, 0x4C, 0x61, 0x43) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/format/flac/FlacReader.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.format.flac 19 | 20 | import com.moriafly.salt.audiotag.rw.ReadStrategy 21 | import com.moriafly.salt.audiotag.rw.Reader 22 | import com.moriafly.salt.audiotag.rw.data.AudioTag 23 | import com.moriafly.salt.audiotag.rw.data.Metadata 24 | import com.moriafly.salt.audiotag.rw.data.Picture 25 | import com.moriafly.salt.audiotag.rw.data.Streaminfo 26 | import kotlinx.io.Source 27 | 28 | class FlacReader : Reader { 29 | override fun read(source: Source, strategy: ReadStrategy): AudioTag { 30 | var streaminfo: Streaminfo? = null 31 | var metadatas: List? = null 32 | var pictures: MutableList? = null 33 | 34 | var metadataBlockDataStreaminfo: MetadataBlockDataStreaminfo? = null 35 | 36 | var fileLevelMetadataLength = 0L 37 | 38 | FlacSignature(source) 39 | fileLevelMetadataLength += 4 40 | 41 | var metadataBlockHeader: MetadataBlockHeader 42 | do { 43 | metadataBlockHeader = MetadataBlockHeader.create(source) 44 | fileLevelMetadataLength += 4 45 | 46 | when (metadataBlockHeader.blockType) { 47 | BlockType.Streaminfo if strategy.streaminfo -> { 48 | metadataBlockDataStreaminfo = MetadataBlockDataStreaminfo.create(source) 49 | } 50 | 51 | BlockType.VorbisComment if strategy.metadatas -> { 52 | MetadataBlockDataVorbisComment.create(source).also { 53 | metadatas = it.userComments.mapNotNull { userComment -> 54 | val parts = userComment.split('=', limit = 2) 55 | if (parts.size == 2) { 56 | val rawField = parts[0].trim() 57 | val value = parts[1].trim() 58 | val normalizedField = rawField.uppercase() 59 | 60 | Metadata(normalizedField, value) 61 | } else { 62 | null 63 | } 64 | } 65 | } 66 | } 67 | 68 | BlockType.Picture if strategy.pictures -> { 69 | MetadataBlockDataPicture.create(source).also { 70 | if (pictures == null) { 71 | pictures = mutableListOf(it.toPicture()) 72 | } else { 73 | pictures.add(it.toPicture()) 74 | } 75 | } 76 | } 77 | 78 | else -> source.skip(metadataBlockHeader.length.toLong()) 79 | } 80 | fileLevelMetadataLength += metadataBlockHeader.length 81 | } while (!metadataBlockHeader.isLastMetadataBlock) 82 | 83 | if (metadataBlockDataStreaminfo != null) { 84 | streaminfo = Streaminfo( 85 | sampleRate = metadataBlockDataStreaminfo.sampleRate, 86 | channelCount = metadataBlockDataStreaminfo.channelCount, 87 | bits = metadataBlockDataStreaminfo.bits, 88 | sampleCount = metadataBlockDataStreaminfo.sampleCount, 89 | fileLevelMetadataLength = fileLevelMetadataLength 90 | ) 91 | } 92 | 93 | return AudioTag( 94 | streaminfo = streaminfo, 95 | metadatas = metadatas, 96 | pictures = pictures 97 | ) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/format/flac/FlacWriter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.format.flac 19 | 20 | import com.moriafly.salt.audiotag.SaltAudioTag 21 | import com.moriafly.salt.audiotag.UnstableSaltAudioTagApi 22 | import com.moriafly.salt.audiotag.rw.WriteOperation 23 | import com.moriafly.salt.audiotag.rw.Writer 24 | import com.moriafly.salt.audiotag.util.SystemFileSystemUtil 25 | import kotlinx.io.buffered 26 | import kotlinx.io.files.Path 27 | import kotlinx.io.files.SystemFileSystem 28 | import kotlinx.io.write 29 | 30 | class FlacWriter : Writer { 31 | private val metadataBlocks = mutableListOf() 32 | 33 | @UnstableSaltAudioTagApi 34 | private fun getWriteMetadataBlockDatas( 35 | operation: WriteOperation.AllMetadata? 36 | ): List { 37 | val writeMetadataBlockDatas = metadataBlocks.map { it.data }.toMutableList() 38 | 39 | if (operation == null) { 40 | return writeMetadataBlockDatas 41 | } 42 | 43 | val metadatas = operation.metadatas 44 | 45 | val vorbisCommentIndex = writeMetadataBlockDatas.indexOfFirst { 46 | it is MetadataBlockDataVorbisComment 47 | } 48 | val vorbisComment = writeMetadataBlockDatas 49 | .find { it is MetadataBlockDataVorbisComment } 50 | 51 | if (vorbisComment != null) { 52 | if (metadatas.isEmpty()) { 53 | writeMetadataBlockDatas.remove(vorbisComment) 54 | } else { 55 | // New VorbisComment 56 | val newVorbisComment = MetadataBlockDataVorbisComment( 57 | vendorString = ( 58 | vorbisComment as MetadataBlockDataVorbisComment 59 | ).vendorString, 60 | userComments = metadatas.map { it.toFlacUserComment() } 61 | ) 62 | writeMetadataBlockDatas[vorbisCommentIndex] = newVorbisComment 63 | } 64 | } else { 65 | if (metadatas.isEmpty()) { 66 | // Do nothing 67 | } else { 68 | // New VorbisComment 69 | val newVorbisComment = MetadataBlockDataVorbisComment( 70 | vendorString = VENDOR_STRING, 71 | userComments = metadatas.map { it.toFlacUserComment() } 72 | ) 73 | writeMetadataBlockDatas.add(newVorbisComment) 74 | } 75 | } 76 | 77 | return writeMetadataBlockDatas 78 | } 79 | 80 | @UnstableSaltAudioTagApi 81 | override fun write(src: Path, dst: Path, vararg operation: WriteOperation) { 82 | val tempFilePath = SystemFileSystemUtil.tempFilePath() 83 | try { 84 | SystemFileSystemUtil.copy(src, tempFilePath) 85 | 86 | SystemFileSystem.source(src).buffered().use { source -> 87 | // Read and create new metadata blocks 88 | FlacSignature(source) 89 | 90 | var metadataBlockHeader: MetadataBlockHeader 91 | do { 92 | metadataBlockHeader = MetadataBlockHeader.create(source) 93 | 94 | val metadataBlockData = when (metadataBlockHeader.blockType) { 95 | BlockType.Streaminfo -> 96 | MetadataBlockDataStreaminfo.create(source) 97 | BlockType.Padding -> 98 | MetadataBlockDataPadding.create(source, metadataBlockHeader.length) 99 | BlockType.Application -> 100 | MetadataBlockDataApplication.create(source, metadataBlockHeader.length) 101 | BlockType.Seektable -> 102 | MetadataBlockDataSeektable.create(source, metadataBlockHeader.length) 103 | BlockType.VorbisComment -> 104 | MetadataBlockDataVorbisComment.create(source) 105 | BlockType.Cuesheet -> 106 | MetadataBlockDataCuesheet.create(source, metadataBlockHeader.length) 107 | BlockType.Picture -> 108 | MetadataBlockDataPicture.create(source) 109 | else -> 110 | error( 111 | "Unsupported metadata block type: ${metadataBlockHeader.blockType}" 112 | ) 113 | } 114 | 115 | metadataBlocks.add( 116 | MetadataBlock( 117 | header = metadataBlockHeader, 118 | data = metadataBlockData 119 | ) 120 | ) 121 | } while (!metadataBlockHeader.isLastMetadataBlock) 122 | 123 | val operationAllMetadata = operation 124 | .find { it is WriteOperation.AllMetadata } 125 | as WriteOperation.AllMetadata? 126 | 127 | val writeMetadataBlockDatas = getWriteMetadataBlockDatas(operationAllMetadata) 128 | 129 | // Write to temp 130 | SystemFileSystem.sink(tempFilePath).buffered().use { sink -> 131 | sink.write(FlacSignature.HEADER) 132 | writeMetadataBlockDatas.forEachIndexed { index, data -> 133 | val dataByteString = data.toByteString() 134 | 135 | sink.write( 136 | MetadataBlockHeader( 137 | isLastMetadataBlock = index == writeMetadataBlockDatas.lastIndex, 138 | blockType = data.blockType, 139 | length = dataByteString.size 140 | ).toByteString() 141 | ) 142 | sink.write(dataByteString) 143 | } 144 | 145 | sink.transferFrom(source) 146 | } 147 | 148 | // Atomic move 149 | SystemFileSystem.atomicMove(tempFilePath, dst) 150 | } 151 | } finally { 152 | if (SystemFileSystem.exists(tempFilePath)) { 153 | SystemFileSystem.delete(tempFilePath) 154 | } 155 | } 156 | } 157 | 158 | companion object { 159 | private val VENDOR_STRING = "Salt Audio Tag ${SaltAudioTag.getVersionName()}" 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/format/flac/MetadataBlock.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | @file:Suppress( 19 | "unused", 20 | "ktlint:standard:filename", 21 | "MemberVisibilityCanBePrivate", 22 | "SpellCheckingInspection" 23 | ) 24 | 25 | package com.moriafly.salt.audiotag.format.flac 26 | 27 | import com.moriafly.salt.audiotag.rw.CanWrite 28 | import com.moriafly.salt.audiotag.rw.data.Picture 29 | import com.moriafly.salt.audiotag.util.writeInt24 30 | import kotlinx.io.Buffer 31 | import kotlinx.io.Source 32 | import kotlinx.io.bytestring.ByteString 33 | import kotlinx.io.bytestring.encodeToByteString 34 | import kotlinx.io.bytestring.hexToByteString 35 | import kotlinx.io.bytestring.toHexString 36 | import kotlinx.io.readByteArray 37 | import kotlinx.io.readByteString 38 | import kotlinx.io.readString 39 | import kotlinx.io.readUIntLe 40 | import kotlinx.io.write 41 | import kotlinx.io.writeIntLe 42 | 43 | internal class MetadataBlock( 44 | val header: MetadataBlockHeader, 45 | val data: MetadataBlockData 46 | ) 47 | 48 | internal sealed class BlockType( 49 | val value: Int 50 | ) { 51 | data object Streaminfo : BlockType(BLOCK_TYPE_STREAMINFO) 52 | 53 | data object Padding : BlockType(BLOCK_TYPE_PADDING) 54 | 55 | data object Application : BlockType(BLOCK_TYPE_APPLICATION) 56 | 57 | data object Seektable : BlockType(BLOCK_TYPE_SEEKTABLE) 58 | 59 | data object VorbisComment : BlockType(BLOCK_TYPE_VORBIS_COMMENT) 60 | 61 | data object Cuesheet : BlockType(BLOCK_TYPE_CUESHEET) 62 | 63 | data object Picture : BlockType(BLOCK_TYPE_PICTURE) 64 | 65 | data object Invalid : BlockType(BLOCK_TYPE_INVALID) 66 | 67 | companion object { 68 | private const val BLOCK_TYPE_STREAMINFO = 0 69 | private const val BLOCK_TYPE_PADDING = 1 70 | private const val BLOCK_TYPE_APPLICATION = 2 71 | private const val BLOCK_TYPE_SEEKTABLE = 3 72 | private const val BLOCK_TYPE_VORBIS_COMMENT = 4 73 | private const val BLOCK_TYPE_CUESHEET = 5 74 | private const val BLOCK_TYPE_PICTURE = 6 75 | 76 | /** 77 | * Forbidden (to avoid confusion with a frame sync code). 78 | */ 79 | private const val BLOCK_TYPE_INVALID = 127 80 | 81 | fun from(value: Int): BlockType = when (value) { 82 | BLOCK_TYPE_STREAMINFO -> Streaminfo 83 | BLOCK_TYPE_PADDING -> Padding 84 | BLOCK_TYPE_APPLICATION -> Application 85 | BLOCK_TYPE_SEEKTABLE -> Seektable 86 | BLOCK_TYPE_VORBIS_COMMENT -> VorbisComment 87 | BLOCK_TYPE_CUESHEET -> Cuesheet 88 | BLOCK_TYPE_PICTURE -> Picture 89 | BLOCK_TYPE_INVALID -> Invalid 90 | else -> error("Unsupported block type: $value.") 91 | } 92 | } 93 | } 94 | 95 | internal data class MetadataBlockHeader( 96 | val isLastMetadataBlock: Boolean, 97 | val blockType: BlockType, 98 | val length: Int 99 | ) : CanWrite { 100 | override fun toByteString(): ByteString = Buffer() 101 | .apply { 102 | writeByte( 103 | ((if (isLastMetadataBlock) 0x80 else 0x00) or (blockType.value and 0x7F)).toByte() 104 | ) 105 | writeByte(((length ushr 16) and 0xFF).toByte()) 106 | writeByte(((length ushr 8) and 0xFF).toByte()) 107 | writeByte((length and 0xFF).toByte()) 108 | } 109 | .readByteString() 110 | 111 | @Suppress("SpellCheckingInspection") 112 | companion object { 113 | fun create(source: Source): MetadataBlockHeader { 114 | val byteString = source.readByteString(4) 115 | 116 | val isLastMetadataBlock = (byteString[0].toInt() and 0b10000000 shr 7) == 1 117 | val blockType = byteString[0].toInt() and 0b01111111 118 | val length = (byteString[1].toInt() and 0xFF shl 16) or 119 | (byteString[2].toInt() and 0xFF shl 8) or 120 | (byteString[3].toInt() and 0xFF) 121 | return MetadataBlockHeader( 122 | isLastMetadataBlock = isLastMetadataBlock, 123 | blockType = BlockType.from(blockType), 124 | length = length 125 | ) 126 | } 127 | } 128 | } 129 | 130 | internal abstract class MetadataBlockData( 131 | val blockType: BlockType 132 | ) : CanWrite 133 | 134 | /** 135 | * The streaminfo metadata block has information about the whole stream, such as sample rate, 136 | * number of channels, total number of samples, etc. It MUST be present as the first metadata block 137 | * in the stream. Other metadata blocks MAY follow. There MUST be no more than one streaminfo 138 | * metadata block per FLAC stream. 139 | * 140 | * @property minBlockSize Samples. 141 | * @property maxBlockSize Samples. 142 | * @property minFrameSize Bytes. 143 | * @property maxFrameSize Bytes. 144 | * @property sampleRate Max 655350 Hz. 145 | * @property channelCount 2 to 8. 146 | * @property bits Bits per sample, 4 to 32. 147 | * @property sampleCount TODO. 148 | * @property unencodedAudioDataMd5Checksum MD5 checksum of the unencoded audio data. This allows the 149 | * decoder to determine if an error exists in the audio data even when, despite the error, the 150 | * bitstream itself is valid. A value of 0 signifies that the value is not known. 151 | * 152 | * 16 bytes (128 bits) HASH. 153 | */ 154 | internal data class MetadataBlockDataStreaminfo( 155 | val minBlockSize: Int, 156 | val maxBlockSize: Int, 157 | val minFrameSize: Int, 158 | val maxFrameSize: Int, 159 | val sampleRate: Int, 160 | val channelCount: Int, 161 | val bits: Int, 162 | val sampleCount: Long, 163 | val unencodedAudioDataMd5Checksum: String 164 | ) : MetadataBlockData(BlockType.Streaminfo) { 165 | override fun toByteString(): ByteString = Buffer() 166 | .apply { 167 | writeShort(minBlockSize.toShort()) 168 | writeShort(maxBlockSize.toShort()) 169 | 170 | writeInt24(minFrameSize) 171 | writeInt24(maxFrameSize) 172 | 173 | // Write sampleRate (20 bits), channelCount (3 bits), and bits (5 bits). 174 | val sampleRateHigh = (sampleRate shr 12).toByte() 175 | val sampleRateMid = (sampleRate shr 4).toByte() 176 | val sampleRateLow = (sampleRate and 0x0F) shl 4 177 | val channelBits = ((channelCount - 1) shl 1) or ((bits - 1) shr 4) 178 | writeByte(sampleRateHigh) 179 | writeByte(sampleRateMid) 180 | writeByte((sampleRateLow or channelBits).toByte()) 181 | 182 | // Write bits lower 4 bits and sampleCount higher 4 bits. 183 | val bitsLower = ((bits - 1) and 0x0F) shl 4 184 | val sampleCountHigher = (sampleCount shr 32).toInt() and 0x0F 185 | writeByte((bitsLower or sampleCountHigher).toByte()) 186 | 187 | // Write remaining sampleCount (32 bits). 188 | writeInt((sampleCount and 0xFFFFFFFFL).toInt()) 189 | 190 | // Write MD5 checksum (16 bytes). 191 | @OptIn(ExperimentalStdlibApi::class) 192 | val md5 = unencodedAudioDataMd5Checksum.hexToByteString() 193 | write(md5) 194 | } 195 | .readByteString() 196 | 197 | init { 198 | @Suppress("ConvertTwoComparisonsToRangeCheck") 199 | require( 200 | 16 <= minBlockSize && 201 | minBlockSize <= maxBlockSize && 202 | maxBlockSize <= 65535 203 | ) { 204 | "The minimum block size and the maximum block size MUST be in the 16-65535 range. " + 205 | "The minimum block size MUST be equal to or less than the maximum block size" 206 | } 207 | 208 | require(unencodedAudioDataMd5Checksum.length == 32) { 209 | "MD5 checksum must be 32 characters long." 210 | } 211 | } 212 | 213 | companion object { 214 | fun create(source: Source): MetadataBlockDataStreaminfo { 215 | val byteString = source.readByteString(34) 216 | 217 | val minBlockSize = (byteString[0].toInt() and 0xFF shl 8) or 218 | (byteString[1].toInt() and 0xFF) 219 | val maxBlockSize = (byteString[2].toInt() and 0xFF shl 8) or 220 | (byteString[3].toInt() and 0xFF) 221 | 222 | val minFrameSize = (byteString[4].toInt() and 0xFF shl 16) or 223 | (byteString[5].toInt() and 0xFF shl 8) or 224 | (byteString[6].toInt() and 0xFF) 225 | val maxFrameSize = (byteString[7].toInt() and 0xFF shl 16) or 226 | (byteString[8].toInt() and 0xFF shl 8) or 227 | (byteString[9].toInt() and 0xFF) 228 | val sampleRate = (byteString[10].toInt() and 0xFF shl 12) or 229 | (byteString[11].toInt() and 0xFF shl 4) or 230 | (byteString[12].toInt() and 0xFF shr 4) 231 | val channelCount = (byteString[12].toInt() and 0x0F shr 1) + 1 232 | val bits = ( 233 | (byteString[12].toInt() and 0x1 shl 4) or 234 | (byteString[13].toInt() and 0xF0 shr 4) 235 | ) + 1 236 | val sampleCount = (byteString[13].toLong() and 0x0F shl 32) or 237 | (byteString[14].toLong() and 0xFF shl 24) or 238 | (byteString[15].toLong() and 0xFF shl 16) or 239 | (byteString[16].toLong() and 0xFF shl 8) or 240 | (byteString[17].toLong() and 0xFF) 241 | 242 | @OptIn(ExperimentalStdlibApi::class) 243 | val unencodedAudioDataMd5Checksum = byteString.substring(18, 34).toHexString() 244 | 245 | return MetadataBlockDataStreaminfo( 246 | minBlockSize = minBlockSize, 247 | maxBlockSize = maxBlockSize, 248 | minFrameSize = minFrameSize, 249 | maxFrameSize = maxFrameSize, 250 | sampleRate = sampleRate, 251 | channelCount = channelCount, 252 | bits = bits, 253 | sampleCount = sampleCount, 254 | unencodedAudioDataMd5Checksum = unencodedAudioDataMd5Checksum 255 | ) 256 | } 257 | } 258 | } 259 | 260 | internal data class MetadataBlockDataPadding( 261 | val length: Int 262 | ) : MetadataBlockData(BlockType.Padding) { 263 | override fun toByteString(): ByteString = Buffer() 264 | .apply { 265 | repeat(length) { 266 | writeByte(0) 267 | } 268 | } 269 | .readByteString() 270 | 271 | companion object { 272 | fun create(source: Source, length: Int): MetadataBlockDataPadding { 273 | source.skip(length.toLong()) 274 | return MetadataBlockDataPadding(length) 275 | } 276 | } 277 | } 278 | 279 | internal data class MetadataBlockDataApplication( 280 | val id: Int, 281 | val data: ByteString 282 | ) : MetadataBlockData(BlockType.Application) { 283 | override fun toByteString(): ByteString = Buffer() 284 | .apply { 285 | writeInt(id) 286 | write(data) 287 | } 288 | .readByteString() 289 | 290 | companion object { 291 | fun create(source: Source, length: Int): MetadataBlockDataApplication { 292 | val id = source.readInt() 293 | val data = source.readByteString(length - 4) 294 | return MetadataBlockDataApplication(id, data) 295 | } 296 | } 297 | } 298 | 299 | internal data class MetadataBlockDataSeektable( 300 | val seekPoints: List 301 | ) : MetadataBlockData(BlockType.Seektable) { 302 | override fun toByteString(): ByteString = Buffer() 303 | .apply { 304 | seekPoints.forEach { 305 | write(it.toByteString()) 306 | } 307 | } 308 | .readByteString() 309 | 310 | data class SeekPoint( 311 | val sampleNumber: Long, 312 | val offset: Long, 313 | val number: Short 314 | ) : CanWrite { 315 | override fun toByteString(): ByteString = Buffer() 316 | .apply { 317 | writeLong(sampleNumber) 318 | writeLong(offset) 319 | writeShort(number) 320 | } 321 | .readByteString() 322 | } 323 | 324 | companion object { 325 | fun create(source: Source, length: Int): MetadataBlockDataSeektable { 326 | val size = length / 18 327 | return MetadataBlockDataSeektable( 328 | seekPoints = (0 until size).map { 329 | SeekPoint( 330 | sampleNumber = source.readLong(), 331 | offset = source.readLong(), 332 | number = source.readShort() 333 | ) 334 | } 335 | ) 336 | } 337 | } 338 | } 339 | 340 | /** 341 | * FLAC tags, without the framing bit. 342 | * 343 | * [Ogg Vorbis](https://www.xiph.org/vorbis/doc/v-comment.html) 344 | */ 345 | internal data class MetadataBlockDataVorbisComment( 346 | val vendorString: String, 347 | val userComments: List 348 | ) : MetadataBlockData(BlockType.VorbisComment) { 349 | override fun toByteString(): ByteString = Buffer() 350 | .apply { 351 | val vendorByteString = vendorString.encodeToByteString() 352 | writeIntLe(vendorByteString.size) 353 | write(vendorByteString) 354 | 355 | writeIntLe(userComments.size) 356 | 357 | userComments.forEach { comment -> 358 | val commentByteString = comment.encodeToByteString() 359 | writeIntLe(commentByteString.size) 360 | write(commentByteString) 361 | } 362 | } 363 | .readByteString() 364 | 365 | companion object { 366 | fun create(source: Source): MetadataBlockDataVorbisComment { 367 | val vendorLength = source.readUIntLe().toLong() 368 | val vendorString = source.readString(vendorLength) 369 | 370 | val userCommentListLength = source.readUIntLe().toLong() 371 | val userComments = ArrayList(userCommentListLength.toInt()) 372 | 373 | for (i in 0 until userCommentListLength) { 374 | val userCommentLength = source.readUIntLe().toLong() 375 | val userComment = source.readString(userCommentLength) 376 | userComments.add(userComment) 377 | } 378 | 379 | return MetadataBlockDataVorbisComment(vendorString, userComments) 380 | } 381 | } 382 | } 383 | 384 | /** 385 | * TODO. 386 | */ 387 | internal data class MetadataBlockDataCuesheet( 388 | val byteString: ByteString 389 | ) : MetadataBlockData(BlockType.Cuesheet) { 390 | override fun toByteString(): ByteString = byteString 391 | 392 | companion object { 393 | fun create( 394 | source: Source, 395 | length: Int 396 | ): MetadataBlockDataCuesheet = MetadataBlockDataCuesheet(source.readByteString(length)) 397 | } 398 | } 399 | 400 | internal data class MetadataBlockDataPicture( 401 | val pictureType: Int, 402 | val mediaType: String, 403 | val description: String, 404 | val width: Int, 405 | val height: Int, 406 | val colorDepth: Int, 407 | val colorsNumber: Int, 408 | val pictureData: ByteArray 409 | ) : MetadataBlockData(BlockType.Picture) { 410 | override fun toByteString(): ByteString = Buffer() 411 | .apply { 412 | writeInt(pictureType) 413 | val mediaTypeByteString = mediaType.encodeToByteString() 414 | writeInt(mediaTypeByteString.size) 415 | write(mediaTypeByteString) 416 | val descriptionByteString = description.encodeToByteString() 417 | writeInt(descriptionByteString.size) 418 | write(descriptionByteString) 419 | writeInt(width) 420 | writeInt(height) 421 | writeInt(colorDepth) 422 | writeInt(colorsNumber) 423 | writeInt(pictureData.size) 424 | write(pictureData) 425 | } 426 | .readByteString() 427 | 428 | fun toPicture(): Picture = Picture( 429 | pictureType = when (pictureType) { 430 | 0 -> Picture.PictureType.Other 431 | 1 -> Picture.PictureType.PngFileIcon32x32 432 | 2 -> Picture.PictureType.GeneralFileIcon 433 | 3 -> Picture.PictureType.FrontCover 434 | 4 -> Picture.PictureType.BackCover 435 | 5 -> Picture.PictureType.LinerNotesPage 436 | 6 -> Picture.PictureType.MediaLabel 437 | 7 -> Picture.PictureType.Lead 438 | 8 -> Picture.PictureType.Artist 439 | 9 -> Picture.PictureType.Conductor 440 | 10 -> Picture.PictureType.Band 441 | 11 -> Picture.PictureType.Composer 442 | 12 -> Picture.PictureType.Lyricist 443 | 13 -> Picture.PictureType.RecordingLocation 444 | 14 -> Picture.PictureType.DuringRecording 445 | 15 -> Picture.PictureType.DuringPerformance 446 | 16 -> Picture.PictureType.MovieScreenCapture 447 | 17 -> Picture.PictureType.BrightColoredFish 448 | 18 -> Picture.PictureType.Illustration 449 | 19 -> Picture.PictureType.BandLogo 450 | 20 -> Picture.PictureType.PublisherLogotype 451 | else -> Picture.PictureType.Unknown 452 | }, 453 | mediaType = mediaType, 454 | description = description, 455 | width = width, 456 | height = height, 457 | colorDepth = colorDepth, 458 | colorsNumber = colorsNumber, 459 | pictureData = pictureData 460 | ) 461 | 462 | override fun equals(other: Any?): Boolean { 463 | if (this === other) return true 464 | if (other == null || this::class != other::class) return false 465 | 466 | other as MetadataBlockDataPicture 467 | 468 | if (pictureType != other.pictureType) return false 469 | if (mediaType != other.mediaType) return false 470 | if (description != other.description) return false 471 | if (width != other.width) return false 472 | if (height != other.height) return false 473 | if (colorDepth != other.colorDepth) return false 474 | if (colorsNumber != other.colorsNumber) return false 475 | if (!pictureData.contentEquals(other.pictureData)) return false 476 | 477 | return true 478 | } 479 | 480 | override fun hashCode(): Int { 481 | var result = pictureType 482 | result = 31 * result + mediaType.hashCode() 483 | result = 31 * result + description.hashCode() 484 | result = 31 * result + width 485 | result = 31 * result + height 486 | result = 31 * result + colorDepth 487 | result = 31 * result + colorsNumber 488 | result = 31 * result + pictureData.contentHashCode() 489 | return result 490 | } 491 | 492 | companion object { 493 | fun create(source: Source): MetadataBlockDataPicture { 494 | val pictureType = source.readInt() 495 | val mediaTypeLength = source.readInt() 496 | val mediaType = source.readString(mediaTypeLength.toLong()) 497 | val descriptionLength = source.readInt() 498 | val description = source.readString(descriptionLength.toLong()) 499 | val width = source.readInt() 500 | val height = source.readInt() 501 | val colorDepth = source.readInt() 502 | val colorsNumber = source.readInt() 503 | val pictureDataLength = source.readInt() 504 | val pictureData = source.readByteArray(pictureDataLength) 505 | 506 | return MetadataBlockDataPicture( 507 | pictureType = pictureType, 508 | mediaType = mediaType, 509 | description = description, 510 | width = width, 511 | height = height, 512 | colorDepth = colorDepth, 513 | colorsNumber = colorsNumber, 514 | pictureData = pictureData 515 | ) 516 | } 517 | } 518 | } 519 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/rw/CanWrite.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.rw 19 | 20 | import kotlinx.io.bytestring.ByteString 21 | 22 | internal interface CanWrite { 23 | fun toByteString(): ByteString 24 | } 25 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/rw/ReadStrategy.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | @file:Suppress("unused") 19 | 20 | package com.moriafly.salt.audiotag.rw 21 | 22 | /** 23 | * The strategy for reading audio files. 24 | * 25 | * @property streaminfo Whether to read the streaminfo block. 26 | * @property metadatas Whether to read the metadata block. 27 | * @property pictures Whether to read the picture block. 28 | * 29 | * @author Moriafly 30 | */ 31 | data class ReadStrategy( 32 | val streaminfo: Boolean, 33 | val metadatas: Boolean, 34 | val pictures: Boolean 35 | ) { 36 | companion object { 37 | val IgnorePicture = ReadStrategy( 38 | streaminfo = true, 39 | metadatas = true, 40 | pictures = false 41 | ) 42 | 43 | val OnlyPicture = ReadStrategy( 44 | streaminfo = false, 45 | metadatas = false, 46 | pictures = true 47 | ) 48 | 49 | val All = ReadStrategy( 50 | streaminfo = true, 51 | metadatas = true, 52 | pictures = true 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/rw/Reader.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.rw 19 | 20 | import com.moriafly.salt.audiotag.rw.data.AudioTag 21 | import kotlinx.io.Source 22 | 23 | internal interface Reader { 24 | fun read(source: Source, strategy: ReadStrategy): AudioTag 25 | } 26 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/rw/WriteOperation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | @file:Suppress("unused") 19 | 20 | package com.moriafly.salt.audiotag.rw 21 | 22 | import com.moriafly.salt.audiotag.UnstableSaltAudioTagApi 23 | import com.moriafly.salt.audiotag.rw.data.Metadata 24 | import com.moriafly.salt.audiotag.util.format 25 | 26 | @UnstableSaltAudioTagApi 27 | sealed class WriteOperation { 28 | class AllMetadata private constructor( 29 | val metadatas: List 30 | ) : WriteOperation() { 31 | companion object { 32 | fun create( 33 | metadatas: List 34 | ): AllMetadata = AllMetadata(metadatas.format()) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/rw/Writer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.rw 19 | 20 | import com.moriafly.salt.audiotag.UnstableSaltAudioTagApi 21 | import kotlinx.io.files.Path 22 | import kotlin.uuid.ExperimentalUuidApi 23 | 24 | internal interface Writer { 25 | /** 26 | * @param src The source file path. 27 | * @param dst The destination file path. 28 | * @param operation The operation to perform. 29 | */ 30 | @OptIn(ExperimentalUuidApi::class) 31 | @UnstableSaltAudioTagApi 32 | fun write(src: Path, dst: Path, vararg operation: WriteOperation) 33 | } 34 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/rw/data/AudioTag.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.rw.data 19 | 20 | import com.moriafly.salt.audiotag.rw.ReadStrategy 21 | 22 | /** 23 | * The audio tag. 24 | * 25 | * @property streaminfo The streaminfo, null if error or [ReadStrategy.streaminfo] is false. 26 | * @property metadatas The metadata, null if error or [ReadStrategy.metadatas] is false. 27 | * @property pictures The picture, null if error or [ReadStrategy.pictures] is false. 28 | * 29 | * @see com.moriafly.salt.audiotag.SaltAudioTag.read 30 | */ 31 | data class AudioTag( 32 | val streaminfo: Streaminfo?, 33 | val metadatas: List?, 34 | val pictures: List? 35 | ) 36 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/rw/data/Metadata.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | @file:Suppress("unused") 19 | 20 | package com.moriafly.salt.audiotag.rw.data 21 | 22 | @Suppress("SpellCheckingInspection") 23 | data class Metadata( 24 | val key: String, 25 | val value: String 26 | ) { 27 | fun toFlacUserComment(): String = "$key=$value" 28 | 29 | /** 30 | * Check if the metadata is valid. 31 | */ 32 | fun isValid(): Boolean = key.isNotBlank() && value.isNotBlank() 33 | 34 | /** 35 | * Format the metadata. 36 | * 37 | * @return The formatted metadata, null if the metadata is invalid. 38 | */ 39 | fun format(): Metadata? = if (this.isValid()) { 40 | Metadata( 41 | key = key.trim(), 42 | value = value.trim() 43 | ) 44 | } else { 45 | null 46 | } 47 | 48 | companion object { 49 | const val TITLE = "TITLE" 50 | const val VERSION = "VERSION" 51 | const val ALBUM = "ALBUM" 52 | const val TRACKNUMBER = "TRACKNUMBER" 53 | const val ARTIST = "ARTIST" 54 | const val PERFORMER = "PERFORMER" 55 | const val COPYRIGHT = "COPYRIGHT" 56 | const val LICENSE = "LICENSE" 57 | const val ORGANIZATION = "ORGANIZATION" 58 | const val DESCRIPTION = "DESCRIPTION" 59 | const val GENRE = "GENRE" 60 | const val DATE = "DATE" 61 | const val LOCATION = "LOCATION" 62 | const val CONTACT = "CONTACT" 63 | const val ISRC = "ISRC" 64 | const val LYRICS = "LYRICS" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/rw/data/Picture.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | @file:Suppress("unused") 19 | 20 | package com.moriafly.salt.audiotag.rw.data 21 | 22 | /** 23 | * Picture. 24 | * 25 | * @property pictureType Picture type. 26 | * @property mediaType Media type. 27 | * @property description Description. 28 | * @property width Width. 29 | * @property height Height. 30 | * @property colorDepth Color depth. 31 | * @property colorsNumber Colors number. 32 | * @property pictureData Picture data. 33 | */ 34 | data class Picture( 35 | val pictureType: PictureType, 36 | val mediaType: String, 37 | val description: String, 38 | val width: Int, 39 | val height: Int, 40 | val colorDepth: Int, 41 | val colorsNumber: Int, 42 | val pictureData: ByteArray 43 | ) { 44 | enum class PictureType { 45 | /** 46 | * Other. 47 | */ 48 | Other, 49 | 50 | /** 51 | * PNG file icon of 32x32 pixels. 52 | * 53 | * See [RFC2083](https://www.rfc-editor.org/rfc/rfc9639.html#RFC2083). 54 | */ 55 | PngFileIcon32x32, 56 | 57 | /** 58 | * General file icon. 59 | */ 60 | GeneralFileIcon, 61 | 62 | /** 63 | * Front cover. 64 | */ 65 | FrontCover, 66 | 67 | /** 68 | * Back cover. 69 | */ 70 | BackCover, 71 | 72 | /** 73 | * Liner notes page. 74 | */ 75 | LinerNotesPage, 76 | 77 | /** 78 | * Media label (e.g., CD, Vinyl or Cassette label). 79 | */ 80 | MediaLabel, 81 | 82 | /** 83 | * Lead artist, lead performer, or soloist. 84 | */ 85 | Lead, 86 | 87 | /** 88 | * Artist or performer. 89 | */ 90 | Artist, 91 | 92 | /** 93 | * Conductor. 94 | */ 95 | Conductor, 96 | 97 | /** 98 | * Band or orchestra. 99 | */ 100 | Band, 101 | 102 | /** 103 | * Composer. 104 | */ 105 | Composer, 106 | 107 | /** 108 | * Lyricist or text writer. 109 | */ 110 | Lyricist, 111 | 112 | /** 113 | * Recording location. 114 | */ 115 | RecordingLocation, 116 | 117 | /** 118 | * During recording. 119 | */ 120 | DuringRecording, 121 | 122 | /** 123 | * During performance. 124 | */ 125 | DuringPerformance, 126 | 127 | /** 128 | * Movie or video screen capture. 129 | */ 130 | MovieScreenCapture, 131 | 132 | /** 133 | * A bright colored fish. 134 | */ 135 | BrightColoredFish, 136 | 137 | /** 138 | * Illustration. 139 | */ 140 | Illustration, 141 | 142 | /** 143 | * Band or artist logotype. 144 | */ 145 | BandLogo, 146 | 147 | /** 148 | * Publisher or studio logotype. 149 | */ 150 | PublisherLogotype, 151 | 152 | Unknown 153 | } 154 | 155 | override fun equals(other: Any?): Boolean { 156 | if (this === other) return true 157 | if (other == null || this::class != other::class) return false 158 | 159 | other as Picture 160 | 161 | if (pictureType != other.pictureType) return false 162 | if (mediaType != other.mediaType) return false 163 | if (description != other.description) return false 164 | if (width != other.width) return false 165 | if (height != other.height) return false 166 | if (colorDepth != other.colorDepth) return false 167 | if (colorsNumber != other.colorsNumber) return false 168 | if (!pictureData.contentEquals(other.pictureData)) return false 169 | 170 | return true 171 | } 172 | 173 | override fun hashCode(): Int { 174 | var result = pictureType.hashCode() 175 | result = 31 * result + mediaType.hashCode() 176 | result = 31 * result + description.hashCode() 177 | result = 31 * result + width 178 | result = 31 * result + height 179 | result = 31 * result + colorDepth 180 | result = 31 * result + colorsNumber 181 | result = 31 * result + pictureData.contentHashCode() 182 | return result 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/rw/data/Streaminfo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | @file:Suppress("unused") 19 | 20 | package com.moriafly.salt.audiotag.rw.data 21 | 22 | import com.moriafly.salt.audiotag.UnstableSaltAudioTagApi 23 | 24 | /** 25 | * @author Moriafly 26 | * 27 | * @property fileLevelMetadataLength file level metadata length, bytes. 28 | */ 29 | data class Streaminfo( 30 | val sampleRate: Int, 31 | val channelCount: Int, 32 | val bits: Int, 33 | val sampleCount: Long, 34 | val fileLevelMetadataLength: Long 35 | ) 36 | 37 | /** 38 | * Get seconds of streaminfo. 39 | */ 40 | val Streaminfo.seconds: Float 41 | get() = sampleCount.toFloat() / sampleRate 42 | 43 | /** 44 | * Get duration of streaminfo, ms. 45 | */ 46 | val Streaminfo.duration: Long 47 | get() = (seconds * 1000).toLong() 48 | 49 | /** 50 | * Guess bitrate of streaminfo. bps. 51 | * 52 | * 1 kbps = 1000 bps 53 | * 54 | * @throws IllegalArgumentException if fileSize is negative or seconds is not positive. 55 | */ 56 | @UnstableSaltAudioTagApi 57 | fun Streaminfo.guessAverageBitrate(fileSize: Long): Float { 58 | require(fileSize >= 0) { "fileSize must be non-negative" } 59 | require(seconds > 0) { "seconds must be positive" } 60 | 61 | val metadataLength = fileLevelMetadataLength.coerceAtMost(fileSize) 62 | val effectiveSize = (fileSize - metadataLength).coerceAtLeast(0L) 63 | return (effectiveSize * 8) / seconds 64 | } 65 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/util/CharsetsUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.util 19 | 20 | // import java.nio.charset.Charset 21 | // 22 | // /** 23 | // * @see sun.nio.cs.GBK 24 | // */ 25 | // val Charsets.GBK: Charset 26 | // get() = charset("GBK") 27 | // 28 | // /** 29 | // * @see sun.nio.cs.GB18030 30 | // */ 31 | // val Charsets.GB18030: Charset 32 | // get() = charset("GB18030") 33 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/util/MetadataUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.util 19 | 20 | import com.moriafly.salt.audiotag.rw.data.Metadata 21 | 22 | internal fun List.format(): List = this.mapNotNull { 23 | it.format() 24 | } 25 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/util/PathUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | @file:Suppress("unused") 19 | 20 | package com.moriafly.salt.audiotag.util 21 | 22 | import kotlinx.io.files.Path 23 | 24 | val Path.extension: String 25 | get() = name.substringAfterLast('.', missingDelimiterValue = "").lowercase() 26 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/util/SinkUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.util 19 | 20 | import kotlinx.io.Sink 21 | 22 | /** 23 | * Writes three bytes containing [int], in the big-endian order, to this sink. 24 | */ 25 | internal fun Sink.writeInt24(int: Int) { 26 | require(int in 0..0xFFFFFF) { "The int must fit in 24 bits (0..16777215)." } 27 | writeByte(((int ushr 16) and 0xFF).toByte()) 28 | writeByte(((int ushr 8) and 0xFF).toByte()) 29 | writeByte((int and 0xFF).toByte()) 30 | } 31 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/com/moriafly/salt/audiotag/util/SystemFileSystemUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | @file:Suppress("unused") 19 | 20 | package com.moriafly.salt.audiotag.util 21 | 22 | import kotlinx.io.IOException 23 | import kotlinx.io.Source 24 | import kotlinx.io.buffered 25 | import kotlinx.io.files.FileNotFoundException 26 | import kotlinx.io.files.Path 27 | import kotlinx.io.files.SystemFileSystem 28 | import kotlinx.io.files.SystemTemporaryDirectory 29 | import kotlin.uuid.ExperimentalUuidApi 30 | import kotlin.uuid.Uuid 31 | 32 | /** 33 | * System file system utilities. 34 | * 35 | * @author Moriafly 36 | */ 37 | object SystemFileSystemUtil { 38 | /** 39 | * Create a temporary file path. 40 | * 41 | * Example: 42 | * 43 | * - C:\Users\moria\AppData\Local\Temp\06be772dd5dc49b3af958f698d908c81 44 | * - /data/user/0/com.moriafly.salt.audiotag/cache/06be772dd5dc49b3af958f698d908c81 45 | */ 46 | @OptIn(ExperimentalUuidApi::class) 47 | fun tempFilePath(): Path { 48 | // 32 chars. 49 | val fileName = Uuid.random().toHexString() 50 | return Path(SystemTemporaryDirectory, fileName) 51 | } 52 | 53 | /** 54 | * Copy regular file [src] to [dst]. 55 | * 56 | * TODO: https://github.com/Kotlin/kotlinx-io/issues/233 57 | * 58 | * @throws FileNotFoundException When [src] does not exist. 59 | * @throws IOException When failed to read [src] or [src] is not a regular file. When it's not 60 | * possible to open the [dst] for writing. When some I/O error occurs. 61 | */ 62 | fun copy(src: Path, dst: Path) { 63 | val metadata = SystemFileSystem.metadataOrNull(src) 64 | ?: throw IOException("Failed to read metadata of $src") 65 | 66 | if (!metadata.isRegularFile) { 67 | throw IOException("Source $src must be a regular file") 68 | } 69 | 70 | SystemFileSystem.source(src).buffered().use { source -> 71 | SystemFileSystem.sink(dst).buffered().use { sink -> 72 | sink.write(source, metadata.size) 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * Write [source] to [dst]. 79 | */ 80 | fun write(source: Source, dst: Path) { 81 | SystemFileSystem.sink(dst).buffered().use { sink -> 82 | sink.transferFrom(source) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/com/moriafly/salt/audiotag/format/flac/FlacTest.kt: -------------------------------------------------------------------------------- 1 | package com.moriafly.salt.audiotag.format.flac 2 | 3 | import com.moriafly.salt.audiotag.SaltAudioTag 4 | import com.moriafly.salt.audiotag.UnstableSaltAudioTagApi 5 | import com.moriafly.salt.audiotag.rw.ReadStrategy 6 | import com.moriafly.salt.audiotag.rw.WriteOperation 7 | import com.moriafly.salt.audiotag.rw.data.Metadata 8 | import kotlinx.io.buffered 9 | import kotlinx.io.files.Path 10 | import kotlinx.io.files.SystemFileSystem 11 | import kotlin.test.Test 12 | 13 | @OptIn(UnstableSaltAudioTagApi::class) 14 | class FlacTest { 15 | private val path = Path("C:\\Users\\moria\\Music\\Pig小优 - 画舫烟中浅.flac") 16 | 17 | @Test 18 | fun testRead() { 19 | val audioTag = SaltAudioTag.read( 20 | path = path, 21 | extension = "flac", 22 | strategy = ReadStrategy.All 23 | ).getOrThrow() 24 | 25 | audioTag.metadatas?.forEach { metadata -> 26 | println("key = ${metadata.key} value = ${metadata.value}") 27 | } 28 | } 29 | 30 | @Test 31 | fun testWrite() { 32 | // val outputPath = Path("C:\\Users\\moria\\Desktop\\G.E.M.邓紫棋 - 桃花诺_output.flac") 33 | // val audioFile = SaltAudioTag.create( 34 | // path = path, 35 | // rwStrategy = RwStrategy.ReadWriteAll 36 | // ) 37 | // audioFile.write(outputPath) 38 | } 39 | 40 | @Test 41 | fun testWriteAddArtist() { 42 | val result = SystemFileSystem.source(path).buffered().use { 43 | SaltAudioTag.read( 44 | source = it, 45 | extension = "flac", 46 | strategy = ReadStrategy.All 47 | ) 48 | } 49 | 50 | val audioTag = result.getOrThrow() 51 | 52 | val outputPath = Path("C:\\Users\\moria\\Desktop\\G.E.M.邓紫棋 - 桃花诺_output.flac") 53 | 54 | if (audioTag.metadatas != null) { 55 | SaltAudioTag.write( 56 | src = path, 57 | dst = outputPath, 58 | extension = "flac", 59 | WriteOperation.AllMetadata.create( 60 | metadatas = audioTag.metadatas + Metadata( 61 | key = "ARTIST", 62 | value = "Salt Audio Tag" 63 | ) 64 | ) 65 | ) 66 | } 67 | } 68 | 69 | @Test 70 | fun testWriteRemoveAllMetadata() { 71 | // val outputPath = Path("C:\\Users\\moria\\Desktop\\G.E.M.邓紫棋 - 桃花诺_output.flac") 72 | // val audioFile = SaltAudioTag.create( 73 | // path = path, 74 | // rwStrategy = RwStrategy.ReadWriteAll 75 | // ) 76 | // audioFile.write( 77 | // input = path, 78 | // output = outputPath, 79 | // WriteOperation.AllMetadata( 80 | // metadatas = emptyList() 81 | // ) 82 | // ) 83 | } 84 | 85 | @Test 86 | fun readPictures() { 87 | val result = SystemFileSystem.source(path).buffered().use { 88 | SaltAudioTag.read( 89 | source = it, 90 | extension = "flac", 91 | strategy = ReadStrategy.OnlyPicture 92 | ) 93 | } 94 | 95 | val audioTag = result.getOrThrow() 96 | 97 | if (audioTag.pictures != null) { 98 | if (audioTag.pictures.isEmpty()) { 99 | println("Pictures is empty") 100 | } 101 | 102 | audioTag.pictures.forEach { picture -> 103 | println( 104 | """ 105 | pictureType = ${picture.pictureType} 106 | mediaType = ${picture.mediaType} 107 | description = ${picture.description} 108 | width = ${picture.width} 109 | height = ${picture.height} 110 | colorDepth = ${picture.colorDepth} 111 | colorsNumber = ${picture.colorsNumber} 112 | pictureData.size = ${picture.pictureData.size} Bytes 113 | """.trimIndent() 114 | ) 115 | } 116 | } else { 117 | println("Pictures is null") 118 | } 119 | } 120 | 121 | companion object { 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/com/moriafly/salt/audiotag/util/SystemFileSystemUtilTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Salt Audio Tag 3 | * Copyright (C) 2025 Moriafly 4 | * 5 | * This library is free software; you can redistribute it and/or modify it under the terms of the 6 | * GNU Lesser General Public License as published by the Free Software Foundation; either version 7 | * 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 10 | * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | * Lesser General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License along with this library; 14 | * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 15 | * 02110-1301 USA 16 | */ 17 | 18 | package com.moriafly.salt.audiotag.util 19 | 20 | import kotlinx.io.buffered 21 | import kotlinx.io.files.Path 22 | import kotlinx.io.files.SystemFileSystem 23 | import kotlinx.io.files.SystemTemporaryDirectory 24 | import kotlinx.io.writeString 25 | import kotlin.test.Test 26 | 27 | class SystemFileSystemUtilTest { 28 | @Test 29 | fun tempDirectory() { 30 | // Windows: C:\Users\moria\AppData\Local\Temp 31 | // Android: /data/user/0/com.moriafly.salt.audiotag/cache 32 | println("SystemTemporaryDirectory = $SystemTemporaryDirectory") 33 | } 34 | 35 | @Test 36 | fun tempFilePath() { 37 | println("TempFilePath = ${SystemFileSystemUtil.tempFilePath()}") 38 | } 39 | 40 | @Test 41 | fun writeTempFilePath() { 42 | val tempFilePath = SystemFileSystemUtil.tempFilePath() 43 | SystemFileSystem.sink(tempFilePath).buffered().use { 44 | it.writeString("Hello, Salt Audio Tag!") 45 | } 46 | println("TempFilePath = $tempFilePath") 47 | } 48 | 49 | @Test 50 | fun copy() { 51 | val tempFilePath = SystemFileSystemUtil.tempFilePath() 52 | SystemFileSystem.sink(tempFilePath).buffered().use { 53 | it.writeString("Hello, Salt Audio Tag!") 54 | } 55 | SystemFileSystemUtil.copy( 56 | src = tempFilePath, 57 | dst = Path("C:\\Users\\moria\\1") 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | versionCode = "2" 3 | versionName = "0.1.0-dev11" 4 | agp = "8.9.1" 5 | android-compileSdk = "35" 6 | android-minSdk = "23" 7 | android-targetSdk = "35" 8 | androidx-activityCompose = "1.10.1" 9 | androidx-appcompat = "1.7.0" 10 | androidx-constraintlayout = "2.2.1" 11 | androidx-core-ktx = "1.15.0" 12 | androidx-test-junit = "1.2.1" 13 | compose-multiplatform = "1.8.0-beta01" 14 | androidx-espresso-core = "3.6.1" 15 | androidx-lifecycle = "2.8.4" 16 | androidx-material = "1.12.0" 17 | junit = "4.13.2" 18 | kotlin = "2.1.0" 19 | kotlinx-coroutines = "1.10.1" 20 | kotlinx-io = "0.7.0" 21 | buildkonfig = "0.15.1" 22 | salt-ui = "2.4.0-alpha03" 23 | filekit = "0.10.0-beta01" 24 | jetbrains-androidx-navigation = "2.8.0-alpha13" 25 | 26 | [libraries] 27 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 28 | kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } 29 | junit = { group = "junit", name = "junit", version.ref = "junit" } 30 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } 31 | androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" } 32 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" } 33 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } 34 | androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } 35 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } 36 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 37 | androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } 38 | androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } 39 | androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } 40 | kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } 41 | kotlinx-coroutines-core-common = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-common", version.ref = "kotlinx-coroutines" } 42 | kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } 43 | kotlinx-io-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-io-core", version.ref = "kotlinx-io" } 44 | kotlinx-io-bytestring = { group = "org.jetbrains.kotlinx", name = "kotlinx-io-bytestring", version.ref = "kotlinx-io" } 45 | salt-ui = { group = "io.github.moriafly", name = "salt-ui", version.ref = "salt-ui" } 46 | filekit-core = { group = "io.github.vinceglb", name = "filekit-core", version.ref = "filekit" } 47 | filekit-dialogs = { group = "io.github.vinceglb", name = "filekit-dialogs", version.ref = "filekit" } 48 | filekit-dialogs-compose = { group = "io.github.vinceglb", name = "filekit-dialogs-compose", version.ref = "filekit" } 49 | jetbrains-androidx-navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "jetbrains-androidx-navigation" } 50 | 51 | [plugins] 52 | androidApplication = { id = "com.android.application", version.ref = "agp" } 53 | androidLibrary = { id = "com.android.library", version.ref = "agp" } 54 | composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } 55 | composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 56 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 57 | buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfig" } 58 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moriafly/SaltAudioTag/eea82a1963ded9edc2535127c4bc3495a7693eb6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=com.moriafly.salt.audiotag.SaltAudioTag 3 | APP_NAME=SaltAudioTag -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; 11 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; 12 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; 13 | 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 18 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 19 | 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; 20 | 7555FF7B242A565900829871 /* SaltAudioTag.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SaltAudioTag.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 22 | 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23 | AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; 24 | /* End PBXFileReference section */ 25 | 26 | /* Begin PBXFrameworksBuildPhase section */ 27 | B92378962B6B1156000C7307 /* Frameworks */ = { 28 | isa = PBXFrameworksBuildPhase; 29 | buildActionMask = 2147483647; 30 | files = ( 31 | ); 32 | runOnlyForDeploymentPostprocessing = 0; 33 | }; 34 | /* End PBXFrameworksBuildPhase section */ 35 | 36 | /* Begin PBXGroup section */ 37 | 058557D7273AAEEB004C7B11 /* Preview Content */ = { 38 | isa = PBXGroup; 39 | children = ( 40 | 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */, 41 | ); 42 | path = "Preview Content"; 43 | sourceTree = ""; 44 | }; 45 | 42799AB246E5F90AF97AA0EF /* Frameworks */ = { 46 | isa = PBXGroup; 47 | children = ( 48 | ); 49 | name = Frameworks; 50 | sourceTree = ""; 51 | }; 52 | 7555FF72242A565900829871 = { 53 | isa = PBXGroup; 54 | children = ( 55 | AB1DB47929225F7C00F7AF9C /* Configuration */, 56 | 7555FF7D242A565900829871 /* iosApp */, 57 | 7555FF7C242A565900829871 /* Products */, 58 | 42799AB246E5F90AF97AA0EF /* Frameworks */, 59 | ); 60 | sourceTree = ""; 61 | }; 62 | 7555FF7C242A565900829871 /* Products */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 7555FF7B242A565900829871 /* SaltAudioTag.app */, 66 | ); 67 | name = Products; 68 | sourceTree = ""; 69 | }; 70 | 7555FF7D242A565900829871 /* iosApp */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 058557BA273AAA24004C7B11 /* Assets.xcassets */, 74 | 7555FF82242A565900829871 /* ContentView.swift */, 75 | 7555FF8C242A565B00829871 /* Info.plist */, 76 | 2152FB032600AC8F00CF470E /* iOSApp.swift */, 77 | 058557D7273AAEEB004C7B11 /* Preview Content */, 78 | ); 79 | path = iosApp; 80 | sourceTree = ""; 81 | }; 82 | AB1DB47929225F7C00F7AF9C /* Configuration */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | AB3632DC29227652001CCB65 /* Config.xcconfig */, 86 | ); 87 | path = Configuration; 88 | sourceTree = ""; 89 | }; 90 | /* End PBXGroup section */ 91 | 92 | /* Begin PBXNativeTarget section */ 93 | 7555FF7A242A565900829871 /* iosApp */ = { 94 | isa = PBXNativeTarget; 95 | buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; 96 | buildPhases = ( 97 | F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */, 98 | 7555FF77242A565900829871 /* Sources */, 99 | B92378962B6B1156000C7307 /* Frameworks */, 100 | 7555FF79242A565900829871 /* Resources */, 101 | ); 102 | buildRules = ( 103 | ); 104 | dependencies = ( 105 | ); 106 | name = iosApp; 107 | packageProductDependencies = ( 108 | ); 109 | productName = iosApp; 110 | productReference = 7555FF7B242A565900829871 /* SaltAudioTag.app */; 111 | productType = "com.apple.product-type.application"; 112 | }; 113 | /* End PBXNativeTarget section */ 114 | 115 | /* Begin PBXProject section */ 116 | 7555FF73242A565900829871 /* Project object */ = { 117 | isa = PBXProject; 118 | attributes = { 119 | BuildIndependentTargetsInParallel = YES; 120 | LastSwiftUpdateCheck = 1130; 121 | LastUpgradeCheck = 1540; 122 | ORGANIZATIONNAME = orgName; 123 | TargetAttributes = { 124 | 7555FF7A242A565900829871 = { 125 | CreatedOnToolsVersion = 11.3.1; 126 | }; 127 | }; 128 | }; 129 | buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; 130 | compatibilityVersion = "Xcode 14.0"; 131 | developmentRegion = en; 132 | hasScannedForEncodings = 0; 133 | knownRegions = ( 134 | en, 135 | Base, 136 | ); 137 | mainGroup = 7555FF72242A565900829871; 138 | packageReferences = ( 139 | ); 140 | productRefGroup = 7555FF7C242A565900829871 /* Products */; 141 | projectDirPath = ""; 142 | projectRoot = ""; 143 | targets = ( 144 | 7555FF7A242A565900829871 /* iosApp */, 145 | ); 146 | }; 147 | /* End PBXProject section */ 148 | 149 | /* Begin PBXResourcesBuildPhase section */ 150 | 7555FF79242A565900829871 /* Resources */ = { 151 | isa = PBXResourcesBuildPhase; 152 | buildActionMask = 2147483647; 153 | files = ( 154 | 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, 155 | 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, 156 | ); 157 | runOnlyForDeploymentPostprocessing = 0; 158 | }; 159 | /* End PBXResourcesBuildPhase section */ 160 | 161 | /* Begin PBXShellScriptBuildPhase section */ 162 | F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = { 163 | isa = PBXShellScriptBuildPhase; 164 | buildActionMask = 2147483647; 165 | files = ( 166 | ); 167 | inputFileListPaths = ( 168 | ); 169 | inputPaths = ( 170 | ); 171 | name = "Compile Kotlin Framework"; 172 | outputFileListPaths = ( 173 | ); 174 | outputPaths = ( 175 | ); 176 | runOnlyForDeploymentPostprocessing = 0; 177 | shellPath = /bin/sh; 178 | shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n"; 179 | }; 180 | /* End PBXShellScriptBuildPhase section */ 181 | 182 | /* Begin PBXSourcesBuildPhase section */ 183 | 7555FF77242A565900829871 /* Sources */ = { 184 | isa = PBXSourcesBuildPhase; 185 | buildActionMask = 2147483647; 186 | files = ( 187 | 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, 188 | 7555FF83242A565900829871 /* ContentView.swift in Sources */, 189 | ); 190 | runOnlyForDeploymentPostprocessing = 0; 191 | }; 192 | /* End PBXSourcesBuildPhase section */ 193 | 194 | /* Begin XCBuildConfiguration section */ 195 | 7555FFA3242A565B00829871 /* Debug */ = { 196 | isa = XCBuildConfiguration; 197 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; 198 | buildSettings = { 199 | ALWAYS_SEARCH_USER_PATHS = NO; 200 | CLANG_ANALYZER_NONNULL = YES; 201 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 202 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 203 | CLANG_CXX_LIBRARY = "libc++"; 204 | CLANG_ENABLE_MODULES = YES; 205 | CLANG_ENABLE_OBJC_ARC = YES; 206 | CLANG_ENABLE_OBJC_WEAK = YES; 207 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 208 | CLANG_WARN_BOOL_CONVERSION = YES; 209 | CLANG_WARN_COMMA = YES; 210 | CLANG_WARN_CONSTANT_CONVERSION = YES; 211 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 212 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 213 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 214 | CLANG_WARN_EMPTY_BODY = YES; 215 | CLANG_WARN_ENUM_CONVERSION = YES; 216 | CLANG_WARN_INFINITE_RECURSION = YES; 217 | CLANG_WARN_INT_CONVERSION = YES; 218 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 219 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 220 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 221 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 222 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 223 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 224 | CLANG_WARN_STRICT_PROTOTYPES = YES; 225 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 226 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 227 | CLANG_WARN_UNREACHABLE_CODE = YES; 228 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 229 | COPY_PHASE_STRIP = NO; 230 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 231 | ENABLE_STRICT_OBJC_MSGSEND = YES; 232 | ENABLE_TESTABILITY = YES; 233 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 234 | GCC_C_LANGUAGE_STANDARD = gnu11; 235 | GCC_DYNAMIC_NO_PIC = NO; 236 | GCC_NO_COMMON_BLOCKS = YES; 237 | GCC_OPTIMIZATION_LEVEL = 0; 238 | GCC_PREPROCESSOR_DEFINITIONS = ( 239 | "DEBUG=1", 240 | "$(inherited)", 241 | ); 242 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 243 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 244 | GCC_WARN_UNDECLARED_SELECTOR = YES; 245 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 246 | GCC_WARN_UNUSED_FUNCTION = YES; 247 | GCC_WARN_UNUSED_VARIABLE = YES; 248 | IPHONEOS_DEPLOYMENT_TARGET = 15.3; 249 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 250 | MTL_FAST_MATH = YES; 251 | ONLY_ACTIVE_ARCH = YES; 252 | SDKROOT = iphoneos; 253 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 254 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 255 | }; 256 | name = Debug; 257 | }; 258 | 7555FFA4242A565B00829871 /* Release */ = { 259 | isa = XCBuildConfiguration; 260 | baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; 261 | buildSettings = { 262 | ALWAYS_SEARCH_USER_PATHS = NO; 263 | CLANG_ANALYZER_NONNULL = YES; 264 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 265 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 266 | CLANG_CXX_LIBRARY = "libc++"; 267 | CLANG_ENABLE_MODULES = YES; 268 | CLANG_ENABLE_OBJC_ARC = YES; 269 | CLANG_ENABLE_OBJC_WEAK = YES; 270 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 271 | CLANG_WARN_BOOL_CONVERSION = YES; 272 | CLANG_WARN_COMMA = YES; 273 | CLANG_WARN_CONSTANT_CONVERSION = YES; 274 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 275 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 276 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 277 | CLANG_WARN_EMPTY_BODY = YES; 278 | CLANG_WARN_ENUM_CONVERSION = YES; 279 | CLANG_WARN_INFINITE_RECURSION = YES; 280 | CLANG_WARN_INT_CONVERSION = YES; 281 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 282 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 283 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 284 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 285 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 286 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 287 | CLANG_WARN_STRICT_PROTOTYPES = YES; 288 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 289 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 290 | CLANG_WARN_UNREACHABLE_CODE = YES; 291 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 292 | COPY_PHASE_STRIP = NO; 293 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 294 | ENABLE_NS_ASSERTIONS = NO; 295 | ENABLE_STRICT_OBJC_MSGSEND = YES; 296 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 297 | GCC_C_LANGUAGE_STANDARD = gnu11; 298 | GCC_NO_COMMON_BLOCKS = YES; 299 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 300 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 301 | GCC_WARN_UNDECLARED_SELECTOR = YES; 302 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 303 | GCC_WARN_UNUSED_FUNCTION = YES; 304 | GCC_WARN_UNUSED_VARIABLE = YES; 305 | IPHONEOS_DEPLOYMENT_TARGET = 15.3; 306 | MTL_ENABLE_DEBUG_INFO = NO; 307 | MTL_FAST_MATH = YES; 308 | SDKROOT = iphoneos; 309 | SWIFT_COMPILATION_MODE = wholemodule; 310 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 311 | VALIDATE_PRODUCT = YES; 312 | }; 313 | name = Release; 314 | }; 315 | 7555FFA6242A565B00829871 /* Debug */ = { 316 | isa = XCBuildConfiguration; 317 | buildSettings = { 318 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 319 | CODE_SIGN_IDENTITY = "Apple Development"; 320 | CODE_SIGN_STYLE = Automatic; 321 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 322 | DEVELOPMENT_TEAM = "${TEAM_ID}"; 323 | ENABLE_PREVIEWS = YES; 324 | FRAMEWORK_SEARCH_PATHS = ( 325 | "$(inherited)", 326 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", 327 | ); 328 | INFOPLIST_FILE = iosApp/Info.plist; 329 | IPHONEOS_DEPLOYMENT_TARGET = 15.3; 330 | LD_RUNPATH_SEARCH_PATHS = ( 331 | "$(inherited)", 332 | "@executable_path/Frameworks", 333 | ); 334 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; 335 | PRODUCT_NAME = "${APP_NAME}"; 336 | PROVISIONING_PROFILE_SPECIFIER = ""; 337 | SWIFT_VERSION = 5.0; 338 | TARGETED_DEVICE_FAMILY = "1,2"; 339 | }; 340 | name = Debug; 341 | }; 342 | 7555FFA7242A565B00829871 /* Release */ = { 343 | isa = XCBuildConfiguration; 344 | buildSettings = { 345 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 346 | CODE_SIGN_IDENTITY = "Apple Development"; 347 | CODE_SIGN_STYLE = Automatic; 348 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 349 | DEVELOPMENT_TEAM = "${TEAM_ID}"; 350 | ENABLE_PREVIEWS = YES; 351 | FRAMEWORK_SEARCH_PATHS = ( 352 | "$(inherited)", 353 | "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", 354 | ); 355 | INFOPLIST_FILE = iosApp/Info.plist; 356 | IPHONEOS_DEPLOYMENT_TARGET = 15.3; 357 | LD_RUNPATH_SEARCH_PATHS = ( 358 | "$(inherited)", 359 | "@executable_path/Frameworks", 360 | ); 361 | PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; 362 | PRODUCT_NAME = "${APP_NAME}"; 363 | PROVISIONING_PROFILE_SPECIFIER = ""; 364 | SWIFT_VERSION = 5.0; 365 | TARGETED_DEVICE_FAMILY = "1,2"; 366 | }; 367 | name = Release; 368 | }; 369 | /* End XCBuildConfiguration section */ 370 | 371 | /* Begin XCConfigurationList section */ 372 | 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { 373 | isa = XCConfigurationList; 374 | buildConfigurations = ( 375 | 7555FFA3242A565B00829871 /* Debug */, 376 | 7555FFA4242A565B00829871 /* Release */, 377 | ); 378 | defaultConfigurationIsVisible = 0; 379 | defaultConfigurationName = Release; 380 | }; 381 | 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { 382 | isa = XCConfigurationList; 383 | buildConfigurations = ( 384 | 7555FFA6242A565B00829871 /* Debug */, 385 | 7555FFA7242A565B00829871 /* Release */, 386 | ); 387 | defaultConfigurationIsVisible = 0; 388 | defaultConfigurationName = Release; 389 | }; 390 | /* End XCConfigurationList section */ 391 | }; 392 | rootObject = 7555FF73242A565900829871 /* Project object */; 393 | } -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moriafly/SaltAudioTag/eea82a1963ded9edc2535127c4bc3495a7693eb6/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import ComposeApp 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | MainViewControllerKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | CADisableMinimumFrameDurationOnPhone 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOSApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /res/DOCUMENTS.md: -------------------------------------------------------------------------------- 1 | # Documents 2 | 3 | - FLAC: https://xiph.org/flac/format.html 4 | - RFC 9639: https://datatracker.ietf.org/doc/rfc9639 -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "SaltAudioTag" 2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 3 | 4 | pluginManagement { 5 | repositories { 6 | google { 7 | mavenContent { 8 | includeGroupAndSubgroups("androidx") 9 | includeGroupAndSubgroups("com.android") 10 | includeGroupAndSubgroups("com.google") 11 | } 12 | } 13 | mavenCentral() 14 | gradlePluginPortal() 15 | } 16 | } 17 | 18 | dependencyResolutionManagement { 19 | repositories { 20 | google { 21 | mavenContent { 22 | includeGroupAndSubgroups("androidx") 23 | includeGroupAndSubgroups("com.android") 24 | includeGroupAndSubgroups("com.google") 25 | } 26 | } 27 | mavenCentral() 28 | } 29 | } 30 | 31 | include(":composeApp") 32 | include(":core") 33 | --------------------------------------------------------------------------------