├── .clang-format ├── .clangd ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── CMakePresets.json ├── LICENSE.md ├── README.md ├── app ├── CMakeLists.txt ├── android │ ├── AndroidManifest.xml │ ├── build.gradle │ └── res │ │ ├── values │ │ └── libs.xml │ │ └── xml │ │ └── qtprovider_paths.xml ├── assets │ ├── Qcm.desktop │ ├── Qcm.desktop.in │ ├── Qcm.metainfo.xml.in │ ├── Qcm.svg │ └── screenshots │ │ ├── main.png │ │ ├── main_compact.png │ │ ├── main_compact_dark.png │ │ └── main_dark.png ├── include │ └── Qcm │ │ ├── action.hpp │ │ ├── app.hpp │ │ ├── backend.hpp │ │ ├── backend_msg.hpp │ │ ├── global.hpp │ │ ├── global_p.hpp │ │ ├── image_provider │ │ ├── http.hpp │ │ ├── qr.hpp │ │ └── response.hpp │ │ ├── macro.hpp │ │ ├── model │ │ ├── app_info.hpp │ │ ├── empty_model.hpp │ │ ├── id_queue.hpp │ │ ├── item_id.hpp │ │ ├── list_models.hpp │ │ ├── lyric.hpp │ │ ├── page_model.hpp │ │ ├── play_queue.hpp │ │ ├── router_msg.hpp │ │ ├── share_store.hpp │ │ ├── sort_filter.hpp │ │ └── store_item.hpp │ │ ├── notifier.hpp │ │ ├── player.hpp │ │ ├── qml │ │ ├── clipboard.hpp │ │ ├── duration.hpp │ │ ├── enum.hpp │ │ └── qml_util.hpp │ │ ├── query │ │ ├── album_query.hpp │ │ ├── artist_query.hpp │ │ ├── lyric_query.hpp │ │ ├── mix_query.hpp │ │ ├── old │ │ │ ├── album_detail.h │ │ │ ├── artist_albums.h │ │ │ ├── artist_detail.h │ │ │ ├── artist_songs.h │ │ │ ├── comments.h │ │ │ ├── mix_create.h │ │ │ ├── mix_delete.h │ │ │ ├── mix_detail.h │ │ │ ├── mix_manipulate.h │ │ │ ├── radio_collection.h │ │ │ ├── radio_detail.h │ │ │ ├── song_detail.h │ │ │ └── user_mix.h │ │ ├── provider_query.hpp │ │ ├── qr_query.hpp │ │ ├── query.hpp │ │ ├── query_api.hpp │ │ ├── search_query.hpp │ │ ├── storage_info.hpp │ │ └── sync_query.hpp │ │ ├── status │ │ ├── app_state.hpp │ │ ├── process.hpp │ │ └── provider_status.hpp │ │ ├── store.hpp │ │ └── util │ │ ├── async.hpp │ │ ├── async.inl │ │ ├── ex.hpp │ │ ├── global_static.hpp │ │ ├── mem.hpp │ │ └── path.hpp ├── qml │ ├── Global.qml │ ├── PlayBar.qml │ ├── Window.qml │ ├── action │ │ ├── AddToMixAction.qml │ │ ├── AppendListAction.qml │ │ ├── CollectAction.qml │ │ ├── ColorSchemeAction.qml │ │ ├── CommentAction.qml │ │ ├── CopyAction.qml │ │ ├── GoToAlbumAction.qml │ │ ├── GoToArtistAction.qml │ │ ├── MixCreateAction.qml │ │ ├── PlaynextAction.qml │ │ ├── SettingAction.qml │ │ └── SubAction.qml │ ├── component │ │ ├── AuthEmail.qml │ │ ├── AuthQr.qml │ │ ├── AuthUsername.qml │ │ ├── ByteSlider.qml │ │ ├── DebugRect.qml │ │ ├── GridView.qml │ │ ├── Image.qml │ │ ├── Leaflet.qml │ │ ├── ListDescription.qml │ │ ├── ListenIcon.qml │ │ ├── LyricView.qml │ │ ├── MainBasePage.qml │ │ ├── Mpris.qml │ │ ├── OrderChip.qml │ │ ├── PageContainer.qml │ │ ├── PagePopup.qml │ │ ├── PageStack.qml │ │ ├── PlaySlider.qml │ │ ├── RoundImage.qml │ │ ├── RouteMsg.qml │ │ ├── SettingRow.qml │ │ ├── SettingSection.qml │ │ ├── SongTag.qml │ │ ├── SyncingLabel.qml │ │ └── VolumeButton.qml │ ├── delegate │ │ ├── CommentDelegate.qml │ │ ├── PicCardDelegate.qml │ │ ├── PicCardGridDelegate.qml │ │ ├── ProgramDelegate.qml │ │ └── SongDelegate.qml │ ├── dialog │ │ ├── AddToMixDialog.qml │ │ ├── GoToArtistDialog.qml │ │ └── MixCreateDialog.qml │ ├── js │ │ ├── canvas.mjs │ │ └── util.mjs │ ├── menu │ │ ├── AlbumMenu.qml │ │ ├── ArtistMenu.qml │ │ ├── MixMenu.qml │ │ ├── ProgramMenu.qml │ │ ├── RadioMenu.qml │ │ ├── SongMenu.qml │ │ └── SortMenu.qml │ └── page │ │ ├── AboutPage.qml │ │ ├── AddProviderPage.qml │ │ ├── CommentPage.qml │ │ ├── DescriptionPage.qml │ │ ├── LibraryPage.qml │ │ ├── MainPage.qml │ │ ├── PlayQueuePage.qml │ │ ├── PlayingPage.qml │ │ ├── ProviderMetaPage.qml │ │ ├── SearchPage.qml │ │ ├── StatusPage.qml │ │ ├── SyncPage.qml │ │ ├── detail │ │ ├── AlbumDetailPage.qml │ │ ├── ArtistDetailPage.qml │ │ ├── MixDetailPage.qml │ │ └── RadioDetailPage.qml │ │ ├── edit │ │ └── ProviderEditPage.qml │ │ ├── setting │ │ ├── AudioSetting.qml │ │ ├── MiscSetting.qml │ │ ├── NetworkSetting.qml │ │ ├── ProviderManagePage.qml │ │ ├── SettingPage.qml │ │ ├── StorageSetting.qml │ │ └── ThemeSetting.qml │ │ └── toplevel │ │ ├── LoadingPage.qml │ │ ├── RetryPage.qml │ │ └── WelcomePage.qml ├── src │ ├── action.cpp │ ├── app.cpp │ ├── backend.cpp │ ├── global.cpp │ ├── image_provider │ │ ├── http.cpp │ │ ├── qr.cpp │ │ └── response.cpp │ ├── log.cpp │ ├── main.cpp │ ├── mod.cppm │ ├── model │ │ ├── app_info.cpp │ │ ├── empty_model.cpp │ │ ├── id_queue.cpp │ │ ├── item_id.cpp │ │ ├── list_models.cpp │ │ ├── lyric.cpp │ │ ├── page_model.cpp │ │ ├── play_queue.cpp │ │ ├── router_msg.cpp │ │ ├── sort_filter.cpp │ │ └── store_item.cpp │ ├── notifier.cpp │ ├── player.cpp │ ├── qml │ │ ├── clipboard.cpp │ │ ├── duration.cpp │ │ ├── enum.cpp │ │ └── qml_util.cpp │ ├── query │ │ ├── album_query.cpp │ │ ├── artist_query.cpp │ │ ├── lyric_query.cpp │ │ ├── mix_query.cpp │ │ ├── provider_query.cpp │ │ ├── qr_query.cpp │ │ ├── query.cpp │ │ ├── query_api.cpp │ │ ├── search_query.cpp │ │ ├── storage_info.cpp │ │ └── sync_query.cpp │ ├── status │ │ ├── app_state.cpp │ │ ├── process.cpp │ │ └── provider.cpp │ ├── store.cpp │ └── util │ │ ├── QmlStackTraceHelper.cpp │ │ ├── global_static.cpp │ │ └── path.cpp └── static.cpp.in ├── cmake ├── FindWrapProtoc.cmake └── flatpak-provider.cmake ├── conanfile.txt ├── core ├── CMakeLists.txt ├── include │ ├── core │ │ ├── array_helper.h │ │ ├── asio │ │ │ ├── basic.h │ │ │ ├── detached_log.h │ │ │ ├── error.h │ │ │ ├── export.h │ │ │ ├── helper.h │ │ │ ├── sync_file.h │ │ │ ├── task.h │ │ │ └── watch_dog.h │ │ ├── bit_flags.h │ │ ├── callable.h │ │ ├── clangd.h │ │ ├── core.h │ │ ├── expected_helper.h │ │ ├── fmt.h │ │ ├── helper.h │ │ ├── log.h │ │ ├── macro.h │ │ ├── math.h │ │ ├── optional_helper.h │ │ ├── qasio │ │ │ ├── qt_execution_context.h │ │ │ ├── qt_executor.h │ │ │ ├── qt_holder.h │ │ │ └── qt_watcher.h │ │ ├── qlist_helper.h │ │ ├── qmeta_helper.h │ │ ├── qobject_bindable_property_p.h │ │ ├── qstr_helper.h │ │ ├── queue_concurrent.h │ │ ├── qvariant_helper.h │ │ ├── sender.h │ │ └── type.h │ └── std23 │ │ ├── __functional_base.h │ │ ├── function.h │ │ ├── function_ref.h │ │ └── move_only_function.h └── src │ ├── asio │ └── asio.cpp │ ├── core.cppm │ ├── fmt.cppm │ ├── helper │ ├── container.cppm │ ├── mod.cppm │ └── str.cppm │ ├── lambda.cppm │ ├── log.cpp │ ├── log.cppm │ ├── mem.cpp │ ├── mem.cppm │ ├── mod.cppm │ ├── path.cpp │ ├── qasio │ ├── qt_execution_context.cpp │ └── qt_executor.cpp │ ├── random.cppm │ └── type_list.cppm ├── crypto ├── CMakeLists.txt ├── crypto.cpp └── include │ └── crypto │ └── crypto.h ├── error ├── CMakeLists.txt └── include │ └── error │ └── error.h ├── flaptak-source.json ├── make_android.sh ├── message ├── CMakeLists.txt └── proto │ ├── message.proto │ └── model.proto ├── mpris ├── CMakeLists.txt ├── include │ └── mpris │ │ ├── mediaplayer2.h │ │ ├── mediaplayer2_adaptor.h │ │ └── mpris.h ├── mediaplayer2.cpp ├── mediaplayer2_adaptor.cpp └── mpris.cpp ├── platform ├── CMakeLists.txt └── src │ ├── linux.cpp │ ├── platform.cpp │ ├── platform.cppm │ └── win.cpp ├── player ├── CMakeLists.txt ├── audio_decoder.h ├── audio_device.h ├── audio_frame.h ├── audio_frame_queue.h ├── audio_stream_params.h ├── context.h ├── ffmpeg_dict.h ├── ffmpeg_error.h ├── ffmpeg_format_context.h ├── ffmpeg_frame.h ├── include │ └── player │ │ ├── metadata.h │ │ ├── notify.h │ │ ├── player.h │ │ └── player_p.h ├── packet_queue.h ├── player.cpp ├── resampler.h └── stream_reader.h ├── test ├── CMakeLists.txt └── src │ ├── asio.cpp │ ├── log.cpp │ └── main.cpp └── third_party ├── CMakeLists.txt └── qr_code ├── CMakeLists.txt ├── include └── qr_code │ └── qrcodegen.hpp └── qrcodegen.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | AccessModifierOffset: -4 3 | 4 | AlignAfterOpenBracket: Align 5 | AlignConsecutiveMacros: true 6 | AlignConsecutiveAssignments: true 7 | AlignConsecutiveDeclarations: true 8 | AlignEscapedNewlines: Left 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | 12 | AllowAllParametersOfDeclarationOnNextLine: true 13 | AllowShortBlocksOnASingleLine: false 14 | AllowShortCaseLabelsOnASingleLine: true 15 | AllowShortIfStatementsOnASingleLine: true 16 | AllowShortLoopsOnASingleLine: true 17 | AllowShortLambdasOnASingleLine: false 18 | 19 | AlwaysBreakAfterReturnType: None 20 | AlwaysBreakTemplateDeclarations: true 21 | 22 | BreakBeforeBraces: Custom 23 | BraceWrapping: 24 | AfterEnum: true 25 | AfterNamespace: true 26 | SplitEmptyFunction: false 27 | SplitEmptyRecord: false 28 | BinPackArguments: false 29 | BinPackParameters: true 30 | CompactNamespaces: false 31 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 32 | Cpp11BracedListStyle: false 33 | IndentCaseLabels: false 34 | IndentPPDirectives: AfterHash 35 | IndentWidth: 4 36 | IndentWrappedFunctionNames: false 37 | KeepEmptyLinesAtTheStartOfBlocks: false 38 | Language: Cpp 39 | PointerAlignment: Left 40 | SortIncludes: false 41 | SpaceAfterCStyleCast: false 42 | SpaceAfterLogicalNot: true 43 | SpaceAfterTemplateKeyword: false 44 | SpaceBeforeAssignmentOperators: true 45 | SpaceBeforeCpp11BracedList: true 46 | SpaceBeforeCtorInitializerColon: false 47 | SpaceBeforeInheritanceColon: true 48 | SpaceBeforeRangeBasedForLoopColon: true 49 | SpaceInEmptyParentheses: false 50 | SpacesInAngles: false 51 | SpacesInCStyleCastParentheses: false 52 | SpacesInContainerLiterals: false 53 | SpacesInParentheses: false 54 | SpacesInSquareBrackets: false 55 | Standard: Cpp11 56 | TabWidth: 4 57 | UseTab: Never 58 | ColumnLimit: 100 59 | -------------------------------------------------------------------------------- /.clangd: -------------------------------------------------------------------------------- 1 | CompileFlags: 2 | Add: [-D__clangd__] 3 | Remove: [-fconcepts-diagnostics-depth*, -mno-direct*] 4 | 5 | Diagnostics: 6 | Suppress: [multiple_decl_in_different_modules] -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | app/assets/screenshots/*.png filter=lfs diff=lfs merge=lfs -text 2 | app/assets/screenshots/*.jpg filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | ko_fi: hypengw 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used to ignore files which are generated 2 | # ---------------------------------------------------------------------------- 3 | 4 | *~ 5 | *.autosave 6 | *.a 7 | *.core 8 | *.moc 9 | *.o 10 | *.obj 11 | *.orig 12 | *.rej 13 | *.so 14 | *.so.* 15 | *_pch.h.cpp 16 | *_resource.rc 17 | *.qm 18 | .#* 19 | *.*# 20 | core 21 | !core/ 22 | tags 23 | .DS_Store 24 | .directory 25 | *.debug 26 | Makefile* 27 | *.prl 28 | *.app 29 | moc_*.cpp 30 | ui_*.h 31 | qrc_*.cpp 32 | Thumbs.db 33 | *.res 34 | *.rc 35 | /.qmake.cache 36 | /.qmake.stash 37 | 38 | # qtcreator generated files 39 | *.pro.user* 40 | 41 | # xemacs temporary files 42 | *.flc 43 | 44 | # Vim temporary files 45 | .*.swp 46 | 47 | # Visual Studio generated files 48 | *.ib_pdb_index 49 | *.idb 50 | *.ilk 51 | *.pdb 52 | *.sln 53 | *.suo 54 | *.vcproj 55 | *vcproj.*.*.user 56 | *.ncb 57 | *.sdf 58 | *.opensdf 59 | *.vcxproj 60 | *vcxproj.* 61 | 62 | # MinGW generated files 63 | *.Debug 64 | *.Release 65 | 66 | # Python byte code 67 | *.pyc 68 | 69 | # Binaries 70 | # -------- 71 | *.dll 72 | *.exe 73 | 74 | # Qt 75 | .qmlls.ini 76 | 77 | # cm 78 | build 79 | /build* 80 | target 81 | .cache 82 | CMakeLists.txt.* 83 | compile_commands.json 84 | /.qmllint.ini 85 | /.vscode 86 | /CMakeUserPresets.json 87 | /env.sh 88 | .gradle 89 | /qml_material 90 | /third_party/qml_material 91 | /third_party/meta_model 92 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hypengw/Qcm/33600fc69a690fedbe7b4a2c1dadb9dc35a0fa2f/.gitmodules -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "configurePresets": [ 4 | { 5 | "name": "android", 6 | "binaryDir": "build/android-release", 7 | "cacheVariables": { 8 | "CMAKE_INSTALL_PREFIX": "build/android-release/install", 9 | "CMAKE_BUILD_TYPE": "Release", 10 | "CMAKE_GENERATOR": "Ninja", 11 | "ANDROID_SDK_ROOT": "$env{SDK_ROOT}", 12 | "ANDROID_NDK_ROOT": "$env{NDK_ROOT}", 13 | "QCM_MODEL_GENERATOR": "${sourceDir}/build/Release/generator/model/qcm_model_generator", 14 | "QT_ANDROID_SIGN_APK": "ON" 15 | }, 16 | "toolchainFile": "build/android-release/generators/conan_toolchain.cmake" 17 | }, 18 | { 19 | "name": "win", 20 | "generator": "Ninja", 21 | "binaryDir": "${sourceDir}/build/${presetName}", 22 | "cacheVariables": { 23 | "CMAKE_BUILD_TYPE": "Release", 24 | "CMAKE_GENERATOR": "Ninja", 25 | "CMAKE_C_COMPILER": "clang", 26 | "CMAKE_CXX_COMPILER": "clang++", 27 | "PKG_CONFIG_EXECUTABLE": "pkg-config", 28 | "CMAKE_INSTALL_PREFIX": "${sourceDir}/install/${presetName}" 29 | }, 30 | "environment": { 31 | "MY_ENVIRONMENT_VARIABLE": "Test", 32 | "PATH": "C:/msys64/clang64/bin;$penv{PATH}" 33 | } 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /app/android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 14 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 39 | 40 | 41 | 46 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:7.4.1' 9 | } 10 | } 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | } 16 | 17 | apply plugin: 'com.android.application' 18 | 19 | dependencies { 20 | implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) 21 | implementation 'androidx.core:core:1.13.1' 22 | } 23 | 24 | android { 25 | /******************************************************* 26 | * The following variables: 27 | * - androidBuildToolsVersion, 28 | * - androidCompileSdkVersion 29 | * - qtAndroidDir - holds the path to qt android files 30 | * needed to build any Qt application 31 | * on Android. 32 | * 33 | * are defined in gradle.properties file. This file is 34 | * updated by QtCreator and androiddeployqt tools. 35 | * Changing them manually might break the compilation! 36 | *******************************************************/ 37 | 38 | namespace androidPackageName 39 | compileSdkVersion androidCompileSdkVersion 40 | buildToolsVersion androidBuildToolsVersion 41 | ndkVersion androidNdkVersion 42 | 43 | // Extract native libraries from the APK 44 | packagingOptions.jniLibs.useLegacyPackaging true 45 | 46 | sourceSets { 47 | main { 48 | manifest.srcFile 'AndroidManifest.xml' 49 | java.srcDirs = [qtAndroidDir + '/src', 'src', 'java'] 50 | aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl'] 51 | res.srcDirs = [qtAndroidDir + '/res', 'res'] 52 | resources.srcDirs = ['resources'] 53 | renderscript.srcDirs = ['src'] 54 | assets.srcDirs = ['assets'] 55 | jniLibs.srcDirs = ['libs'] 56 | } 57 | } 58 | 59 | tasks.withType(JavaCompile) { 60 | options.incremental = true 61 | } 62 | 63 | compileOptions { 64 | sourceCompatibility JavaVersion.VERSION_1_8 65 | targetCompatibility JavaVersion.VERSION_1_8 66 | } 67 | 68 | lintOptions { 69 | abortOnError false 70 | } 71 | 72 | // Do not compress Qt binary resources file 73 | aaptOptions { 74 | noCompress 'rcc' 75 | } 76 | 77 | defaultConfig { 78 | resConfig "en" 79 | minSdkVersion qtMinSdkVersion 80 | targetSdkVersion qtTargetSdkVersion 81 | ndk.abiFilters = qtTargetAbiList.split(",") 82 | } 83 | 84 | buildTypes { 85 | customDebugType { 86 | debuggable true 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/android/res/values/libs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/android/res/xml/qtprovider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/assets/Qcm.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Qcm 3 | Comment=Qt client for netease cloud music 4 | Exec=Qcm %U 5 | Type=Application 6 | Icon=io.github.hypengw.Qcm 7 | Categories=Audio;AudioVideo;Qt; 8 | Terminal=false 9 | SingleMainWindow=true 10 | X-GNOME-SingleWindow=true 11 | -------------------------------------------------------------------------------- /app/assets/Qcm.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=@APP_NAME@ 3 | Comment=@APP_SUMMARY@ 4 | Exec=@PROJECT_NAME@ %U 5 | Type=Application 6 | Icon=@APP_ID@ 7 | Categories=Audio;AudioVideo;Qt; 8 | Terminal=false 9 | SingleMainWindow=true 10 | X-GNOME-SingleWindow=true 11 | -------------------------------------------------------------------------------- /app/assets/Qcm.metainfo.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | @APP_ID@ 4 | @APP_NAME@ 5 | @APP_SUMMARY@ 6 | 7 | hypengw 8 | 9 | CC0-1.0 10 | GPL-2.0 11 | @APP_ID@.desktop 12 | 13 | Material3 cloud music player 14 | Music Service: 15 | Jellyfin(wip) 16 | Netease Cloud Music(网易云音乐) 17 | 18 | https://github.com/hypengw/Qcm 19 | https://github.com/hypengw/Qcm/issues 20 | 21 | 22 | main window 23 | https://github.com/hypengw/Qcm/blob/master/app/assets/screenshots/main.png?raw=true 24 | 25 | 26 | dark 27 | https://github.com/hypengw/Qcm/blob/master/app/assets/screenshots/main_dark.png?raw=true 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Audio 43 | 44 | 45 | Netease 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/assets/screenshots/main.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5db4a1ccc428677f8c21d8fa5a81f0aa833b2e3af3cc99100c02912a19f63cc3 3 | size 301007 4 | -------------------------------------------------------------------------------- /app/assets/screenshots/main_compact.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:860ceaa0345a4146ee5adc448e0e4831759d1de15d3292bc6b84f105d671c0a9 3 | size 104968 4 | -------------------------------------------------------------------------------- /app/assets/screenshots/main_compact_dark.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:b31addd996a1a20401042aa01809c8f6a1746b8f5eca1bac6f055b98c4dcf3e1 3 | size 104217 4 | -------------------------------------------------------------------------------- /app/assets/screenshots/main_dark.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:22c496f9024e54622f860c091f3ad5e335771e8c9301bd3d8e7d7a9d0ab64d05 3 | size 300713 4 | -------------------------------------------------------------------------------- /app/include/Qcm/action.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "core/core.h" 6 | #include "Qcm/qml/enum.hpp" 7 | #include "Qcm/model/router_msg.hpp" 8 | #include "Qcm/model/item_id.hpp" 9 | 10 | Q_MOC_INCLUDE("Qcm/model/id_queue.hpp") 11 | namespace qcm 12 | { 13 | 14 | namespace model 15 | { 16 | class IdQueue; 17 | } 18 | class Action : public QObject { 19 | Q_OBJECT 20 | QML_ELEMENT 21 | QML_SINGLETON 22 | public: 23 | Action(QObject* parent); 24 | ~Action(); 25 | static auto instance() -> Action*; 26 | static Action* create(QQmlEngine*, QJSEngine*); 27 | // make qml prefer create 28 | Action() = delete; 29 | 30 | Q_SIGNALS: 31 | void switch_user(model::ItemId userId); 32 | void open_drawer(); 33 | void logout(); 34 | void route(const model::RouteMsg& msg); 35 | void route_by_id(const model::ItemId& id, const QVariantMap& props = {}); 36 | void route_special(QVariant name_id); 37 | void popup_special(QVariant name_id); 38 | void popup_page(const QJSValue& url_or_comp, QVariantMap props, QVariantMap popup_props = {}, 39 | const QJSValue& callback = {}); 40 | void switch_main_page(qint32 idx); 41 | void toast(QString text, qint32 duration = 3000, enums::ToastFlags flags = {}, 42 | QObject* action = nullptr); 43 | void collect(model::ItemId itemId, bool act = true); 44 | 45 | void next(); 46 | void prev(); 47 | void switch_queue(model::IdQueue*); 48 | void play_by_id(model::ItemId songId, model::ItemId sourceId = {}); 49 | void queue_ids(const std::vector& songIds, model::ItemId sourceId = {}); 50 | void switch_ids(const std::vector& songIds, model::ItemId sourceId = {}); 51 | void play(const QUrl& url, bool refresh = false); 52 | void sync_item(const model::ItemId& itemId, bool notify = false); 53 | void sync_collection(enums::CollectionType); 54 | void sync_library_collection(i64 library_id, enums::CollectionType); 55 | void record(enums::RecordAction); 56 | void playbackLog(enums::PlaybackState state, model::ItemId item, model::ItemId souce, 57 | QVariantMap extra = {}); 58 | }; 59 | 60 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/global_p.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Qcm/global.hpp" 3 | 4 | #include 5 | #include 6 | 7 | namespace qcm 8 | { 9 | 10 | auto get_pool_size() -> std::size_t; 11 | 12 | class Global::Private { 13 | public: 14 | Private(Global* p); 15 | ~Private(); 16 | 17 | Arc qt_ctx; 18 | asio::thread_pool pool; 19 | 20 | Arc session; 21 | 22 | QUuid uuid; 23 | 24 | MetadataImpl metadata_impl; 25 | 26 | QQmlComponent* copy_action_comp; 27 | 28 | mutable std::mutex mutex; 29 | }; 30 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/image_provider/http.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "core/asio/helper.h" 9 | #include "core/asio/watch_dog.h" 10 | #include "Qcm/image_provider/response.hpp" 11 | 12 | import ncrequest; 13 | 14 | namespace ncm 15 | { 16 | class Client; 17 | } 18 | 19 | namespace qcm 20 | { 21 | 22 | class QcmAsyncImageResponse : public QcmImageResponse { 23 | Q_OBJECT 24 | public: 25 | QcmAsyncImageResponse(); 26 | ~QcmAsyncImageResponse() override; 27 | auto textureFactory() const -> QQuickTextureFactory* override; 28 | void cancel() override { m_wdog.cancel(); } 29 | 30 | auto& wdog() { return m_wdog; } 31 | QImage image; 32 | 33 | private: 34 | helper::WatchDog m_wdog; 35 | }; 36 | 37 | class QcmImageProviderInner; 38 | class QcmImageProvider : public QQuickAsyncImageProvider { 39 | public: 40 | QcmImageProvider(); 41 | ~QcmImageProvider(); 42 | 43 | QQuickImageResponse* requestImageResponse(const QString& id, 44 | const QSize& requestedSize) override; 45 | 46 | static ncrequest::Request makeReq(const QString& id, const QSize& requestedSize, ncm::Client&); 47 | 48 | private: 49 | rc m_inner; 50 | }; 51 | 52 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/image_provider/qr.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "core/asio/helper.h" 7 | #include "Qcm/image_provider/response.hpp" 8 | 9 | namespace qcm 10 | { 11 | 12 | class QrAsyncImageResponse : public QcmImageResponse { 13 | public: 14 | QQuickTextureFactory* textureFactory() const override { 15 | return QQuickTextureFactory::textureFactoryForImage(image); 16 | } 17 | 18 | QImage image; 19 | }; 20 | 21 | class QrImageProvider : public QQuickAsyncImageProvider { 22 | public: 23 | QrImageProvider(); 24 | 25 | QQuickImageResponse* requestImageResponse(const QString& id, 26 | const QSize& requestedSize) override; 27 | }; 28 | } // namespace qcm 29 | -------------------------------------------------------------------------------- /app/include/Qcm/image_provider/response.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "core/core.h" 5 | 6 | namespace qcm 7 | { 8 | 9 | auto image_response_count() -> std::atomic&; 10 | 11 | class QcmImageResponse : public QQuickImageResponse { 12 | public: 13 | ~QcmImageResponse(); 14 | auto errorString() const -> QString; 15 | void set_error(QAnyStringView error); 16 | 17 | template 18 | static auto make_rc() { 19 | return rc(new T, rc_deleter); 20 | } 21 | 22 | protected: 23 | QcmImageResponse(); 24 | 25 | private: 26 | void done(); 27 | static void rc_deleter(QcmImageResponse* p); 28 | 29 | QString m_error; 30 | }; 31 | 32 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/model/app_info.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace qcm 6 | { 7 | namespace model 8 | { 9 | 10 | class AppInfo { 11 | Q_GADGET 12 | QML_VALUE_TYPE(app_info) 13 | public: 14 | AppInfo(); 15 | ~AppInfo(); 16 | 17 | Q_PROPERTY(QString id MEMBER id CONSTANT FINAL) 18 | Q_PROPERTY(QString name MEMBER name CONSTANT FINAL) 19 | Q_PROPERTY(QString version MEMBER version CONSTANT FINAL) 20 | Q_PROPERTY(QString author MEMBER author CONSTANT FINAL) 21 | Q_PROPERTY(QString summary MEMBER summary CONSTANT FINAL) 22 | private: 23 | QString id; 24 | QString name; 25 | QString version; 26 | QString author; 27 | QString summary; 28 | }; 29 | 30 | } // namespace model 31 | } // namespace qcm 32 | -------------------------------------------------------------------------------- /app/include/Qcm/model/empty_model.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "core/core.h" 5 | #include "Qcm/backend_msg.hpp" 6 | 7 | namespace qcm::model 8 | { 9 | class EmptyModel : public QObject { 10 | Q_OBJECT 11 | QML_ANONYMOUS 12 | 13 | Q_PROPERTY(qcm::model::Song song READ song CONSTANT FINAL) 14 | public: 15 | EmptyModel(QObject* parent = nullptr): QObject(parent) {} 16 | ~EmptyModel() {} 17 | 18 | auto song() const -> const model::Song& { return m_song; } 19 | 20 | private: 21 | model::Song m_song; 22 | }; 23 | } // namespace qcm::model -------------------------------------------------------------------------------- /app/include/Qcm/model/item_id.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "core/core.h" 8 | #include "Qcm/qml/enum.hpp" 9 | 10 | namespace qcm::model 11 | { 12 | 13 | // itemid url 14 | // itemid://{id_type}:{library_id}/{id} 15 | class ItemId { 16 | Q_GADGET 17 | QML_VALUE_TYPE(item_id) 18 | Q_PROPERTY(qcm::enums::ItemType type MEMBER m_type) 19 | Q_PROPERTY(QString sid READ idStr) 20 | Q_PROPERTY(QString libraryId READ libraryIdStr) 21 | Q_PROPERTY(bool valid READ valid) 22 | Q_PROPERTY(QUrl url READ toUrl) 23 | Q_PROPERTY(QUrl pageUrl READ toPageUrl) 24 | public: 25 | ItemId(); 26 | ItemId(std::nullptr_t); 27 | ItemId(enums::ItemType, i64 id, i64 library_id = -1); 28 | 29 | explicit ItemId(const QUrl&); 30 | ItemId(const ItemId&) = default; 31 | ItemId& operator=(const ItemId&) = default; 32 | ItemId(ItemId&&) noexcept = default; 33 | ItemId& operator=(ItemId&&) noexcept = default; 34 | 35 | ItemId& operator=(const QUrl&); 36 | 37 | auto type() const -> enums::ItemType; 38 | auto idStr() const -> QString; 39 | auto libraryIdStr() const -> QString; 40 | 41 | auto id() const -> i64; 42 | auto libraryId() const -> i64; 43 | 44 | void setType(enums::ItemType); 45 | void setId(i64); 46 | void setLibraryId(i64); 47 | 48 | void setType(QStringView); 49 | void setId(QStringView); 50 | void setUrl(const QUrl&); 51 | void setLibraryId(QStringView); 52 | 53 | std::strong_ordering operator<=>(const ItemId&) const noexcept; 54 | bool operator==(const ItemId&) const noexcept; 55 | bool operator==(const QUrl&) const; 56 | bool operator==(std::string_view) const; 57 | bool operator==(QStringView) const; 58 | 59 | bool valid() const; 60 | auto toUrl() const -> QUrl; 61 | 62 | auto toPageUrl() const -> QUrl; 63 | 64 | private: 65 | enums::ItemType m_type; 66 | i64 m_library_id; 67 | i64 m_id; 68 | }; 69 | } // namespace qcm::model 70 | 71 | template<> 72 | struct std::hash { 73 | std::size_t operator()(const qcm::model::ItemId& k) const; 74 | }; 75 | -------------------------------------------------------------------------------- /app/include/Qcm/model/lyric.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "core/core.h" 5 | #include "meta_model/qgadget_list_model.hpp" 6 | 7 | namespace qcm 8 | { 9 | 10 | class LyricItem { 11 | Q_GADGET 12 | public: 13 | Q_PROPERTY(qlonglong milliseconds MEMBER milliseconds FINAL) 14 | Q_PROPERTY(QString content MEMBER content FINAL) 15 | 16 | qlonglong milliseconds; 17 | QString content; 18 | }; 19 | 20 | class LyricModel : public meta_model::QGadgetListModel { 21 | Q_OBJECT 22 | QML_ELEMENT 23 | 24 | Q_PROPERTY(qlonglong currentIndex READ currentIndex NOTIFY currentIndexChanged FINAL) 25 | Q_PROPERTY(qlonglong position READ position WRITE setPosition NOTIFY positionChanged FINAL) 26 | Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged FINAL) 27 | public: 28 | LyricModel(QObject* = nullptr); 29 | ~LyricModel(); 30 | 31 | QString source() const; 32 | void setSource(QString); 33 | 34 | qlonglong position() const; 35 | void setPosition(qlonglong); 36 | 37 | qlonglong currentIndex() const; 38 | 39 | Q_SIGNAL void currentIndexChanged(qlonglong); 40 | Q_SIGNAL void positionChanged(); 41 | Q_SIGNAL void sourceChanged(); 42 | 43 | Q_SLOT void setCurrentIndex(qlonglong); 44 | 45 | private: 46 | Q_SLOT void parseLrc(); 47 | Q_SLOT void refreshIndex(); 48 | 49 | private: 50 | qlonglong m_cur_idx; 51 | qlonglong m_position; 52 | QString m_source; 53 | }; 54 | 55 | } // namespace qcm 56 | -------------------------------------------------------------------------------- /app/include/Qcm/model/page_model.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "core/core.h" 4 | #include "meta_model/qgadget_list_model.hpp" 5 | #include "Qcm/macro.hpp" 6 | 7 | 8 | namespace qcm 9 | { 10 | 11 | class Page { 12 | Q_GADGET 13 | QML_ANONYMOUS 14 | public: 15 | GADGET_PROPERTY_DEF(QString, name, name) 16 | GADGET_PROPERTY_DEF(QString, icon, icon) 17 | GADGET_PROPERTY_DEF(QString, source, source) 18 | GADGET_PROPERTY_DEF(bool, cache, cache) 19 | GADGET_PROPERTY_DEF(bool, primary, primary) 20 | }; 21 | 22 | class PageModel : public meta_model::QGadgetListModel { 23 | Q_OBJECT 24 | QML_ANONYMOUS 25 | using base_type = meta_model::QGadgetListModel; 26 | 27 | public: 28 | PageModel(QObject* parent = nullptr); 29 | ~PageModel(); 30 | 31 | static void init_main_pages(PageModel*); 32 | }; 33 | 34 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/model/router_msg.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "Qcm/macro.hpp" 7 | 8 | namespace qcm 9 | { 10 | namespace model 11 | { 12 | 13 | class RouteMsg { 14 | Q_GADGET 15 | public: 16 | RouteMsg() {} 17 | ~RouteMsg() {} 18 | GADGET_PROPERTY_DEF(QUrl, url, url) 19 | GADGET_PROPERTY_DEF(QVariantMap, props, props) 20 | }; 21 | 22 | } // namespace model 23 | } // namespace qcm 24 | -------------------------------------------------------------------------------- /app/include/Qcm/model/share_store.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include "meta_model/share_store.hpp" 9 | #include "core/core.h" 10 | 11 | namespace qcm 12 | { 13 | 14 | struct ShareStoreExt { 15 | using ptr = std::unique_ptr; 16 | ShareStoreExt() 17 | : extra(ptr(new QQmlPropertyMap(), [](QQmlPropertyMap* p) { 18 | p->deleteLater(); 19 | })) { 20 | QJSEngine::setObjectOwnership(extra.get(), QJSEngine::ObjectOwnership::CppOwnership); 21 | } 22 | ptr extra; 23 | }; 24 | 25 | template 26 | class ShareStore 27 | : public meta_model::ShareStore, ShareStoreExt> { 28 | public: 29 | using base_type = meta_model::ShareStore, ShareStoreExt>; 30 | ShareStore(): base_type() {} 31 | }; 32 | 33 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/model/sort_filter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include "meta_model/qgadget_list_model.hpp" 6 | 7 | namespace qcm 8 | { 9 | 10 | struct SortTypeItem { 11 | Q_GADGET 12 | Q_PROPERTY(QString name MEMBER name) 13 | Q_PROPERTY(qint32 type MEMBER type) 14 | public: 15 | qint32 type { 0 }; 16 | QString name; 17 | }; 18 | 19 | class SortTypeModel : public meta_model::QGadgetListModel { 20 | Q_OBJECT 21 | 22 | Q_PROPERTY(qint32 currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY 23 | currentIndexChanged FINAL) 24 | Q_PROPERTY(qint32 currentType READ currentType NOTIFY currentTypeChanged FINAL) 25 | Q_PROPERTY(bool asc READ asc WRITE setAsc NOTIFY ascChanged FINAL) 26 | using Base = meta_model::QGadgetListModel; 27 | 28 | public: 29 | SortTypeModel(QObject* parent); 30 | 31 | auto currentIndex() const -> qint32; 32 | auto currentType() const -> qint32; 33 | void setCurrentIndex(qint32); 34 | 35 | auto asc() const -> bool; 36 | void setAsc(bool); 37 | 38 | Q_SIGNAL void ascChanged(); 39 | Q_SIGNAL void currentIndexChanged(); 40 | Q_SIGNAL void currentTypeChanged(); 41 | 42 | private: 43 | qint32 m_current_idx; 44 | bool m_asc; 45 | }; 46 | 47 | class AlbumSortTypeModel : public SortTypeModel { 48 | Q_OBJECT 49 | QML_ELEMENT 50 | public: 51 | AlbumSortTypeModel(QObject* parent = nullptr); 52 | }; 53 | 54 | class ArtistSortTypeModel : public SortTypeModel { 55 | Q_OBJECT 56 | QML_ELEMENT 57 | public: 58 | ArtistSortTypeModel(QObject* parent = nullptr); 59 | }; 60 | 61 | class SongSortTypeModel : public SortTypeModel { 62 | Q_OBJECT 63 | QML_ELEMENT 64 | public: 65 | SongSortTypeModel(QObject* parent = nullptr); 66 | }; 67 | 68 | class SongSortFilterModel : public QSortFilterProxyModel { 69 | Q_OBJECT 70 | QML_ELEMENT 71 | 72 | Q_PROPERTY(qint32 sortType READ sortType WRITE setSortType NOTIFY sortTypeChanged FINAL) 73 | Q_PROPERTY(bool asc READ asc WRITE setAsc NOTIFY ascChanged FINAL) 74 | public: 75 | SongSortFilterModel(QObject* parent = nullptr); 76 | auto sortType() const -> qint32; 77 | void setSortType(qint32); 78 | bool asc() const; 79 | void setAsc(bool); 80 | 81 | bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; 82 | 83 | Q_SIGNAL void ascChanged(); 84 | Q_SIGNAL void sortTypeChanged(); 85 | 86 | private: 87 | Q_SLOT void freshSortType(); 88 | Q_SLOT void freshSort(); 89 | 90 | qint32 m_sort_type; 91 | bool m_asc; 92 | }; 93 | 94 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/notifier.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "core/core.h" 5 | #include "Qcm/model/item_id.hpp" 6 | #include "Qcm/qml/enum.hpp" 7 | 8 | namespace qcm 9 | { 10 | 11 | class Notifier : public QObject { 12 | Q_OBJECT 13 | QML_ELEMENT 14 | QML_SINGLETON 15 | public: 16 | Notifier(QObject* parent); 17 | ~Notifier(); 18 | static auto instance() -> Notifier*; 19 | static Notifier* create(QQmlEngine*, QJSEngine*); 20 | // make qml prefer create 21 | Notifier() = delete; 22 | 23 | Q_SIGNALS: 24 | void collected(const model::ItemId&, bool); 25 | void collection_synced(enums::CollectionType type, model::ItemId userId, QDateTime time); 26 | void itemChanged(const model::ItemId&); 27 | void providerAdded(const QString& name); 28 | }; 29 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/qml/clipboard.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | namespace qcm 9 | { 10 | class Clipboard : public QObject { 11 | Q_OBJECT 12 | QML_ELEMENT 13 | QML_SINGLETON 14 | 15 | Q_PROPERTY(QString text READ text WRITE setText NOTIFY changed FINAL) 16 | Q_PROPERTY(QClipboard::Mode mode READ mode WRITE setMode NOTIFY modeChanged FINAL) 17 | 18 | public: 19 | Clipboard(QObject* parent = nullptr); 20 | QString text() const; 21 | void setText(const QString&); 22 | 23 | QClipboard::Mode mode() const; 24 | void setMode(QClipboard::Mode); 25 | 26 | Q_INVOKABLE void clear(); 27 | 28 | Q_SIGNAL void changed(); 29 | Q_SIGNAL void modeChanged(); 30 | 31 | private: 32 | QClipboard::Mode m_mode; 33 | }; 34 | } // namespace qcm 35 | -------------------------------------------------------------------------------- /app/include/Qcm/qml/duration.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "core/core.h" 6 | #include "core/core.h" 7 | 8 | namespace qcm 9 | { 10 | struct Duration { 11 | Q_GADGET 12 | QML_VALUE_TYPE(duration) 13 | public: 14 | i64 value { 0 }; 15 | }; 16 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/qml/qml_util.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "Qcm/model/router_msg.hpp" 7 | #include "Qcm/model/item_id.hpp" 8 | #include "Qcm/qml/enum.hpp" 9 | 10 | namespace qcm 11 | { 12 | 13 | namespace qml 14 | { 15 | 16 | class Util : public QObject { 17 | Q_OBJECT 18 | QML_ELEMENT 19 | QML_SINGLETON 20 | 21 | public: 22 | Util(std::monostate); 23 | ~Util() override; 24 | static Util* create(QQmlEngine* qmlEngine, QJSEngine* jsEngine); 25 | 26 | Q_INVOKABLE model::ItemId createItemid() const; 27 | Q_INVOKABLE model::RouteMsg create_route_msg(QVariantMap) const; 28 | 29 | Q_INVOKABLE std::vector collect_ids(QAbstractItemModel* model) const; 30 | Q_INVOKABLE int dyn_card_width(qint32 containerWidth, qint32 spacing = 0) const; 31 | Q_INVOKABLE void print(const QJSValue&) const; 32 | Q_INVOKABLE QUrl special_route_url(enums::SpecialRoute) const; 33 | Q_INVOKABLE model::RouteMsg route_msg(enums::SpecialRoute) const; 34 | 35 | Q_INVOKABLE QUrl image_url(model::ItemId id, 36 | enums::ImageType image_type = enums::ImageType::ImagePrimary) const; 37 | Q_INVOKABLE QUrl image_url(const QString&) const; 38 | Q_INVOKABLE QUrl audio_url(model::ItemId id) const; 39 | 40 | Q_INVOKABLE model::ItemId artistId(QString) const; 41 | Q_INVOKABLE QString mprisTrackid(model::ItemId) const; 42 | Q_INVOKABLE QString joinName(const QJSValue&, const QString& = "/") const; 43 | Q_INVOKABLE QString formatDateTime(const QJSValue&, const QString&) const; 44 | }; 45 | } // namespace qml 46 | 47 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/query/album_query.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "Qcm/query/query.hpp" 5 | #include "Qcm/model/store_item.hpp" 6 | #include "Qcm/model/list_models.hpp" 7 | 8 | namespace qcm 9 | { 10 | class AlbumsQuery : public QueryList, public QueryExtra { 11 | Q_OBJECT 12 | QML_ELEMENT 13 | 14 | public: 15 | AlbumsQuery(QObject* parent = nullptr); 16 | 17 | void reload() override; 18 | void fetchMore(qint32) override; 19 | }; 20 | 21 | class AlbumQuery : public QueryList, public QueryExtra { 22 | Q_OBJECT 23 | QML_ELEMENT 24 | 25 | Q_PROPERTY(qcm::model::ItemId itemId READ itemId WRITE setItemId NOTIFY itemIdChanged) 26 | public: 27 | AlbumQuery(QObject* parent = nullptr); 28 | void reload() override; 29 | 30 | auto itemId() const -> model::ItemId; 31 | void setItemId(model::ItemId); 32 | 33 | Q_SIGNAL void itemIdChanged(); 34 | 35 | private: 36 | model::ItemId m_item_id; 37 | }; 38 | 39 | } // namespace qcm 40 | -------------------------------------------------------------------------------- /app/include/Qcm/query/artist_query.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "meta_model/qgadget_list_model.hpp" 6 | #include "Qcm/query/query.hpp" 7 | #include "Qcm/model/list_models.hpp" 8 | 9 | namespace qcm 10 | { 11 | class ArtistsQuery : public QueryList, public QueryExtra { 12 | Q_OBJECT 13 | QML_ELEMENT 14 | public: 15 | ArtistsQuery(QObject* parent = nullptr); 16 | void reload() override; 17 | void fetchMore(qint32) override; 18 | }; 19 | 20 | class AlbumArtistsQuery : public QueryList, public QueryExtra { 21 | Q_OBJECT 22 | QML_ELEMENT 23 | public: 24 | AlbumArtistsQuery(QObject* parent = nullptr); 25 | void reload() override; 26 | void fetchMore(qint32) override; 27 | }; 28 | 29 | class ArtistQuery : public Query, public QueryExtra { 30 | Q_OBJECT 31 | QML_ELEMENT 32 | 33 | Q_PROPERTY(qcm::model::ItemId itemId READ itemId WRITE setItemId NOTIFY itemIdChanged) 34 | public: 35 | ArtistQuery(QObject* parent = nullptr); 36 | void reload() override; 37 | 38 | auto itemId() const -> model::ItemId; 39 | void setItemId(model::ItemId); 40 | 41 | Q_SIGNAL void itemIdChanged(); 42 | 43 | private: 44 | model::ItemId m_item_id; 45 | }; 46 | 47 | class ArtistAlbumQuery : public QueryList, public QueryExtra { 48 | Q_OBJECT 49 | QML_ELEMENT 50 | Q_PROPERTY(qcm::model::ItemId itemId READ itemId WRITE setItemId NOTIFY itemIdChanged) 51 | public: 52 | ArtistAlbumQuery(QObject* parent = nullptr); 53 | void reload() override; 54 | void fetchMore(qint32) override; 55 | 56 | auto itemId() const -> model::ItemId; 57 | void setItemId(model::ItemId); 58 | 59 | Q_SIGNAL void itemIdChanged(); 60 | 61 | private: 62 | model::ItemId m_item_id; 63 | }; 64 | 65 | } // namespace qcm 66 | -------------------------------------------------------------------------------- /app/include/Qcm/query/lyric_query.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "Qcm/query/query.hpp" 5 | #include "Qcm/model/lyric.hpp" 6 | 7 | namespace qcm 8 | { 9 | class LyricQuery : public Query, public QueryExtra { 10 | Q_OBJECT 11 | QML_ELEMENT 12 | 13 | Q_PROPERTY(qcm::model::ItemId itemId READ itemId WRITE setItemId NOTIFY itemIdChanged) 14 | public: 15 | LyricQuery(QObject* parent = nullptr); 16 | void reload() override; 17 | 18 | auto itemId() const -> model::ItemId; 19 | void setItemId(model::ItemId); 20 | 21 | Q_SIGNAL void itemIdChanged(); 22 | 23 | private: 24 | model::ItemId m_item_id; 25 | }; 26 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/query/mix_query.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "meta_model/qgadget_list_model.hpp" 6 | #include "Qcm/query/query.hpp" 7 | #include "Qcm/model/list_models.hpp" 8 | 9 | namespace qcm 10 | { 11 | 12 | class MixesQuery : public QueryList, public QueryExtra { 13 | Q_OBJECT 14 | QML_ELEMENT 15 | public: 16 | MixesQuery(QObject* parent = nullptr); 17 | void reload() override; 18 | void fetchMore(qint32) override; 19 | }; 20 | 21 | class MixQuery : public Query, public QueryExtra { 22 | Q_OBJECT 23 | QML_ELEMENT 24 | 25 | Q_PROPERTY(qcm::model::ItemId itemId READ itemId WRITE setItemId NOTIFY itemIdChanged) 26 | public: 27 | MixQuery(QObject* parent = nullptr); 28 | void reload() override; 29 | 30 | auto itemId() const -> model::ItemId; 31 | void setItemId(model::ItemId); 32 | 33 | Q_SIGNAL void itemIdChanged(); 34 | 35 | private: 36 | model::ItemId m_item_id; 37 | }; 38 | 39 | } // namespace qcm 40 | -------------------------------------------------------------------------------- /app/include/Qcm/query/old/mix_delete.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #pragma once 4 | 5 | #include 6 | 7 | #include "qcm_interface/query.h" 8 | #include "Qcm/query/query_load.h" 9 | #include "Qcm/sql/item_sql.h" 10 | #include "Qcm/app.hpp" 11 | #include "core/qasio/qt_sql.h" 12 | #include "Qcm/global.hpp" 13 | #include "Qcm/macro.hpp" 14 | #include "Qcm/util/async.inl" 15 | #include "Qcm/sql/collection_sql.h" 16 | 17 | namespace qcm 18 | { 19 | 20 | class MixDelete : public QObject { 21 | Q_OBJECT 22 | public: 23 | MixDelete(QObject* parent = nullptr): QObject(parent) {} 24 | }; 25 | 26 | class MixDeleteQuery : public Query, public QueryExtra { 27 | Q_OBJECT 28 | QML_ELEMENT 29 | 30 | Q_PROPERTY(std::vector itemIds READ ids WRITE setIds NOTIFY idsChanged FINAL) 31 | public: 32 | MixDeleteQuery(QObject* parent = nullptr): Query, public QueryExtra(parent) {} 33 | 34 | auto ids() const { return m_ids; } 35 | void setIds(const std::vector& ids) { 36 | m_ids = ids; 37 | idsChanged(); 38 | } 39 | 40 | Q_SIGNAL void idsChanged(); 41 | 42 | public: 43 | auto mix_delete(const model::ItemId& user_id, 44 | std::span ids) -> task { 45 | auto sql = App::instance()->collect_sql(); 46 | co_await sql->remove(user_id, ids); 47 | } 48 | 49 | void reload() override { 50 | set_status(Status::Querying); 51 | auto self = helper::QWatcher { this }; 52 | auto user_id = Global::instance()->qsession()->user()->userId(); 53 | auto c = Global::instance()->qsession()->client(); 54 | if (! c) return; 55 | spawn( [self, c = c.value(), ids = m_ids, user_id] -> task { 56 | auto out = co_await c.api->delete_mix(c, ids); 57 | if (out) { 58 | co_await self->mix_delete(user_id, ids); 59 | } 60 | co_await asio::post( 61 | asio::bind_executor(qcm::qexecutor(), asio::use_awaitable)); 62 | if (self) { 63 | if (out) { 64 | self->set_status(Status::Finished); 65 | for (auto& el : ids) { 66 | Notifier::instance()->collected(el, false); 67 | } 68 | } else { 69 | self->set_error(convert_from(out.error().what())); 70 | self->set_status(Status::Error); 71 | } 72 | } 73 | co_return; 74 | }); 75 | } 76 | 77 | private: 78 | std::vector m_ids; 79 | }; 80 | 81 | } // namespace qcm 82 | -------------------------------------------------------------------------------- /app/include/Qcm/query/qr_query.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "Qcm/backend.hpp" 5 | #include "Qcm/util/async.hpp" 6 | #include "Qcm/query/query.hpp" 7 | 8 | namespace qcm 9 | { 10 | 11 | class QrAuthUrlQuery : public Query, public QueryExtra { 12 | Q_OBJECT 13 | QML_ELEMENT 14 | 15 | Q_PROPERTY(QString tmpProvider READ tmpProvider WRITE setTmpProvider NOTIFY tmpProviderChanged) 16 | 17 | public: 18 | QrAuthUrlQuery(QObject* parent = nullptr); 19 | void reload() override; 20 | 21 | auto tmpProvider() const -> QString; 22 | void setTmpProvider(const QString&); 23 | 24 | Q_SIGNAL void typeNameChanged(); 25 | Q_SIGNAL void tmpProviderChanged(); 26 | 27 | private: 28 | QString m_tmp_provider; 29 | }; 30 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/query/storage_info.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Qcm/query/query.hpp" 4 | 5 | namespace qcm::qml 6 | { 7 | 8 | class StorageInfo : public QObject { 9 | Q_OBJECT 10 | Q_PROPERTY(double total READ total NOTIFY totalChanged) 11 | public: 12 | StorageInfo(QObject* parent); 13 | 14 | auto total() const -> double; 15 | void setTotal(double); 16 | 17 | Q_SIGNAL void totalChanged(); 18 | 19 | private: 20 | double m_total; 21 | }; 22 | 23 | class StorageInfoQuerier : public Query, public QueryExtra { 24 | Q_OBJECT 25 | QML_ELEMENT 26 | public: 27 | StorageInfoQuerier(QObject* parent = nullptr); 28 | void reload() override; 29 | }; 30 | 31 | } // namespace qcm::qml -------------------------------------------------------------------------------- /app/include/Qcm/query/sync_query.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "Qcm/query/query.hpp" 5 | #include "Qcm/model/store_item.hpp" 6 | #include "Qcm/model/list_models.hpp" 7 | 8 | namespace qcm 9 | { 10 | 11 | class SyncQuery : public Query, public QueryExtra { 12 | Q_OBJECT 13 | QML_ELEMENT 14 | 15 | Q_PROPERTY( 16 | qcm::model::ItemId providerId READ providerId WRITE setProviderId NOTIFY providerIdChanged) 17 | public: 18 | SyncQuery(QObject* parent = nullptr); 19 | void reload() override; 20 | auto providerId() const -> model::ItemId; 21 | void setProviderId(const model::ItemId& v); 22 | 23 | Q_SIGNAL void providerIdChanged(); 24 | 25 | private: 26 | model::ItemId m_provider_id; 27 | }; 28 | 29 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/status/app_state.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "core/core.h" 4 | 5 | #include 6 | #include 7 | #include "Qcm/util/async.hpp" 8 | 9 | namespace qcm 10 | { 11 | class AppState : public QObject { 12 | Q_OBJECT 13 | QML_ANONYMOUS 14 | public: 15 | AppState(QObject* parent = nullptr); 16 | ~AppState(); 17 | 18 | struct Loading { 19 | bool operator==(const Loading&) const = default; 20 | }; 21 | struct Welcome { 22 | bool operator==(const Welcome&) const = default; 23 | }; 24 | struct Main { 25 | bool operator==(const Main&) const = default; 26 | }; 27 | struct Error { 28 | QString err; 29 | bool fatal { false }; 30 | bool operator==(const Error&) const { return true; } 31 | }; 32 | 33 | using StateTypelist = ycore::type_list; 34 | using State = StateTypelist::to; 35 | 36 | template 37 | auto is_state() const -> bool { 38 | return state().index() == StateTypelist::index(); 39 | } 40 | 41 | auto state() const -> const State&; 42 | void set_state(const State&); 43 | 44 | void load_session(); 45 | 46 | Q_SIGNAL void stateChanged(); 47 | Q_SIGNAL void retry(); 48 | 49 | Q_SIGNAL void loading(); 50 | Q_SIGNAL void welcome(); 51 | Q_SIGNAL void main(); 52 | Q_SIGNAL void error(QString); 53 | 54 | Q_SLOT void onQmlCompleted(); 55 | 56 | private: 57 | Q_SLOT void on_retry(); 58 | void triggerSignal(); 59 | 60 | State m_state; 61 | }; 62 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/status/process.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Qcm/backend_msg.hpp" 4 | 5 | namespace qcm 6 | { 7 | void process_msg(msg::QcmMessage&&); 8 | } -------------------------------------------------------------------------------- /app/include/Qcm/status/provider_status.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "meta_model/qgadget_list_model.hpp" 4 | #include "Qcm/backend_msg.hpp" 5 | 6 | namespace qcm 7 | { 8 | class ProviderMetaStatusModel 9 | : public meta_model::QGadgetListModel { 11 | Q_OBJECT 12 | QML_ELEMENT 13 | 14 | using Model = msg::model::ProviderMeta; 15 | using Base = meta_model::QGadgetListModel; 16 | 17 | public: 18 | ProviderMetaStatusModel(QObject* parent = nullptr); 19 | ~ProviderMetaStatusModel(); 20 | }; 21 | 22 | class LibraryStatus : public QObject { 23 | Q_OBJECT 24 | 25 | Q_PROPERTY(QtProtobuf::int64List activedIds READ activedIds NOTIFY activedIdsChanged) 26 | public: 27 | LibraryStatus(QObject* parent = nullptr); 28 | ~LibraryStatus(); 29 | auto activedIds() -> const QtProtobuf::int64List&; 30 | 31 | Q_INVOKABLE bool actived(i64 id) const; 32 | Q_INVOKABLE void setActived(i64 id, bool); 33 | 34 | Q_SIGNAL void activedChanged(i64, bool); 35 | Q_SIGNAL void activedIdsChanged(); 36 | 37 | private: 38 | std::set m_inactived; 39 | QtProtobuf::int64List m_ids; 40 | }; 41 | 42 | class ProviderStatusModel 43 | : public meta_model::QGadgetListModel { 44 | Q_OBJECT 45 | QML_ELEMENT 46 | using Base = 47 | meta_model::QGadgetListModel; 48 | 49 | Q_PROPERTY(bool syncing READ syncing NOTIFY syncingChanged FINAL) 50 | Q_PROPERTY( 51 | qcm::LibraryStatus* libraryStatus READ libraryStatus NOTIFY libraryStatusChanged FINAL) 52 | 53 | public: 54 | ProviderStatusModel(QObject* parent = nullptr); 55 | ~ProviderStatusModel(); 56 | 57 | void updateSyncStatus(const msg::model::ProviderSyncStatus&); 58 | auto syncing() const -> bool; 59 | auto libraryStatus() const -> LibraryStatus*; 60 | 61 | Q_INVOKABLE QVariant itemById(const model::ItemId&) const; 62 | Q_INVOKABLE QVariant metaById(const model::ItemId&) const; 63 | Q_INVOKABLE QString svg(qint32) const; 64 | Q_INVOKABLE QString svg(const model::ItemId&) const; 65 | 66 | Q_SIGNAL void syncingChanged(bool); 67 | Q_SIGNAL void libraryStatusChanged(); 68 | 69 | private: 70 | void setSyncing(bool); 71 | void checkSyncing(); 72 | 73 | bool m_syncing; 74 | LibraryStatus* m_lib_status; 75 | }; 76 | 77 | } // namespace qcm 78 | -------------------------------------------------------------------------------- /app/include/Qcm/store.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Qcm/model/share_store.hpp" 4 | 5 | #include "Qcm/backend_msg.hpp" 6 | 7 | namespace qcm 8 | { 9 | 10 | class AppStore : public QObject { 11 | Q_OBJECT 12 | QML_NAMED_ELEMENT(Store) 13 | QML_SINGLETON 14 | public: 15 | AppStore(QObject* parent); 16 | ~AppStore(); 17 | static auto instance() -> AppStore*; 18 | static AppStore* create(QQmlEngine*, QJSEngine*); 19 | // make qml prefer create 20 | AppStore() = delete; 21 | 22 | Q_INVOKABLE QQmlPropertyMap* extra(model::ItemId id) const; 23 | 24 | using album_store = meta_model::ItemTrait::store_type; 25 | using song_store = meta_model::ItemTrait::store_type; 26 | using artist_store = meta_model::ItemTrait::store_type; 27 | using mix_store = meta_model::ItemTrait::store_type; 28 | 29 | using album_item = album_store::store_item_type; 30 | using song_item = song_store::store_item_type; 31 | 32 | album_store albums; 33 | song_store songs; 34 | artist_store artists; 35 | mix_store mixes; 36 | }; 37 | 38 | namespace model 39 | { 40 | extern const std::set AlbumJsonFields; 41 | extern const std::set ArtistJsonFields; 42 | extern const std::set MixJsonFields; 43 | extern const std::set SongJsonFields; 44 | } // namespace model 45 | 46 | template 47 | auto merge_store_extra(T& store, i64 key, const google::protobuf::Struct& in) { 48 | if (auto extend = store.query_extend(key); extend) { 49 | std::set const* json_fields { nullptr }; 50 | if constexpr (std::same_as) { 51 | json_fields = &model::AlbumJsonFields; 52 | } else if constexpr (std::same_as) { 53 | json_fields = &model::SongJsonFields; 54 | } else if constexpr (std::same_as) { 55 | json_fields = &model::ArtistJsonFields; 56 | } else if constexpr (std::same_as) { 57 | json_fields = &model::MixJsonFields; 58 | } else { 59 | static_assert(false); 60 | } 61 | msg::merge_extra(*(extend->extra), in, *json_fields); 62 | } 63 | } 64 | 65 | } // namespace qcm 66 | -------------------------------------------------------------------------------- /app/include/Qcm/util/async.inl: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "Qcm/util/async.hpp" 8 | #include "core/asio/watch_dog.h" 9 | #include "core/asio/error.h" 10 | #include "core/qasio/qt_watcher.h" 11 | 12 | namespace qcm 13 | { 14 | template 15 | void QAsyncResult::spawn(Fn&& f, const std::source_location loc) { 16 | helper::QWatcher self { this }; 17 | auto main_ex { get_executor() }; 18 | auto ex = asio::make_strand(pool_executor()); 19 | auto alloc = asio::recycling_allocator(); 20 | if (use_queue()) { 21 | push(f, loc); 22 | } else { 23 | asio::co_spawn(ex, 24 | watch_dog().watch(ex, std::forward(f), asio::chrono::minutes(3), alloc), 25 | asio::bind_allocator(alloc, [self, main_ex, loc](std::exception_ptr p) { 26 | helper::handle_asio_exception( 27 | p, 28 | [main_ex, self](std::string_view error) { 29 | auto e_str = std::string(error); 30 | asio::post(main_ex, [self, e_str]() { 31 | if (self) { 32 | self->set_error(QString::fromStdString(e_str)); 33 | self->set_status(Status::Error); 34 | } 35 | }); 36 | }, 37 | loc); 38 | })); 39 | } 40 | } 41 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/util/ex.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "core/asio/task.h" 6 | #include "core/qasio/qt_executor.h" 7 | #include "core/core.h" 8 | 9 | namespace qcm 10 | { 11 | 12 | auto qexecutor_switch() -> task; 13 | auto qexecutor() -> QtExecutor&; 14 | auto pool_executor() -> asio::thread_pool::executor_type; 15 | auto strand_executor() -> asio::strand; 16 | 17 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/util/global_static.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "core/core.h" 7 | 8 | namespace qcm 9 | { 10 | class Global; 11 | class GlobalStatic { 12 | friend class Global; 13 | 14 | public: 15 | GlobalStatic(); 16 | ~GlobalStatic(); 17 | static auto instance() -> GlobalStatic*; 18 | 19 | struct HolderImpl; 20 | 21 | template 22 | struct Holder { 23 | Holder(rc d): d_ptr(d) {} 24 | ~Holder() = default; 25 | auto operator->() -> HolderImpl* { return d_ptr.get(); }; 26 | operator T*() { return static_cast(data(*d_ptr)); }; 27 | 28 | private: 29 | rc d_ptr; 30 | }; 31 | 32 | template 33 | auto add(std::string_view name, T* instance, std::function deleter) -> Holder { 34 | return add_impl(name, static_cast(instance), [deleter](voidp p) { 35 | deleter(static_cast(p)); 36 | }); 37 | } 38 | 39 | private: 40 | auto add_impl(std::string_view name, voidp instance, 41 | std::function deleter) -> rc; 42 | static auto data(HolderImpl&) -> voidp; 43 | 44 | void reset(); 45 | 46 | class Private; 47 | C_DECLARE_PRIVATE(GlobalStatic, d_ptr); 48 | }; 49 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/util/mem.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "core/core.h" 4 | #include 5 | 6 | namespace qcm 7 | { 8 | struct MemResourceMgr { 9 | using pmr_sync_pool = std::pmr::synchronized_pool_resource; 10 | MemoryStatResource* pool_stat { new MemoryStatResource {} }; 11 | pmr_sync_pool* pool { new pmr_sync_pool { pool_stat } }; 12 | 13 | MemoryStatResource* session_mem { new MemoryStatResource { pool } }; 14 | MemoryStatResource* backend_mem { new MemoryStatResource { pool } }; 15 | MemoryStatResource* player_mem { new MemoryStatResource { pool } }; 16 | MemoryStatResource* store_mem { new MemoryStatResource { pool } }; 17 | }; 18 | 19 | auto mem_mgr() -> MemResourceMgr&; 20 | } // namespace qcm -------------------------------------------------------------------------------- /app/include/Qcm/util/path.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "core/core.h" 7 | 8 | namespace qcm 9 | { 10 | auto config_path() -> std::filesystem::path; 11 | auto data_path() -> std::filesystem::path; 12 | auto cache_path() -> std::filesystem::path; 13 | 14 | bool init_path(std::span); 15 | } // namespace qcm 16 | -------------------------------------------------------------------------------- /app/qml/action/AddToMixAction.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Action { 7 | id: root 8 | icon.name: MD.Token.icon.queue 9 | text: qsTr('add to mix') 10 | required property QA.item_id songId 11 | onTriggered: { 12 | MD.Util.showPopup('qrc:/Qcm/App/qml/dialog/AddToMixDialog.qml', { 13 | "songId": root.songId 14 | }, QA.Global.main_win.Overlay.overlay); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/qml/action/AppendListAction.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Action { 7 | id: root 8 | icon.name: MD.Token.icon.playlist_add 9 | text: qsTr('add to list') 10 | onTriggered: { 11 | if (getSongIds) { 12 | QA.Action.queue_ids(root.getSongIds()); 13 | } else if(getSongs) { 14 | QA.Action.queue(root.getSongs()); 15 | } 16 | } 17 | property var getSongIds: null 18 | property var getSongs: null 19 | } 20 | -------------------------------------------------------------------------------- /app/qml/action/CollectAction.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Action { 7 | id: root 8 | property bool liked: false// QA.Global.session.user.collection.contains(itemId) 9 | property var itemId: null 10 | 11 | icon.name: liked ? MD.Token.icon.done : MD.Token.icon.add 12 | text: qsTr(liked ? 'favorited' : 'favorite') 13 | 14 | onTriggered: { 15 | QA.Action.collect(root.itemId, !liked); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/qml/action/ColorSchemeAction.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.App as QA 3 | import Qcm.Material as MD 4 | 5 | MD.Action { 6 | icon.name: MD.Token.isDarkTheme ? MD.Token.icon.dark_mode : MD.Token.icon.light_mode 7 | onTriggered: QA.Global.toggleColorScheme() 8 | } -------------------------------------------------------------------------------- /app/qml/action/CommentAction.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Action { 7 | id: root 8 | required property var itemId 9 | icon.name: MD.Token.icon.comment 10 | text: qsTr('comment') 11 | onTriggered: { 12 | QA.Action.popup_page('qrc:/Qcm/App/qml/page/CommentPage.qml', { 13 | "itemId": root.itemId 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/qml/action/CopyAction.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Action { 7 | text: qsTr('copy') 8 | icon.name: MD.Token.icon.link 9 | property var getCopyString: function () { 10 | return ''; 11 | } 12 | 13 | onTriggered: { 14 | QA.Clipboard.text = getCopyString(); 15 | QA.Action.toast(qsTr("Copied to clipboard"), 2000); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/qml/action/GoToAlbumAction.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Action { 7 | id: root 8 | icon.name: MD.Token.icon.album 9 | text: qsTr('go to album') 10 | 11 | property var albumId 12 | enabled: { 13 | console.error(albumId); 14 | return albumId.valid; 15 | } 16 | onTriggered: { 17 | QA.Action.route_by_id(root.albumId); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/qml/action/GoToArtistAction.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Action { 7 | id: root 8 | icon.name: MD.Token.icon.person 9 | text: qsTr('go to artist') 10 | 11 | property var getItemIds: null 12 | 13 | onTriggered: { 14 | if (!getItemIds) 15 | return; 16 | const itemIds = root.getItemIds(); 17 | if (itemIds.length === 1) { 18 | QA.Action.route_by_id(itemIds[0]); 19 | } else { 20 | MD.Util.showPopup('qrc:/Qcm/App/qml/dialog/GoToArtistDialog.qml', { 21 | "itemIds": itemIds 22 | }, QA.Global.main_win); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/qml/action/MixCreateAction.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Action { 7 | icon.name: MD.Token.icon.add 8 | text: qsTr('create mix') 9 | onTriggered: { 10 | MD.Util.showPopup('qrc:/Qcm/App/qml/dialog/MixCreateDialog.qml', {}, QA.Global.main_win.Overlay.overlay); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/qml/action/PlaynextAction.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Action { 7 | id: root 8 | property QA.item_id songId: song.itemId 9 | property QA.song song 10 | 11 | enabled: root.itemId !== QA.App.playqueue.currentSong.itemId 12 | icon.name: MD.Token.icon.play_arrow 13 | text: qsTr('play next') 14 | 15 | onTriggered: { 16 | if(song.itemId.valid) { 17 | QA.Action.play(root.song); 18 | } else { 19 | QA.Action.play_by_id(root.songId); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/qml/action/SettingAction.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.App as QA 3 | import Qcm.Material as MD 4 | 5 | MD.Action { 6 | id: root 7 | icon.name: MD.Token.icon.settings 8 | text: qsTr('settings') 9 | 10 | onTriggered: { 11 | QA.Action.popup_special(QA.Enum.SRSetting); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/qml/action/SubAction.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQml 3 | 4 | import Qcm.App as QA 5 | import Qcm.Material as MD 6 | 7 | MD.Action { 8 | id: root 9 | property bool liked: false 10 | property var itemId: null 11 | property QtObject querier: null 12 | 13 | icon.name: liked ? MD.Token.icon.done : MD.Token.icon.add 14 | text: qsTr(liked ? 'favorited' : 'favorite') 15 | onTriggered: { 16 | querier.sub = !liked; 17 | querier.itemId = root.itemId; 18 | querier.query(); 19 | } 20 | Binding on liked { 21 | value: root.querier.sub 22 | when: root.querier.status === QA.Enum.Finished 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/qml/component/AuthEmail.qml: -------------------------------------------------------------------------------- 1 | pragma ComponentBehavior: Bound 2 | import QtQuick 3 | import QtQuick.Layouts 4 | import QtQuick.Templates as T 5 | 6 | import Qcm.App as QA 7 | import Qcm.Msg as QM 8 | import Qcm.Material as MD 9 | 10 | ColumnLayout { 11 | id: root 12 | spacing: 12 13 | property string originEmail 14 | property string originPw 15 | readonly property bool modified: m_tf_email.text != originEmail || m_tf_pw.text != originPw 16 | 17 | MD.TextField { 18 | id: m_tf_email 19 | Layout.fillWidth: true 20 | type: MD.Enum.TextFieldFilled 21 | text: root.originEmail 22 | placeholderText: qsTr('Email') 23 | } 24 | MD.TextField { 25 | id: m_tf_pw 26 | Layout.fillWidth: true 27 | type: MD.Enum.TextFieldFilled 28 | text: root.originPw 29 | placeholderText: qsTr('Password') 30 | } 31 | 32 | property QM.emailAuth auth 33 | function updateInfo(info) { 34 | auth.email = m_tf_email.text; 35 | auth.pw = m_tf_pw.text; 36 | info.email = auth; 37 | } 38 | function reset() { 39 | m_tf_email.text = root.originEmail; 40 | m_tf_pw.text = root.originPw; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/qml/component/AuthUsername.qml: -------------------------------------------------------------------------------- 1 | pragma ComponentBehavior: Bound 2 | import QtQuick 3 | import QtQuick.Layouts 4 | import QtQuick.Templates as T 5 | 6 | import Qcm.App as QA 7 | import Qcm.Msg as QM 8 | import Qcm.Material as MD 9 | 10 | ColumnLayout { 11 | id: root 12 | spacing: 12 13 | 14 | property string originUsername 15 | property string originPw 16 | readonly property bool modified: m_tf_username.text != originUsername || m_tf_pw.text != originPw 17 | 18 | MD.TextField { 19 | id: m_tf_username 20 | Layout.fillWidth: true 21 | type: MD.Enum.TextFieldFilled 22 | text: root.originUsername 23 | placeholderText: qsTr('User Name') 24 | } 25 | MD.TextField { 26 | id: m_tf_pw 27 | Layout.fillWidth: true 28 | type: MD.Enum.TextFieldFilled 29 | text: root.originPw 30 | placeholderText: qsTr('Password') 31 | } 32 | property QM.usernameAuth auth 33 | function updateInfo(info) { 34 | auth.username = m_tf_username.text; 35 | auth.pw = m_tf_pw.text; 36 | info.username = auth; 37 | } 38 | 39 | function reset() { 40 | m_tf_username.text = root.originUsername; 41 | m_tf_pw.text = root.originPw; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/qml/component/ByteSlider.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Templates as T 3 | import Qcm.Material as MD 4 | 5 | MD.Slider { 6 | readonly property real byteValue: value > 9 ? (value - 9) * m_GB : value * m_MB 7 | readonly property int m_GB: Math.pow(2, 30) 8 | readonly property int m_MB: 100 * Math.pow(2, 20) 9 | 10 | function setByteValue(v) { 11 | value = v >= m_GB ? v / m_GB + 9 : v / m_MB; 12 | } 13 | 14 | snapMode: T.Slider.SnapAlways 15 | } 16 | -------------------------------------------------------------------------------- /app/qml/component/DebugRect.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | Rectangle { 4 | anchors.fill: parent 5 | opacity: 0.2 6 | color: 'red' 7 | } 8 | -------------------------------------------------------------------------------- /app/qml/component/GridView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.Material as MD 3 | 4 | MD.VerticalGridView { 5 | id: root 6 | 7 | property int spacing: 12 8 | property int fixedCellWidth: 160 9 | readonly property int widthNoMargin: width - leftMargin - rightMargin 10 | cacheBuffer: flow === GridView.FlowTopToBottom ? cellHeight * 2 : cellWidth * 3 11 | 12 | topMargin: 8 13 | bottomMargin: 8 14 | leftMargin: spacing / 2 15 | rightMargin: spacing / 2 16 | cellHeight: fixedCellWidth + 100 17 | cellWidth: widthNoMargin / Math.max(Math.floor(widthNoMargin / (fixedCellWidth + spacing)), 1) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/qml/component/Image.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.App as QA 3 | import Qcm.Material as MD 4 | 5 | import "../js/util.mjs" as Util 6 | 7 | MD.Image { 8 | property size displaySize 9 | property bool fixedSize: true 10 | 11 | sourceSize: { 12 | if(fixedSize) { 13 | return displaySize; 14 | } else { 15 | return QA.App.bound_image_size(displaySize); 16 | } 17 | // QA.App.image_size(displaySize, QA.Global.cover_quality, this) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/qml/component/ListDescription.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.App as QA 3 | import Qcm.Material as MD 4 | 5 | MD.ListItem { 6 | id: root 7 | property string description 8 | visible: root.description.length > 0 9 | 10 | contentItem: Item { 11 | implicitHeight: item_text.implicitHeight 12 | MD.Text { 13 | id: item_text 14 | anchors.left: parent.left 15 | anchors.right: parent.right 16 | maximumLineCount: 3 17 | typescale: MD.Token.typescale.body_medium 18 | text: root.description 19 | } 20 | } 21 | 22 | onClicked: { 23 | QA.Action.popup_page('qrc:/Qcm/App/qml/page/DescriptionPage.qml', { 24 | "text": description 25 | }); 26 | } 27 | } -------------------------------------------------------------------------------- /app/qml/component/ListenIcon.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | 4 | import Qcm.Material as MD 5 | 6 | StackLayout { 7 | id: root 8 | currentIndex: 0 9 | property bool isPlaying: false 10 | property int trackNumber: 0 11 | property int index: -1 12 | property int count: 0 13 | 14 | Binding on currentIndex { 15 | value: 1 16 | when: root.isPlaying 17 | } 18 | 19 | MD.Text { 20 | horizontalAlignment: Qt.AlignHCenter 21 | verticalAlignment: Qt.AlignVCenter 22 | typescale: MD.Token.typescale.body_medium 23 | opacity: 0.6 24 | text: { 25 | if (root.index != -1) { 26 | return root.index + 1; 27 | } 28 | return root.trackNumber == 0 ? '' : root.trackNumber; 29 | } 30 | } 31 | MD.Icon { 32 | name: MD.Token.icon.equalizer 33 | size: 24 34 | color: MD.Token.color.primary 35 | horizontalAlignment: Qt.AlignHCenter 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/qml/component/MainBasePage.qml: -------------------------------------------------------------------------------- 1 | import QtCore 2 | import QtQuick 3 | 4 | import Qcm.Material as MD 5 | 6 | MD.Page { 7 | id: root 8 | padding: 0 9 | 10 | property int pageIndex: -1 11 | 12 | header: MD.AppBar { 13 | id: m_bar 14 | title: root.title 15 | leadingAction: MD.Action { 16 | icon.name: root.canBack ? MD.Token.icon.arrow_back : MD.Token.icon.menu 17 | onTriggered: { 18 | if (root.canBack) 19 | root.back(); 20 | else 21 | m_drawer.open(); 22 | } 23 | } 24 | } 25 | title: m_page_stack.currentItem?.title ?? "" 26 | canBack: m_page_stack.canBack 27 | 28 | //control._canBack ? m_back_action : (Window.window?.barAction ?? null) 29 | //MD.Action { 30 | // id: m_back_action 31 | // icon.name: MD.Token.icon.arrow_back 32 | // onTriggered: { 33 | // if (control.canBack) { 34 | // control.back(); 35 | // } else if (control.pageContext?.canBack) { 36 | // control.pageContext.back(); 37 | // } 38 | // } 39 | //} 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/qml/component/OrderChip.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.Material as MD 3 | 4 | MD.EmbedChip { 5 | property bool asc: true 6 | text: "order" 7 | leftPadding: 8 8 | trailingIconName: asc ? MD.Token.icon.arrow_upward : MD.Token.icon.arrow_downward 9 | } 10 | -------------------------------------------------------------------------------- /app/qml/component/PageContainer.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | import Qcm.Material as MD 4 | 5 | MD.StackView { 6 | id: root 7 | 8 | readonly property alias current_page: root.m_current_page 9 | property string m_current_page: '' 10 | readonly property var m_page_cache: new Map() 11 | 12 | readonly property bool canBack: currentItem?.canBack ?? false 13 | 14 | function back() { 15 | currentItem.back(); 16 | } 17 | 18 | function switchByKey(key, url_or_comp, props, is_cache) { 19 | if (is_cache) { 20 | let cache = m_page_cache.get(key); 21 | if (!cache) { 22 | cache = MD.Util.createItem(url_or_comp, props, null); 23 | cache.visible = false; 24 | m_page_cache.set(key, cache); 25 | } 26 | replace(currentItem, cache); 27 | } else { 28 | replace(currentItem, url_or_comp, props); 29 | } 30 | m_current_page = key; 31 | } 32 | function switchTo(page_url, props, is_cache = true) { 33 | const key = JSON.stringify({ 34 | "url": page_url, 35 | "props": props 36 | }); 37 | if (key === m_current_page) 38 | return; 39 | switchByKey(key, page_url, props, is_cache); 40 | } 41 | function switchToComp(name, comp, props, is_cache = true) { 42 | switchByKey(name, comp, props, is_cache); 43 | } 44 | 45 | Component.onDestruction: { 46 | m_page_cache.forEach(page => { 47 | return page.destroy(); 48 | }); 49 | } 50 | // replaceEnter: MD.FadeThrough {} 51 | // pushEnter: MD.FadeThrough {} 52 | } 53 | -------------------------------------------------------------------------------- /app/qml/component/PageStack.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Templates as T 3 | 4 | import Qcm.Material as MD 5 | 6 | MD.StackView { 7 | id: root 8 | 9 | readonly property var pages: new Map() 10 | 11 | property bool canBack: (currentItem?.canBack ?? false) || depth > 1 12 | function back() { 13 | if (currentItem?.canBack) { 14 | currentItem.back(); 15 | } else { 16 | pop_page(); 17 | } 18 | } 19 | 20 | implicitHeight: currentItem.implicitHeight 21 | implicitWidth: currentItem.implicitWidth 22 | 23 | Behavior on implicitHeight { 24 | NumberAnimation { 25 | easing.type: Easing.InOutQuad 26 | duration: 350 27 | } 28 | } 29 | 30 | function push_page(url, props = {}) { 31 | const key = JSON.stringify({ 32 | "url": url, 33 | "props": MD.Util.paramsString(props) 34 | }); 35 | let item = pages.get(key); 36 | if (item) { 37 | popup(item); 38 | } else { 39 | item = MD.Util.createItem(url, props, null); 40 | if (item) { 41 | pages.set(key, item); 42 | push(item); 43 | } 44 | } 45 | } 46 | 47 | function pop_page(bottom) { 48 | if (bottom === null) { 49 | pop(null); 50 | pages.forEach(v => { 51 | return v.destroy(1000); 52 | }); 53 | pages.clear(); 54 | } else { 55 | const item = pop(); 56 | Array.from(pages.entries()).filter(el => { 57 | return el[1] === item; 58 | }).map(el => { 59 | pages.delete(el[0]); 60 | item.destroy(1000); 61 | }); 62 | } 63 | } 64 | 65 | function popup(item) { 66 | const items = []; 67 | while (currentItem !== item) 68 | items.unshift(pop(T.StackView.Immediate)); 69 | if (items.length === 0) 70 | return; 71 | const target = pop(T.StackView.Immediate); 72 | push(items, T.StackView.Immediate); 73 | push(target); 74 | } 75 | 76 | Component.onDestruction: { 77 | pages.forEach(page => { 78 | page?.destroy(); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/qml/component/RoundImage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | Canvas { 4 | id: canvas 5 | 6 | required property Image image 7 | readonly property int status: canvas.image.status 8 | 9 | implicitHeight: image.implicitHeight 10 | implicitWidth: image.implicitWidth 11 | onStatusChanged: { 12 | if (status === Image.Ready) 13 | canvas.imageLoaded(); 14 | 15 | } 16 | Component.onCompleted: { 17 | image.visible = false; 18 | image.parent = canvas; 19 | } 20 | onPaint: { 21 | if (image.paintedHeight === 0) 22 | return ; 23 | 24 | const ctx = getContext("2d"); 25 | const c = canvas; 26 | const ratio = Screen.devicePixelRatio; 27 | ctx.beginPath(); 28 | ctx.arc(c.width / 2, c.height / 2, Math.min(c.width, c.height) / 2, 0, Math.PI * 2, false); 29 | ctx.closePath(); 30 | ctx.clip(); 31 | ctx.scale(1 / ratio, 1 / ratio); 32 | ctx.drawImage(image, image.x, image.y, c.width * ratio, c.width * ratio); 33 | ctx.scale(ratio, ratio); 34 | } 35 | onImageLoaded: { 36 | requestPaint(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/qml/component/RouteMsg.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | QtObject { 4 | id: root 5 | 6 | required property var props 7 | required property url qml 8 | } 9 | -------------------------------------------------------------------------------- /app/qml/component/SettingRow.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import Qcm.Material as MD 4 | 5 | MD.ListItem { 6 | id: root 7 | Layout.fillWidth: true 8 | font.capitalization: Font.Capitalize 9 | corners: { 10 | let s = root.start ? MD.Token.shape.corner.medium : 0; 11 | let e = root.end ? MD.Token.shape.corner.medium : 0; 12 | return MD.Util.corner(s, e); 13 | } 14 | property bool canInput: true 15 | property bool start: false 16 | property bool end: false 17 | 18 | mdState.backgroundColor: MD.MProp.color.surface_container 19 | 20 | MD.InputBlock { 21 | when: !root.canInput 22 | target: root 23 | } 24 | 25 | Component.onCompleted: { 26 | if (root.trailing?.clicked) 27 | clicked.connect(root.trailing.clicked); 28 | if (root.trailing?.checkable) 29 | clicked.connect(root.trailing.toggle); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/qml/component/SettingSection.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import Qcm.Material as MD 4 | import Qcm.App as QA 5 | 6 | MD.Pane { 7 | id: root 8 | default property alias columnData: m_column.data 9 | property alias title: m_title.text 10 | padding: 0 11 | font.capitalization: Font.Capitalize 12 | 13 | signal columnChanged 14 | onColumnChanged: { 15 | if (!visible) 16 | return; 17 | const children = m_column.children.filter(el => { 18 | const ok = el instanceof QA.SettingRow; 19 | if (ok) { 20 | el.start = false; 21 | el.end = false; 22 | } 23 | return ok && el.visible; 24 | }); 25 | let start = children[0]; 26 | if (start) 27 | start.start = true; 28 | let end = children[children.length - 1]; 29 | if (end) 30 | end.end = true; 31 | } 32 | Component.onCompleted: { 33 | visibleChanged.connect(columnChanged); 34 | columnChanged(); 35 | } 36 | 37 | ColumnLayout { 38 | id: m_column 39 | anchors.fill: parent 40 | spacing: root.spacing 41 | 42 | MD.Label { 43 | id: m_title 44 | Layout.leftMargin: 12 45 | Layout.rightMargin: 12 46 | MD.MProp.textColor: MD.Token.color.primary 47 | typescale: MD.Token.typescale.title_medium 48 | visible: text 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/qml/component/SongTag.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.Material as MD 3 | 4 | MD.Pane { 5 | id: root 6 | 7 | required property string tag 8 | MD.MProp.textColor: MD.Token.color.tertiary 9 | height: tag_label.lineHeight 10 | padding: 0 11 | horizontalPadding: 2 12 | 13 | MD.Text { 14 | id: tag_label 15 | text: root.tag 16 | font.capitalization: Font.AllUppercase 17 | typescale: MD.Token.typescale.label_small 18 | verticalAlignment: Qt.AlignVCenter 19 | } 20 | 21 | background: Rectangle { 22 | color: 'transparent' 23 | implicitHeight: 0 24 | radius: 1 25 | border.width: 1 26 | border.color: root.MD.MProp.textColor 27 | } 28 | MD.InputBlock { 29 | target: root 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/qml/component/SyncingLabel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.Material as MD 3 | import Qcm.App as QA 4 | 5 | MD.Label { 6 | visible: QA.App.providerStatus.syncing 7 | text: 'Syncing database, please wait.' 8 | maximumLineCount: 2 9 | opacity: 0.8 10 | } 11 | -------------------------------------------------------------------------------- /app/qml/component/VolumeButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.App as QA 3 | import Qcm.Material as MD 4 | 5 | MD.IconButton { 6 | id: control 7 | property real volume: 0 8 | icon.name: volume > 0.5 ? MD.Token.icon.volume_up : (QA.Global.player.volume == 0.0 ? MD.Token.icon.volume_mute : MD.Token.icon.volume_down) 9 | 10 | signal volumeSeted(volume: real) 11 | 12 | Component { 13 | id: comp_popup 14 | MD.Popup { 15 | dim: false 16 | modal: true 17 | height: 120 18 | width: slider.handle.width * 1.5 19 | property alias value: slider.value 20 | contentItem: MD.Slider { 21 | id: slider 22 | orientation: Qt.Vertical 23 | signal volumeSeted(volume: real) 24 | onMoved: { 25 | volumeSeted(value); 26 | } 27 | } 28 | } 29 | } 30 | 31 | onClicked: { 32 | const popup = MD.Util.showPopup(comp_popup, { 33 | "value": control.volume, 34 | "y": 0 35 | }, this); 36 | popup.x = (control.width - popup.width) / 2.0; 37 | popup.y = -popup.height; 38 | const slider = popup.contentItem; 39 | popup.contentItem.volumeSeted.connect(control.volumeSeted); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/qml/delegate/CommentDelegate.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | 3 | import Qcm.Material as MD 4 | import Qcm.App as QA 5 | 6 | 7 | MD.ListItem { 8 | id: root 9 | width: ListView.view.width 10 | text: model.user.name 11 | supportText: Qt.formatDateTime(model.time, 'yyyy.M.d') 12 | leader: QA.Image { 13 | radius: 8 14 | source: QA.Util.image_url(model.user.picUrl) 15 | sourceSize.height: 48 16 | sourceSize.width: 48 17 | } 18 | 19 | below: MD.Text { 20 | text: model.content 21 | maximumLineCount: -1 22 | typescale: MD.Token.typescale.body_large 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/qml/delegate/PicCardDelegate.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Card { 7 | id: root 8 | 9 | property alias image: image 10 | property string subText 11 | property int picWidth: 160 12 | 13 | horizontalPadding: 0 14 | 15 | contentItem: ColumnLayout { 16 | QA.Image { 17 | id: image 18 | Layout.preferredWidth: displaySize.width 19 | Layout.preferredHeight: displaySize.height 20 | displaySize: Qt.size(root.picWidth, root.picWidth) 21 | fixedSize: false 22 | radius: root.background.radius 23 | } 24 | 25 | ColumnLayout { 26 | Layout.leftMargin: 16 27 | Layout.rightMargin: 16 28 | Layout.topMargin: 8 29 | Layout.bottomMargin: 8 30 | Layout.fillWidth: true 31 | 32 | MD.Label { 33 | id: label 34 | Layout.fillWidth: true 35 | text: root.text 36 | maximumLineCount: 2 37 | typescale: MD.Token.typescale.body_medium 38 | } 39 | 40 | MD.Label { 41 | id: label_sub 42 | Layout.alignment: Qt.AlignHCenter 43 | text: root.subText 44 | visible: !!text 45 | opacity: 0.6 46 | typescale: MD.Token.typescale.body_medium 47 | maximumLineCount: 1 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/qml/delegate/PicCardGridDelegate.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.App as QA 3 | 4 | Item { 5 | id: root 6 | 7 | property alias text: m_card.text 8 | property alias subText: m_card.subText 9 | property alias picWidth: m_card.picWidth 10 | property alias image: m_card.image 11 | 12 | signal clicked 13 | 14 | width: GridView.view.cellWidth 15 | height: GridView.view.cellHeight 16 | implicitHeight: children[0].implicitHeight 17 | 18 | QA.PicCardDelegate { 19 | id: m_card 20 | anchors.horizontalCenter: parent.horizontalCenter 21 | width: parent.GridView.view.fixedCellWidth 22 | picWidth: parent.GridView.view.fixedCellWidth 23 | } 24 | 25 | Connections { 26 | target: m_card 27 | function onClicked() { 28 | root.clicked(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/qml/dialog/AddToMixDialog.qml: -------------------------------------------------------------------------------- 1 | pragma ComponentBehavior: Bound 2 | import QtQuick 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Dialog { 7 | id: root 8 | title: qsTr(`add to mix`) 9 | padding: 0 10 | standardButtons: MD.Dialog.Cancel 11 | 12 | required property var songId 13 | 14 | MD.VerticalListView { 15 | id: m_view 16 | anchors.fill: parent 17 | expand: true 18 | topMargin: 8 19 | bottomMargin: 8 20 | 21 | busy: m_qr.status === QA.Enum.Querying 22 | model: m_qr.data 23 | header: MD.ListItem { 24 | font.capitalization: Font.Capitalize 25 | width: ListView.view.width 26 | leader: Item { 27 | implicitHeight: 48 28 | implicitWidth: 48 29 | MD.Icon { 30 | anchors.centerIn: parent 31 | name: MD.Token.icon.add 32 | } 33 | } 34 | corners: indexCorners(0, 1 + ListView.view.count, 16) 35 | action: QA.MixCreateAction {} 36 | } 37 | delegate: MD.ListItem { 38 | required property int index 39 | required property var model 40 | text: model.name 41 | supportText: `${model.trackCount} songs` 42 | width: ListView.view.width 43 | maximumLineCount: 2 44 | leader: QA.Image { 45 | radius: 8 46 | source: QA.Util.image_url(model.picUrl) 47 | sourceSize.height: 48 48 | sourceSize.width: 48 49 | } 50 | corners: indexCorners(index + 1, count + 1, 16) 51 | onImplicitWidthChanged: { 52 | const v = ListView.view; 53 | v.implicitWidth = Math.max(v.implicitWidth, implicitWidth); 54 | } 55 | onClicked: { 56 | m_qr_manipulate.mixId = model.itemId; 57 | m_qr_manipulate.itemIds = [root.songId]; 58 | m_qr_manipulate.reload(); 59 | } 60 | } 61 | 62 | QA.UserMixQuery { 63 | id: m_qr 64 | Component.onCompleted: reload() 65 | } 66 | 67 | QA.MixManipulateQuery { 68 | id: m_qr_manipulate 69 | oper: QA.Enum.ManipulateMixAdd 70 | 71 | onFinished: { 72 | MD.Util.closePopup(root); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/qml/dialog/GoToArtistDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.App as QA 3 | import Qcm.Material as MD 4 | 5 | MD.Dialog { 6 | id: root 7 | property list itemIds 8 | title: qsTr('go to artist') 9 | standardButtons: MD.Dialog.Cancel 10 | 11 | MD.VerticalListView { 12 | anchors.fill: parent 13 | model: root.itemIds 14 | 15 | expand: true 16 | 17 | delegate: MD.ListItem { 18 | required property var modelData 19 | required property int index 20 | width: ListView.view.width 21 | corners: indexCorners(index, count, 16) 22 | 23 | leader: QA.Image { 24 | source: QA.Util.image_url(m_qr.data.item.itemId) 25 | sourceSize.height: 48 26 | sourceSize.width: 48 27 | radius: 24 28 | } 29 | text: m_qr.data.item.name 30 | onImplicitWidthChanged: { 31 | const v = ListView.view; 32 | v.implicitWidth = Math.max(v.implicitWidth, implicitWidth); 33 | } 34 | 35 | QA.ArtistQuery { 36 | id: m_qr 37 | itemId: modelData 38 | onItemIdChanged: { 39 | reload(); 40 | } 41 | } 42 | onClicked: { 43 | MD.Util.closePopup(root); 44 | QA.Action.route_by_id(modelData); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/qml/dialog/MixCreateDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Dialog { 7 | id: root 8 | title: qsTr('Create A Mix') 9 | standardButtons: MD.Dialog.Ok | MD.Dialog.Cancel 10 | 11 | onAccepted: { 12 | item_input.validator = item_valid; 13 | if (item_input.acceptableInput) { 14 | m_query.reload(); 15 | } else { 16 | item_input.focus = true; 17 | } 18 | } 19 | 20 | ColumnLayout { 21 | anchors.fill: parent 22 | spacing: 24 23 | 24 | ColumnLayout { 25 | MD.TextField { 26 | id: item_input 27 | Layout.fillWidth: true 28 | placeholderText: qsTr('mix name') 29 | } 30 | } 31 | RegularExpressionValidator { 32 | id: item_valid 33 | regularExpression: /.+/ 34 | } 35 | 36 | QA.MixCreateQuery { 37 | id: m_query 38 | name: item_input.text 39 | onFinished: { 40 | // QA.App.playqueueCreated(); 41 | MD.Util.closePopup(root); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/qml/js/canvas.mjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hypengw/Qcm/33600fc69a690fedbe7b4a2c1dadb9dc35a0fa2f/app/qml/js/canvas.mjs -------------------------------------------------------------------------------- /app/qml/js/util.mjs: -------------------------------------------------------------------------------- 1 | 2 | const BYTE_UNITS = [ 3 | 'B', 4 | 'kB', 5 | 'MB', 6 | 'GB', 7 | 'TB', 8 | 'PB', 9 | 'EB', 10 | 'ZB', 11 | 'YB', 12 | ]; 13 | 14 | export function pretty_bytes(number, maxFrac = 0) { 15 | const UNITS = BYTE_UNITS; 16 | const exponent = number < 1 17 | ? 0 18 | : Math.min(Math.floor(Math.log(number) / Math.log(1024)), UNITS.length - 1); 19 | const unit = UNITS[exponent]; 20 | const prefix = number < 0 ? '-' : ''; 21 | 22 | const num_str = (number / 1024 ** exponent).toFixed(maxFrac); 23 | return `${prefix}${num_str} ${unit}`; 24 | } 25 | 26 | export function array_split(arr, count) { 27 | if (!arr.length) return []; 28 | const header = arr.slice(0, count); 29 | return [header].concat(array_split(arr.slice(count), count)); 30 | } 31 | 32 | export function bound_img_size(x) { 33 | return 30 * (1 << Math.ceil(Math.log2( x / 30 ))); 34 | } -------------------------------------------------------------------------------- /app/qml/menu/AlbumMenu.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.App as QA 3 | import Qcm.Material as MD 4 | 5 | MD.Menu { 6 | id: root 7 | 8 | required property QA.item_id itemId 9 | 10 | dim: false 11 | font.capitalization: Font.Capitalize 12 | modal: true 13 | 14 | QA.CommentAction { 15 | itemId: root.itemId 16 | } 17 | 18 | QA.CollectAction { 19 | itemId: root.itemId 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/qml/menu/ArtistMenu.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.App as QA 3 | import Qcm.Material as MD 4 | 5 | MD.Menu { 6 | id: root 7 | 8 | required property QA.item_id itemId 9 | 10 | dim: false 11 | font.capitalization: Font.Capitalize 12 | modal: true 13 | 14 | QA.CollectAction { 15 | itemId: root.itemId 16 | } 17 | } -------------------------------------------------------------------------------- /app/qml/menu/MixMenu.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.App as QA 3 | import Qcm.Material as MD 4 | 5 | MD.Menu { 6 | id: root 7 | 8 | required property QA.item_id itemId 9 | required property QA.item_id userId 10 | readonly property bool isUserPlaylist: QA.Global.session.user.userId === root.userId 11 | 12 | dim: false 13 | font.capitalization: Font.Capitalize 14 | modal: true 15 | 16 | QA.CommentAction { 17 | itemId: root.itemId 18 | } 19 | 20 | QA.CollectAction { 21 | enabled: !root.isUserPlaylist 22 | itemId: root.itemId 23 | } 24 | 25 | MD.Action { 26 | enabled: root.isUserPlaylist 27 | icon.name: MD.Token.icon.delete 28 | text: qsTr('delete') 29 | onTriggered: { 30 | m_query_delete.itemIds = [root.itemId]; 31 | m_query_delete.reload(); 32 | } 33 | } 34 | QA.MixDeleteQuery { 35 | id: m_query_delete 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/qml/menu/ProgramMenu.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.App as QA 3 | import Qcm.Material as MD 4 | 5 | MD.Menu { 6 | id: root 7 | 8 | required property QA.item_id itemId 9 | required property QA.song song 10 | 11 | dim: false 12 | font.capitalization: Font.Capitalize 13 | modal: true 14 | 15 | QA.PlaynextAction { 16 | song: root.song 17 | } 18 | QA.CommentAction { 19 | itemId: root.itemId 20 | } 21 | } -------------------------------------------------------------------------------- /app/qml/menu/RadioMenu.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.App as QA 3 | import Qcm.Material as MD 4 | 5 | MD.Menu { 6 | id: root 7 | 8 | required property QA.item_id itemId 9 | 10 | dim: false 11 | font.capitalization: Font.Capitalize 12 | modal: true 13 | 14 | 15 | QA.CollectAction { 16 | itemId: root.itemId 17 | } 18 | } -------------------------------------------------------------------------------- /app/qml/menu/SongMenu.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.App as QA 3 | import Qcm.Material as MD 4 | 5 | MD.Menu { 6 | id: root 7 | 8 | property QA.item_id itemId 9 | property QA.item_id sourceId 10 | property bool canDelete: false 11 | // no aot, it's bugly 12 | property var song: QA.App.empty.song 13 | readonly property QA.item_id _itemId: { 14 | return itemId.valid ? itemId : song.itemId; 15 | } 16 | readonly property list artists: { 17 | const ex = QA.Store.extra(_itemId); 18 | return ex?.artists ?? []; 19 | } 20 | 21 | dim: false 22 | font.capitalization: Font.Capitalize 23 | modal: true 24 | 25 | QA.PlaynextAction { 26 | enabled: root._itemId !== QA.App.playqueue.currentSong.itemId 27 | songId: root._itemId 28 | } 29 | 30 | QA.AddToMixAction { 31 | songId: root._itemId 32 | } 33 | 34 | QA.GoToAlbumAction { 35 | albumId: root.song.albumId 36 | } 37 | 38 | QA.GoToArtistAction { 39 | enabled: root.artists.length > 0 40 | getItemIds: function () { 41 | return root.artists.map(el => QA.Util.artistId(el.id)); 42 | } 43 | } 44 | QA.CommentAction { 45 | itemId: root._itemId 46 | } 47 | 48 | MD.Action { 49 | enabled: root.canDelete 50 | icon.name: MD.Token.icon.delete 51 | text: qsTr('delete') 52 | onTriggered: 53 | //m_qr_manipulate.mixId = root.sourceId; 54 | //m_qr_manipulate.itemIds = [root._itemId]; 55 | //m_qr_manipulate.reload(); 56 | {} 57 | } 58 | 59 | MD.Menu { 60 | title: qsTr('copy') 61 | QA.CopyAction { 62 | text: qsTr('title') 63 | icon.name: MD.Token.icon.title 64 | getCopyString: function () { 65 | return root.song.name; 66 | } 67 | } 68 | QA.CopyAction { 69 | text: qsTr('album') 70 | icon.name: MD.Token.icon.album 71 | getCopyString: function () { 72 | return root.song.album.name; 73 | } 74 | } 75 | QA.CopyAction { 76 | text: qsTr('url') 77 | icon.name: MD.Token.icon.link 78 | getCopyString: function () { 79 | return QA.Global.server_url(root.song.itemId); 80 | } 81 | } 82 | } 83 | // QA.MixManipulateQuery { 84 | // id: m_qr_manipulate 85 | // oper: QA.Enum.ManipulateMixDel 86 | // } 87 | } 88 | -------------------------------------------------------------------------------- /app/qml/menu/SortMenu.qml: -------------------------------------------------------------------------------- 1 | pragma ComponentBehavior: Bound 2 | import QtQuick 3 | import Qcm.Material as MD 4 | 5 | MD.Menu { 6 | id: root 7 | model: null 8 | contentDelegate: MD.MenuItem { 9 | required property var model 10 | text: model.name 11 | icon.name: { 12 | const m = root.model; 13 | if (m.currentIndex == model.index) { 14 | return m.asc ? MD.Token.icon.arrow_upward : MD.Token.icon.arrow_downward; 15 | } else { 16 | return ' '; 17 | } 18 | } 19 | onClicked: { 20 | const m = root.model; 21 | if (m.currentIndex == model.index) { 22 | m.asc = !m.asc; 23 | } else { 24 | m.currentIndex = model.index; 25 | } 26 | root.close(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/qml/page/CommentPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.App as QA 3 | import Qcm.Material as MD 4 | 5 | MD.Page { 6 | id: root 7 | padding: 0 8 | title: m_view.model.total ? `Comments(${m_view.model.total})` : 'Comment' 9 | bottomPadding: radius 10 | scrolling: !m_view.atYBeginning 11 | 12 | property QA.item_id itemId 13 | 14 | MD.VerticalListView { 15 | id: m_view 16 | anchors.fill: parent 17 | expand: true 18 | busy: m_query.status === QA.Enum.Querying 19 | model: m_query.data 20 | 21 | delegate: QA.CommentDelegate {} 22 | 23 | QA.CommentsQuery { 24 | id: m_query 25 | delay: false 26 | itemId: root.itemId 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/qml/page/DescriptionPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Page { 7 | id: root 8 | property alias text: label.text 9 | title: qsTr('description') 10 | bottomPadding: radius 11 | scrolling: !m_flick.atYBeginning 12 | 13 | MD.VerticalFlickable { 14 | id: m_flick 15 | 16 | anchors.fill: parent 17 | leftMargin: 24 18 | rightMargin: 24 19 | 20 | ColumnLayout { 21 | id: content 22 | 23 | height: implicitHeight 24 | width: parent.width 25 | spacing: 12 26 | 27 | MD.TextEdit { 28 | id: label 29 | Layout.fillWidth: true 30 | readOnly: true 31 | wrapMode: Text.Wrap 32 | typescale: MD.Token.typescale.body_medium 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/qml/page/StatusPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Page { 7 | id: root 8 | title: qsTr('status') 9 | padding: 0 10 | bottomPadding: radius 11 | scrolling: !m_flick.atYBeginning 12 | 13 | MD.VerticalFlickable { 14 | id: m_flick 15 | anchors.fill: parent 16 | topMargin: 4 17 | leftMargin: 24 18 | rightMargin: 24 19 | bottomMargin: 4 20 | 21 | ColumnLayout { 22 | id: content 23 | 24 | height: implicitHeight 25 | width: parent.width 26 | spacing: 8 27 | 28 | MD.Text { 29 | Layout.fillWidth: true 30 | typescale: MD.Token.typescale.body_large 31 | text: QA.App.import_path_list().join('\n') 32 | maximumLineCount: 12 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/qml/page/setting/MiscSetting.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Page { 7 | id: root 8 | font.capitalization: Font.Capitalize 9 | title: qsTr('settings') 10 | bottomPadding: radius 11 | scrolling: !m_flick.atYBeginning 12 | 13 | MD.VerticalFlickable { 14 | id: m_flick 15 | anchors.fill: parent 16 | leftMargin: 0 17 | rightMargin: 0 18 | 19 | ColumnLayout { 20 | height: implicitHeight 21 | spacing: 12 22 | width: parent.width 23 | 24 | SettingSection { 25 | id: sec_misc 26 | Layout.fillWidth: true 27 | title: qsTr('misc') 28 | 29 | SettingRow { 30 | Layout.fillWidth: true 31 | font.capitalization: Font.MixedCase 32 | text: qsTr('Random Playback Mode') 33 | supportText: qsTr('Truly random playback instead of shuffle') 34 | 35 | actionItem: MD.Switch { 36 | checked: QA.App.playqueue.randomMode 37 | onCheckedChanged: { 38 | QA.App.playqueue.randomMode = checked; 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/qml/page/setting/ProviderManagePage.qml: -------------------------------------------------------------------------------- 1 | pragma ComponentBehavior: Bound 2 | import QtCore 3 | import QtQuick 4 | import QtQuick.Layouts 5 | import Qcm.App as QA 6 | import Qcm.Material as MD 7 | 8 | MD.Page { 9 | id: root 10 | font.capitalization: Font.Capitalize 11 | title: qsTr('provider manage') 12 | bottomPadding: radius 13 | scrolling: !m_view.atYBeginning 14 | 15 | QA.DeleteProviderQuery { 16 | id: m_delete_query 17 | } 18 | 19 | MD.VerticalListView { 20 | id: m_view 21 | anchors.fill: parent 22 | leftMargin: 0 23 | rightMargin: 0 24 | expand: true 25 | model: QA.App.providerStatus 26 | 27 | delegate: MD.ListItem { 28 | required property int index 29 | required property var model 30 | 31 | width: parent.width 32 | leader: MD.IconSvg { 33 | sourceData: QA.App.providerStatus.svg(index) 34 | size: 24 35 | } 36 | text: model.name 37 | trailing: MD.IconButton { 38 | icon.name: MD.Token.icon.close 39 | onClicked: { 40 | const query = m_delete_query; 41 | query.providerId = model.itemId; 42 | query.reload(); 43 | } 44 | } 45 | 46 | onClicked: { 47 | root.MD.MProp.page.pushItem('qrc:/Qcm/App/qml/page/edit/ProviderEditPage.qml', { 48 | itemId: model.itemId 49 | }); 50 | } 51 | } 52 | 53 | footer: ColumnLayout { 54 | anchors.left: parent.left 55 | anchors.right: parent.right 56 | anchors.leftMargin: 12 57 | anchors.rightMargin: 12 58 | 59 | MD.Space { 60 | spacing: 12 61 | } 62 | MD.Button { 63 | Layout.alignment: Qt.AlignRight 64 | type: MD.Enum.BtElevated 65 | action: MD.Action { 66 | text: qsTr('add') 67 | onTriggered: { 68 | root.MD.MProp.page.pushItem('qrc:/Qcm/App/qml/page/AddProviderPage.qml'); 69 | } 70 | } 71 | } 72 | MD.Space { 73 | spacing: 12 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/qml/page/setting/SettingPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Page { 7 | id: root 8 | font.capitalization: Font.Capitalize 9 | title: qsTr('settings') 10 | bottomPadding: radius 11 | scrolling: !m_flick.atYBeginning 12 | 13 | MD.VerticalFlickable { 14 | id: m_flick 15 | anchors.fill: parent 16 | leftMargin: 0 17 | rightMargin: 0 18 | 19 | ColumnLayout { 20 | width: parent.width 21 | height: implicitHeight 22 | QA.SettingSection { 23 | Layout.fillWidth: true 24 | spacing: 2 25 | horizontalPadding: 16 26 | QA.SettingRow { 27 | icon.name: MD.Token.icon.hard_drive 28 | text: 'provider manage' 29 | supportText: '' 30 | onClicked: { 31 | root.MD.MProp.page.pushItem('qrc:/Qcm/App/qml/page/setting/ProviderManagePage.qml'); 32 | } 33 | } 34 | } 35 | 36 | MD.Divider {} 37 | 38 | QA.SettingSection { 39 | Layout.fillWidth: true 40 | spacing: 2 41 | horizontalPadding: 16 42 | QA.SettingRow { 43 | icon.name: MD.Token.icon.palette 44 | text: 'Theme' 45 | supportText: '' 46 | onClicked: { 47 | root.MD.MProp.page.pushItem('qrc:/Qcm/App/qml/page/setting/ThemeSetting.qml'); 48 | } 49 | } 50 | QA.SettingRow { 51 | icon.name: MD.Token.icon.media_output 52 | text: 'Audio' 53 | supportText: '' 54 | onClicked: { 55 | root.MD.MProp.page.pushItem('qrc:/Qcm/App/qml/page/setting/AudioSetting.qml'); 56 | } 57 | } 58 | QA.SettingRow { 59 | icon.name: MD.Token.icon.router 60 | text: 'Network' 61 | supportText: '' 62 | onClicked: 63 | // root.MD.MProp.page.pushItem('qrc:/Qcm/App/qml/page/setting/NetworkSetting.qml'); 64 | {} 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/qml/page/toplevel/LoadingPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import Qcm.Material as MD 3 | 4 | MD.Page { 5 | Column { 6 | anchors.centerIn: parent 7 | spacing: 12 8 | MD.IconSvg { 9 | anchors.horizontalCenter: parent.horizontalCenter 10 | source: 'qrc:/Qcm/App/assets/Qcm.svg' 11 | sourceSize: Qt.size(width, height) 12 | width: 96 13 | height: 96 14 | } 15 | MD.CircularIndicator { 16 | anchors.horizontalCenter: parent.horizontalCenter 17 | running: visible 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/qml/page/toplevel/RetryPage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import Qcm.App as QA 4 | import Qcm.Material as MD 5 | 6 | MD.Page { 7 | id: root 8 | padding: 0 9 | 10 | property alias text: item_text.text 11 | property var retryCallback: function () {} 12 | 13 | ColumnLayout { 14 | anchors.left: parent.left 15 | anchors.right: parent.right 16 | anchors.verticalCenter: parent.verticalCenter 17 | 18 | MD.Label { 19 | id: item_text 20 | Layout.alignment: Qt.AlignHCenter 21 | MD.MProp.textColor: MD.Token.color.error 22 | maximumLineCount: 10 23 | font.capitalization: Font.MixedCase 24 | typescale: MD.Token.typescale.label_large 25 | } 26 | 27 | MD.Button { 28 | Layout.alignment: Qt.AlignHCenter 29 | type: MD.Enum.BtText 30 | action:MD.Action { 31 | icon.name: MD.Token.icon.refresh 32 | text: 'retry' 33 | onTriggered: root.retryCallback() 34 | } 35 | } 36 | } 37 | 38 | MD.IconButton { 39 | anchors.left: parent.left 40 | anchors.bottom: parent.bottom 41 | anchors.leftMargin: 16 42 | anchors.bottomMargin: 16 43 | 44 | action: QA.SettingAction {} 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/qml/page/toplevel/WelcomePage.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Templates as T 4 | 5 | import Qcm.App as QA 6 | import Qcm.Material as MD 7 | 8 | MD.Page { 9 | id: root 10 | showBackground: true 11 | MD.MProp.backgroundColor: MD.MProp.color.surface 12 | 13 | Item { 14 | anchors.fill: parent 15 | implicitWidth: m_stack.implicitWidth 16 | implicitHeight: m_stack.implicitHeight 17 | QA.PageStack { 18 | id: m_stack 19 | anchors.fill: parent 20 | 21 | MD.MProp.page: m_page_ctx 22 | MD.PageContext { 23 | id: m_page_ctx 24 | inherit: root.MD.MProp.page 25 | showHeader: true 26 | leadingAction: MD.Action { 27 | icon.name: MD.Token.icon.arrow_back 28 | onTriggered: { 29 | m_stack.back(); 30 | } 31 | } 32 | 33 | onPushItem: function (comp, props) { 34 | m_stack.pushItem(comp, props, T.StackView.PushTransition); 35 | } 36 | } 37 | 38 | initialItem: QA.AddProviderPage { 39 | showHeader: false 40 | } 41 | } 42 | 43 | MD.IconButton { 44 | anchors.left: parent.left 45 | anchors.bottom: parent.bottom 46 | anchors.leftMargin: 4 47 | anchors.bottomMargin: 4 48 | 49 | action: QA.SettingAction {} 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/image_provider/qr.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/image_provider/qr.hpp" 2 | 3 | #include 4 | 5 | #include "qr_code/qrcodegen.hpp" 6 | 7 | #include "Qcm/app.hpp" 8 | 9 | namespace qcm 10 | { 11 | QrImageProvider::QrImageProvider(): QQuickAsyncImageProvider() {} 12 | 13 | QQuickImageResponse* QrImageProvider::requestImageResponse(const QString& id, 14 | const QSize& requestedSize) { 15 | auto rsp = QrAsyncImageResponse::make_rc(); 16 | 17 | if (id.isEmpty()) { 18 | return rsp.get(); 19 | } 20 | 21 | auto ex = qcm::pool_executor(); 22 | asio::post(ex, [id, requestedSize, rsp]() { 23 | auto bs = id.toUtf8(); 24 | up qr_; 25 | try { 26 | qr_ = std::make_unique(qrcodegen::QrCode::encodeBinary( 27 | std::vector { bs.begin(), bs.end() }, qrcodegen::QrCode::Ecc::MEDIUM)); 28 | } catch (const std::exception& e) { 29 | rsp->set_error(std::format("{} ({})", e.what(), id)); 30 | return; 31 | } 32 | auto& qr = *qr_; 33 | // 创建二维码画布 34 | QImage qr_img = QImage(qr.getSize(), qr.getSize(), QImage::Format_RGB888); 35 | 36 | for (int y = 0; y < qr.getSize(); y++) { 37 | for (int x = 0; x < qr.getSize(); x++) { 38 | if (qr.getModule(x, y) == 0) 39 | qr_img.setPixel(x, y, qRgb(255, 255, 255)); 40 | else 41 | qr_img.setPixel(x, y, qRgb(0, 0, 0)); 42 | } 43 | } 44 | if (requestedSize.isValid()) qr_img = qr_img.scaled(requestedSize); 45 | rsp->image = qr_img; 46 | }); 47 | 48 | return rsp.get(); 49 | } 50 | 51 | } // namespace qcm -------------------------------------------------------------------------------- /app/src/image_provider/response.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/image_provider/response.hpp" 2 | 3 | #include 4 | 5 | namespace qcm 6 | { 7 | 8 | auto image_response_count() -> std::atomic& { 9 | static std::atomic count { 0 }; 10 | return count; 11 | } 12 | 13 | QcmImageResponse::QcmImageResponse() { image_response_count()++; } 14 | QcmImageResponse::~QcmImageResponse() { image_response_count()--; } 15 | auto QcmImageResponse::errorString() const -> QString { return m_error; } 16 | void QcmImageResponse::set_error(QAnyStringView error) { m_error = error.toString(); } 17 | void QcmImageResponse::done() { QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection); } 18 | void QcmImageResponse::rc_deleter(QcmImageResponse* p) { p->done(); } 19 | 20 | } // namespace qcm -------------------------------------------------------------------------------- /app/src/log.cpp: -------------------------------------------------------------------------------- 1 | module; 2 | #include "core/log.h" 3 | module qcm.log; 4 | 5 | QCM_LOG_IMPL 6 | -------------------------------------------------------------------------------- /app/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "Qcm/app.hpp" 10 | #include "core/log.h" 11 | import platform; 12 | 13 | #include 14 | Q_IMPORT_QML_PLUGIN(Qcm_AppPlugin) 15 | 16 | #include 17 | 18 | int main(int argc, char* argv[]) { 19 | plt::malloc_init(); 20 | auto logger = qcm::LogManager::instance(); 21 | ncrequest::global_init(); 22 | 23 | // set by qml_material 24 | // qputenv("QT_QUICK_FLICKABLE_WHEEL_DECELERATION", "5000"); 25 | // qputenv("QSGCURVEGLYPHATLAS_FONT_SIZE", "64"); 26 | 27 | QGuiApplication gui_app(argc, argv); 28 | auto main_qthread = gui_app.thread(); 29 | QString backend_exe; 30 | 31 | QCoreApplication::setApplicationName(APP_NAME); 32 | QCoreApplication::setApplicationVersion(APP_VERSION); 33 | 34 | { 35 | QCommandLineParser parser; 36 | parser.addHelpOption(); 37 | parser.addVersionOption(); 38 | 39 | QCommandLineOption log_level_opt( 40 | "log-level", "Log Level (debug, info, warn, error)", "level", "warn"); 41 | QCommandLineOption backend_opt( 42 | { "b", "backend" }, "backend executable path", "path", "QcmBackend"); 43 | 44 | parser.addOption(log_level_opt); 45 | parser.addOption(backend_opt); 46 | parser.process(gui_app); 47 | logger->set_level(qcm::log::level_from(parser.value(log_level_opt).toStdString())); 48 | 49 | backend_exe = parser.value(backend_opt); 50 | { 51 | auto path = QDir(backend_exe); 52 | if (! path.isAbsolute()) { 53 | backend_exe = QDir(QCoreApplication::applicationDirPath()).filePath(backend_exe); 54 | } 55 | } 56 | QLoggingCategory::setFilterRules( 57 | QLatin1String(std::format("qcm.debug={}", logger->level() == qcm::LogLevel::DEBUG))); 58 | } 59 | 60 | KDSingleApplication single; 61 | if (! single.isPrimaryInstance()) { 62 | WARN_LOG("another qcm running, triggering"); 63 | single.sendMessageWithTimeout("hello", 5); 64 | exit(0); 65 | } 66 | 67 | int re { 0 }; 68 | { 69 | qcm::App app { backend_exe, {} }; 70 | QObject::connect(&single, &KDSingleApplication::messageReceived, app.instance(), []() { 71 | emit qcm::App::instance() -> instanceStarted(); 72 | }); 73 | app.init(); 74 | 75 | re = gui_app.exec(); 76 | main_qthread->setProperty("exec", false); 77 | } 78 | 79 | return re; 80 | } 81 | -------------------------------------------------------------------------------- /app/src/mod.cppm: -------------------------------------------------------------------------------- 1 | export module qcm; 2 | 3 | namespace qcm 4 | { 5 | } -------------------------------------------------------------------------------- /app/src/model/app_info.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/model/app_info.hpp" 2 | 3 | namespace qcm::model 4 | { 5 | AppInfo::AppInfo() { 6 | this->name = APP_NAME; 7 | this->id = APP_ID; 8 | this->author = APP_AUTHOR; 9 | this->summary = APP_SUMMARY; 10 | this->version = APP_VERSION; 11 | } 12 | AppInfo::~AppInfo() {} 13 | } // namespace qcm::model 14 | 15 | #include -------------------------------------------------------------------------------- /app/src/model/empty_model.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/model/empty_model.hpp" 2 | 3 | #include 4 | -------------------------------------------------------------------------------- /app/src/model/list_models.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/model/list_models.hpp" 2 | #include "Qcm/model/store_item.hpp" 3 | #include "Qcm/store.hpp" 4 | 5 | #include "Qcm/util/mem.hpp" 6 | 7 | namespace qcm::model 8 | { 9 | AlbumListModel::AlbumListModel(QObject* parent): base_type(parent, { mem_mgr().store_mem }) {} 10 | QQmlPropertyMap* AlbumListModel::extra(i32 idx) const { 11 | if (auto extend = AppStore::instance()->albums.query_extend(this->key_at(idx)); extend) { 12 | return extend->extra.get(); 13 | } 14 | return nullptr; 15 | } 16 | SongListModel::SongListModel(QObject* parent): base_type(parent, { mem_mgr().store_mem }) {} 17 | QQmlPropertyMap* SongListModel::extra(i32 idx) const { 18 | if (auto extend = AppStore::instance()->songs.query_extend(this->key_at(idx)); extend) { 19 | return extend->extra.get(); 20 | } 21 | return nullptr; 22 | } 23 | 24 | AlbumSongListModel::AlbumSongListModel(QObject* parent) 25 | : base_type(parent, { mem_mgr().store_mem }) { 26 | connect(&m_item, &model::AlbumStoreItem::itemChanged, this, &AlbumSongListModel::albumChanged); 27 | } 28 | AlbumSongListModel::~AlbumSongListModel() {} 29 | auto AlbumSongListModel::album() const -> album_type { return m_item.item(); } 30 | void AlbumSongListModel::setAlbum(const album_type& album) { m_item.setItem(album); } 31 | auto AlbumSongListModel::extra() const -> QQmlPropertyMap* { return m_item.extra(); } 32 | 33 | ArtistListModel::ArtistListModel(QObject* parent): base_type(parent, { mem_mgr().store_mem }) {} 34 | 35 | MixListModel::MixListModel(QObject* parent): base_type(parent) {} 36 | } // namespace qcm::model 37 | 38 | #include -------------------------------------------------------------------------------- /app/src/model/page_model.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/model/page_model.hpp" 2 | 3 | namespace qcm 4 | { 5 | 6 | void PageModel::init_main_pages(PageModel* self) { 7 | std::array arr { Page { .name = "library", 8 | .icon = "library_music", 9 | .source = "qrc:/Qcm/App/qml/page/LibraryPage.qml", 10 | .cache = true }, 11 | Page { .name = "search", 12 | .icon = "search", 13 | .source = "qrc:/Qcm/App/qml/page/SearchPage.qml" } }; 14 | self->insert(0, arr); 15 | #ifndef NDEBUG 16 | self->insert(self->rowCount(), 17 | Page { .name = "test", 18 | .icon = "bug_report", 19 | .source = "qrc:/Qcm/Material/Example/Example.qml" }); 20 | #endif 21 | } 22 | PageModel::PageModel(QObject* parent): base_type(parent) {} 23 | PageModel::~PageModel() {} 24 | } // namespace qcm 25 | 26 | #include -------------------------------------------------------------------------------- /app/src/model/router_msg.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/model/router_msg.hpp" 2 | 3 | #include 4 | -------------------------------------------------------------------------------- /app/src/model/store_item.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/model/store_item.hpp" 2 | 3 | #include "Qcm/store.hpp" 4 | 5 | namespace qcm::model 6 | { 7 | 8 | SongStoreItem::SongStoreItem(QObject* parent): base_type(AppStore::instance()->songs, parent) {} 9 | AlbumStoreItem::AlbumStoreItem(QObject* parent): base_type(AppStore::instance()->albums, parent) {} 10 | ArtistStoreItem::ArtistStoreItem(QObject* parent) 11 | : base_type(AppStore::instance()->artists, parent) {} 12 | MixStoreItem::MixStoreItem(QObject* parent): base_type(AppStore::instance()->mixes, parent) {} 13 | 14 | } // namespace qcm::model 15 | 16 | #include -------------------------------------------------------------------------------- /app/src/notifier.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/notifier.hpp" 2 | #include "Qcm/global.hpp" 3 | 4 | #include "Qcm/util/global_static.hpp" 5 | namespace qcm 6 | { 7 | 8 | auto Notifier::instance() -> Notifier* { 9 | static auto the = 10 | GlobalStatic::instance()->add("notifier", new Notifier(nullptr), [](Notifier* p) { 11 | delete p; 12 | }); 13 | return the; 14 | }; 15 | 16 | Notifier::Notifier(QObject* parent): QObject(parent) {} 17 | Notifier::~Notifier() {} 18 | 19 | Notifier* Notifier::create(QQmlEngine*, QJSEngine*) { 20 | auto act = instance(); 21 | // not delete on qml 22 | QJSEngine::setObjectOwnership(act, QJSEngine::CppOwnership); 23 | return act; 24 | } 25 | } // namespace qcm 26 | 27 | 28 | #include -------------------------------------------------------------------------------- /app/src/qml/clipboard.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/qml/clipboard.hpp" 2 | #include 3 | 4 | namespace qcm 5 | { 6 | Clipboard::Clipboard(QObject* parent): QObject(parent), m_mode(QClipboard::Mode::Clipboard) {} 7 | 8 | QString Clipboard::text() const { return QGuiApplication::clipboard()->text(m_mode); } 9 | void Clipboard::setText(const QString& data) { 10 | QGuiApplication::clipboard()->setText(data, m_mode); 11 | } 12 | 13 | QClipboard::Mode Clipboard::mode() const { return m_mode; } 14 | void Clipboard::setMode(QClipboard::Mode m) { 15 | if (std::exchange(m_mode, m) != m) { 16 | emit modeChanged(); 17 | } 18 | } 19 | 20 | void Clipboard::clear() { QGuiApplication::clipboard()->clear(m_mode); } 21 | } // namespace qcm 22 | 23 | #include -------------------------------------------------------------------------------- /app/src/qml/duration.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/qml/duration.hpp" 2 | 3 | #include -------------------------------------------------------------------------------- /app/src/qml/enum.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/qml/enum.hpp" 2 | #include "core/helper.h" 3 | #include "core/log.h" 4 | 5 | namespace qcm 6 | { 7 | } 8 | 9 | // IMPL_CONVERT(std::string_view, qcm::enums::CollectionType) { 10 | // switch (in) { 11 | // case in_type::CTAlbum: { 12 | // out = "album"sv; 13 | // break; 14 | // } 15 | // case in_type::CTArtist: { 16 | // out = "artist"sv; 17 | // break; 18 | // } 19 | // case in_type::CTPlaylist: { 20 | // out = "playlist"sv; 21 | // break; 22 | // } 23 | // case in_type::CTRadio: { 24 | // out = "djradio"sv; 25 | // break; 26 | // } 27 | // default: { 28 | // _assert_rel_(false); 29 | // } 30 | // } 31 | // } 32 | 33 | #include -------------------------------------------------------------------------------- /app/src/query/lyric_query.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/query/lyric_query.hpp" 2 | #include "Qcm/app.hpp" 3 | #include "Qcm/backend.hpp" 4 | #include "Qcm/util/async.inl" 5 | 6 | namespace qcm 7 | { 8 | LyricQuery::LyricQuery(QObject* parent): Query(parent) { 9 | connect_requet_reload(&LyricQuery::itemIdChanged); 10 | } 11 | void LyricQuery::reload() { 12 | set_status(Status::Querying); 13 | auto app = App::instance(); 14 | auto backend = app->backend(); 15 | auto req = msg::GetSubtitleReq {}; 16 | 17 | if (m_item_id.valid()) { 18 | req.setSongId(m_item_id.id()); 19 | 20 | auto self = helper::QWatcher { this }; 21 | spawn([self, backend, req] mutable -> task { 22 | auto rsp = co_await backend->send(std::move(req)); 23 | co_await qcm::qexecutor_switch(); 24 | auto t = self->tdata(); 25 | if (rsp) { 26 | msg::GetSubtitleRsp& el = *rsp; 27 | auto view = std::views::transform(el.subtitle().items(), [](auto& el) { 28 | return LyricItem { .milliseconds = el.start(), .content = el.text() }; 29 | }); 30 | t->setCurrentIndex(-1); 31 | t->resetModel(view); 32 | t->setCurrentIndex(-1); 33 | } else { 34 | t->resetModel(); 35 | t->setCurrentIndex(-1); 36 | } 37 | self->set_status(Status::Finished); 38 | co_return; 39 | }); 40 | } else { 41 | cancel(); 42 | auto t = this->tdata(); 43 | t->resetModel(); 44 | t->setCurrentIndex(-1); 45 | set_status(Status::Finished); 46 | } 47 | } 48 | 49 | auto LyricQuery::itemId() const -> model::ItemId { return m_item_id; } 50 | void LyricQuery::setItemId(model::ItemId id) { 51 | if (ycore::cmp_exchange(m_item_id, id)) { 52 | itemIdChanged(); 53 | } 54 | } 55 | } // namespace qcm 56 | 57 | #include "Qcm/query/moc_lyric_query.cpp" -------------------------------------------------------------------------------- /app/src/query/qr_query.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/query/qr_query.hpp" 2 | #include "Qcm/app.hpp" 3 | #include "Qcm/util/async.inl" 4 | 5 | namespace qcm 6 | { 7 | QrAuthUrlQuery::QrAuthUrlQuery(QObject* parent): Query(parent) {} 8 | void QrAuthUrlQuery::reload() { 9 | set_status(Status::Querying); 10 | auto backend = App::instance()->backend(); 11 | auto req = msg::QrAuthUrlReq {}; 12 | req.setTmpProvider(m_tmp_provider); 13 | auto self = helper::QWatcher { this }; 14 | spawn([self, backend, req] mutable -> task { 15 | auto rsp = co_await backend->send(std::move(req)); 16 | co_await qcm::qexecutor_switch(); 17 | // ignore error 18 | if (rsp) { 19 | self->set(std::move(rsp)); 20 | } 21 | co_return; 22 | }); 23 | } 24 | 25 | auto QrAuthUrlQuery::tmpProvider() const -> QString { return m_tmp_provider; } 26 | void QrAuthUrlQuery::setTmpProvider(const QString& v) { 27 | if (ycore::cmp_exchange(m_tmp_provider, v)) { 28 | tmpProviderChanged(); 29 | } 30 | } 31 | } // namespace qcm 32 | 33 | #include -------------------------------------------------------------------------------- /app/src/query/storage_info.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/query/storage_info.hpp" 2 | #include "Qcm/util/async.inl" 3 | #include "Qcm/global.hpp" 4 | #include "Qcm/app.hpp" 5 | 6 | #include "core/asio/basic.h" 7 | 8 | namespace qcm::qml 9 | { 10 | 11 | StorageInfo::StorageInfo(QObject* parent): QObject(parent) {} 12 | auto StorageInfo::total() const -> double { return m_total; } 13 | void StorageInfo::setTotal(double v) { 14 | if (ycore::cmp_exchange(m_total, v)) { 15 | totalChanged(); 16 | } 17 | } 18 | StorageInfoQuerier::StorageInfoQuerier(QObject* parent): Query(parent) {} 19 | void StorageInfoQuerier::reload() { 20 | // set_status(Status::Querying); 21 | // auto media_cache_sql = App::instance()->media_cache_sql(); 22 | // auto cache_sql = App::instance()->cache_sql(); 23 | // this->spawn([media_cache_sql, cache_sql, this]() -> asio::awaitable { 24 | // auto media_size = co_await media_cache_sql->total_size(); 25 | // auto normal_size = co_await cache_sql->total_size(); 26 | 27 | // co_await asio::post( 28 | // asio::bind_executor(qcm::qexecutor(), asio::use_awaitable)); 29 | 30 | // auto d = data().value(); 31 | // d->setTotal(media_size + normal_size); 32 | // set_status(Status::Finished); 33 | // }); 34 | } 35 | } // namespace qcm::qml 36 | 37 | #include -------------------------------------------------------------------------------- /app/src/query/sync_query.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/query/sync_query.hpp" 2 | #include "Qcm/backend.hpp" 3 | #include "Qcm/app.hpp" 4 | #include "Qcm/util/async.inl" 5 | 6 | namespace qcm 7 | { 8 | SyncQuery::SyncQuery(QObject* parent): Query(parent) {} 9 | void SyncQuery::reload() { 10 | set_status(Status::Querying); 11 | auto backend = App::instance()->backend(); 12 | auto req = msg::SyncReq {}; 13 | req.setProviderId(m_provider_id.id()); 14 | auto self = helper::QWatcher { this }; 15 | spawn([self, backend, req] mutable -> task { 16 | auto rsp = co_await backend->send(std::move(req)); 17 | co_await qcm::qexecutor_switch(); 18 | self->inspect_set(rsp, [self](msg::SyncRsp& el) { 19 | }); 20 | co_return; 21 | }); 22 | } 23 | auto SyncQuery::providerId() const -> model::ItemId { return m_provider_id; } 24 | void SyncQuery::setProviderId(const model::ItemId& v) { 25 | m_provider_id = v; 26 | providerIdChanged(); 27 | } 28 | 29 | } // namespace qcm 30 | 31 | #include -------------------------------------------------------------------------------- /app/src/status/app_state.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/status/app_state.hpp" 2 | #include "core/helper.h" 3 | 4 | namespace qcm 5 | { 6 | AppState::AppState(QObject* parent): QObject(parent), m_state(Loading {}) { 7 | connect(this, &AppState::retry, this, &AppState::on_retry); 8 | } 9 | AppState::~AppState() {} 10 | 11 | auto AppState::state() const -> const State& { return m_state; } 12 | void AppState::set_state(const State& v) { 13 | if (v == m_state) { 14 | if (auto e = std::get_if(&m_state)) { 15 | e->err.append("\n"); 16 | e->err.append(std::get_if(&v)->err); 17 | stateChanged(); 18 | triggerSignal(); 19 | } 20 | return; 21 | } 22 | m_state = v; 23 | stateChanged(); 24 | triggerSignal(); 25 | } 26 | 27 | void AppState::triggerSignal() { 28 | std::visit(overloaded { [this](const Loading& s) { 29 | this->loading(); 30 | }, 31 | [this](const Welcome& s) { 32 | this->welcome(); 33 | }, 34 | [this](const Main& s) { 35 | this->main(); 36 | }, 37 | [this](const Error& e) { 38 | this->error(e.err); 39 | } }, 40 | m_state); 41 | } 42 | 43 | void AppState::onQmlCompleted() { 44 | if (! is_state()) { 45 | triggerSignal(); 46 | } 47 | } 48 | 49 | void AppState::on_retry() { 50 | this->set_state(Loading {}); 51 | 52 | QMetaObject::invokeMethod( 53 | this, 54 | [this] { 55 | disconnect(this, &AppState::retry, nullptr, nullptr); 56 | connect(this, &AppState::retry, this, &AppState::on_retry); 57 | }, 58 | Qt::QueuedConnection); 59 | } 60 | }; // namespace qcm 61 | 62 | #include -------------------------------------------------------------------------------- /app/src/status/process.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/status/process.hpp" 2 | #include "core/asio/basic.h" 3 | #include "Qcm/util/ex.hpp" 4 | #include "Qcm/global.hpp" 5 | #include "Qcm/app.hpp" 6 | #include "Qcm/status/provider_status.hpp" 7 | 8 | void qcm::process_msg(msg::QcmMessage&& msg) { 9 | using M = msg::MessageTypeGadget::MessageType; 10 | switch (msg.type()) { 11 | case M::PROVIDER_META_STATUS_MSG: { 12 | asio::post(qcm::qexecutor(), [msg = std::move(msg)] { 13 | auto p = App::instance()->provider_meta_status(); 14 | p->sync(msg.providerMetaStatusMsg().metas()); 15 | }); 16 | break; 17 | } 18 | case M::PROVIDER_STATUS_MSG: { 19 | asio::post(qcm::qexecutor(), [msg = std::move(msg)] { 20 | auto p = App::instance()->provider_status(); 21 | for (auto& s : msg.providerStatusMsg().statuses()) { 22 | log::info("{}", s.name()); 23 | } 24 | auto view = std::views::transform(msg.providerStatusMsg().statuses(), [](auto&& el) { 25 | return qcm::model::ProviderStatus { el }; 26 | }); 27 | auto size = view.size(); 28 | p->sync(view); 29 | 30 | auto state = App::instance()->app_state(); 31 | if (! state->is_state()) { 32 | if (size == 0) { 33 | state->set_state(AppState::Welcome {}); 34 | } else { 35 | state->set_state(AppState::Main {}); 36 | } 37 | } 38 | }); 39 | break; 40 | } 41 | case M::PROVIDER_SYNC_STATUS_MSG: { 42 | asio::post(qcm::qexecutor(), [msg = std::move(msg)] { 43 | auto p = App::instance()->provider_status(); 44 | p->updateSyncStatus(msg.providerSyncStatusMsg().status()); 45 | }); 46 | break; 47 | } 48 | default: { 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/store.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/store.hpp" 2 | #include "Qcm/util/global_static.hpp" 3 | namespace qcm 4 | { 5 | AppStore::AppStore(QObject* parent): QObject(parent) {} 6 | AppStore::~AppStore() {} 7 | auto AppStore::instance() -> AppStore* { 8 | static auto the = 9 | GlobalStatic::instance()->add("store", new AppStore(nullptr), [](AppStore* p) { 10 | delete p; 11 | }); 12 | return the; 13 | } 14 | AppStore* AppStore::create(QQmlEngine*, QJSEngine*) { 15 | auto self = instance(); 16 | // not delete on qml 17 | QJSEngine::setObjectOwnership(self, QJSEngine::CppOwnership); 18 | return self; 19 | } 20 | 21 | auto AppStore::extra(model::ItemId item_id) const -> QQmlPropertyMap* { 22 | using ItemType = enums::ItemType; 23 | auto id = item_id.id(); 24 | switch (item_id.type()) { 25 | case ItemType::ItemAlbum: { 26 | if (auto extend = albums.query_extend(id)) { 27 | return extend->extra.get(); 28 | } 29 | break; 30 | } 31 | case ItemType::ItemSong: { 32 | if (auto extend = songs.query_extend(id)) { 33 | return extend->extra.get(); 34 | } 35 | break; 36 | } 37 | case ItemType::ItemArtist: { 38 | if (auto extend = artists.query_extend(id)) { 39 | return extend->extra.get(); 40 | } 41 | break; 42 | } 43 | default: { 44 | break; 45 | } 46 | } 47 | return nullptr; 48 | } 49 | 50 | const std::set model::AlbumJsonFields { u"artists" }; 51 | const std::set model::ArtistJsonFields {}; 52 | const std::set model::MixJsonFields {}; 53 | const std::set model::SongJsonFields { u"artists", u"album" }; 54 | 55 | } // namespace qcm 56 | 57 | #include -------------------------------------------------------------------------------- /app/src/util/QmlStackTraceHelper.cpp: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | ** MIT License 3 | ** 4 | ** Copyright (C) 2018-2023 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com 5 | ** Author: Sérgio Martins 6 | ** 7 | ** This file is part of KDToolBox (https://github.com/KDAB/KDToolBox). 8 | ** 9 | ** Permission is hereby granted, free of charge, to any person obtaining a copy 10 | ** of this software and associated documentation files (the "Software"), to deal 11 | ** in the Software without restriction, including without limitation the rights 12 | ** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | ** copies of the Software, ** and to permit persons to whom the Software is 14 | ** furnished to do so, subject to the following conditions: 15 | ** 16 | ** The above copyright notice and this permission notice (including the next paragraph) 17 | ** shall be included in all copies or substantial portions of the Software. 18 | ** 19 | ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | ** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | ** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | ** LIABILITY, WHETHER IN AN ACTION OF ** CONTRACT, TORT OR OTHERWISE, 24 | ** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | ** DEALINGS IN THE SOFTWARE. 26 | ****************************************************************************/ 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | extern "C" char *qt_v4StackTrace(void *executionContext); 37 | 38 | namespace KDAB 39 | { 40 | 41 | QString qmlStackTrace(QV4::ExecutionEngine *engine) 42 | { 43 | return QString::fromUtf8(qt_v4StackTrace(engine->currentContext())); 44 | } 45 | 46 | void printQmlStackTraces() 47 | { 48 | const auto windows = qApp->topLevelWindows(); 49 | for (QWindow *w : windows) 50 | { 51 | if (auto qw = qobject_cast(w)) 52 | { 53 | QQuickItem *item = qw->contentItem(); 54 | QQmlContext *context = QQmlEngine::contextForObject(item); 55 | if (!context) 56 | continue; 57 | QQmlEnginePrivate *enginePriv = QQmlEnginePrivate::get(context->engine()); 58 | QV4::ExecutionEngine *v4engine = enginePriv->v4engine(); 59 | qDebug() << "Stack trace for" << qw; 60 | qDebug().noquote() << qmlStackTrace(v4engine); 61 | qDebug() << "\n"; 62 | } 63 | } 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /app/src/util/global_static.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/util/global_static.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "core/log.h" 9 | 10 | namespace qcm 11 | { 12 | struct GlobalStatic::HolderImpl { 13 | std::atomic instance { nullptr }; 14 | std::function deleter; 15 | 16 | void reset() { 17 | if (deleter && instance) { 18 | deleter(instance); 19 | deleter = {}; 20 | instance = nullptr; 21 | } 22 | } 23 | 24 | HolderImpl() {} 25 | ~HolderImpl() { reset(); } 26 | }; 27 | 28 | class GlobalStatic::Private { 29 | public: 30 | std::map, std::less<>> instances; 31 | }; 32 | 33 | GlobalStatic::GlobalStatic(): d_ptr(make_up()) {} 34 | GlobalStatic::~GlobalStatic() { reset(); } 35 | auto GlobalStatic::instance() -> GlobalStatic* { 36 | static GlobalStatic theGlobalStatic; 37 | return &theGlobalStatic; 38 | } 39 | 40 | auto GlobalStatic::data(HolderImpl& holder) -> voidp { 41 | _assert_rel_(holder.instance); 42 | return holder.instance; 43 | } 44 | 45 | auto GlobalStatic::add_impl(std::string_view name, voidp instance, 46 | std::function deleter) -> rc { 47 | C_D(GlobalStatic); 48 | auto holder = make_rc(); 49 | holder->instance = instance; 50 | holder->deleter = deleter; 51 | d->instances.insert({ std::string(name), holder }); 52 | return holder; 53 | } 54 | void GlobalStatic::reset() { 55 | C_D(GlobalStatic); 56 | for (auto& el : d->instances) { 57 | el.second->reset(); 58 | } 59 | d->instances.clear(); 60 | } 61 | } // namespace qcm -------------------------------------------------------------------------------- /app/src/util/path.cpp: -------------------------------------------------------------------------------- 1 | #include "Qcm/util/path.hpp" 2 | #include "core/log.h" 3 | #include "core/qstr_helper.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | using path = std::filesystem::path; 10 | 11 | std::filesystem::path qcm::config_path() { 12 | auto locs = QStandardPaths::standardLocations(QStandardPaths::AppConfigLocation); 13 | _assert_(locs.size() > 0); 14 | return std::filesystem::path(rstd::to_string(locs[0])); 15 | } 16 | 17 | std::filesystem::path qcm::data_path() { 18 | auto locs = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); 19 | _assert_(locs.size() > 0); 20 | return std::filesystem::path(rstd::to_string(locs[0])); 21 | } 22 | 23 | std::filesystem::path qcm::cache_path() { 24 | auto locs = QStandardPaths::standardLocations(QStandardPaths::CacheLocation); 25 | _assert_(locs.size() > 0); 26 | return std::filesystem::path(rstd::to_string(locs[0])); 27 | } 28 | 29 | bool qcm::init_path(std::span pathes) { 30 | for (auto& p : pathes) { 31 | std::error_code ec; 32 | std::filesystem::create_directories(p, ec); 33 | _assert_msg_(! ec, "path: {}, info: {}({})", p.string(), ec.message(), ec.value()); 34 | } 35 | return true; 36 | } 37 | -------------------------------------------------------------------------------- /app/static.cpp.in: -------------------------------------------------------------------------------- 1 | #include 2 | @IMPORT_PLUGINS@ -------------------------------------------------------------------------------- /conanfile.txt: -------------------------------------------------------------------------------- 1 | [requires] 2 | openssl/[~3] 3 | libcurl/[~8] 4 | ffmpeg/[~7] 5 | 6 | [options] 7 | openssl*:shared=True 8 | libcurl*:shared=True 9 | ffmpeg*:shared=True 10 | ffmpeg*:swscale=False 11 | ffmpeg*:avdevice=False 12 | ffmpeg*:with_freetype=False 13 | ffmpeg*:with_openh264=False 14 | ffmpeg*:with_asm=False 15 | ffmpeg*:with_bzip2=False 16 | ffmpeg*:with_lzma=False 17 | ffmpeg*:with_libiconv=False 18 | ffmpeg*:with_vorbis=False 19 | ffmpeg*:with_libx264=False 20 | ffmpeg*:with_libx265=False 21 | ffmpeg*:with_libsvtav1=False 22 | ffmpeg*:with_libaom=False 23 | ffmpeg*:with_libdav1d=False 24 | ffmpeg*:with_libdrm=False 25 | ffmpeg*:with_programs=False 26 | ffmpeg*:with_videotoolbox=False 27 | ffmpeg*:with_audiotoolbox=False 28 | ffmpeg*:disable_all_filters=True 29 | ffmpeg*:disable_all_encoders=True 30 | ffmpeg*:disable_all_decoders=True 31 | ffmpeg*:disable_all_protocols=True 32 | ffmpeg*:disable_all_parsers=True 33 | ffmpeg*:disable_all_demuxers=True 34 | ffmpeg*:disable_all_muxers=True 35 | ffmpeg*:disable_all_output_devices=True 36 | ffmpeg*:disable_all_hardware_accelerators=True 37 | ffmpeg*:disable_all_bitstream_filters=True 38 | ffmpeg*:disable_all_devices=True 39 | ffmpeg*:disable_all_input_devices=True 40 | ffmpeg*:disable_all_output_devices=True 41 | ffmpeg*:enable_protocols=file,http 42 | ffmpeg*:enable_filters=aformat,anull,atrim,format,null,setpts,trim 43 | ffmpeg*:enable_parsers=aac,aac_latm,ac3,cook,dca,flac,gsm,mpegaudio,tak 44 | ffmpeg*:enable_demuxers=image2,aac,ac3,aiff,ape,asf,au,avi,flac,flv,matroska,mov,m4v,mp3,mpc,mpc8,ogg,pcm_alaw,pcm_mulaw,pcm_f64be,pcm_f64le,pcm_f32be,pcm_f32le,pcm_s32be,pcm_s32le,pcm_s24be,pcm_s24le,pcm_s16be,pcm_s16le,pcm_s8,pcm_u32be,pcm_u32le,pcm_u24be,pcm_u24le,pcm_u16be,pcm_u16le,pcm_u8,rm,shorten,tak,tta,wav,wv,xwma,dsf 45 | ffmpeg*:enable_decoders=aac,aac_latm,ac3,alac,als,ape,atrac1,atrac3,eac3,flac,gsm,gsm_ms,mp1,mp1float,mp2,mp2float,mp3,mp3adu,mp3adufloat,mp3float,mp3on4,mp3on4float,mpc7,mpc8,opus,ra_144,ra_288,ralf,shorten,tak,tta,wavpack,wmalossless,wmapro,wmav1,wmav2,wmavoice,pcm_alaw,pcm_bluray,pcm_dvd,pcm_f32be,pcm_f32le,pcm_f64be,pcm_f64le,pcm_lxf,pcm_mulaw,pcm_s8,pcm_s8_planar,pcm_s16be,pcm_s16be_planar,pcm_s16le,pcm_s16le_planar,pcm_s24be,pcm_s24daud,pcm_s24le,pcm_s24le_planar,pcm_s32be,pcm_s32le,pcm_s32le_planar,pcm_u8,pcm_u16be,pcm_u16le,pcm_u24be,pcm_u24le,pcm_u32be,pcm_u32le,pcm_zork,dsd_lsbf,dsd_msbf,dsd_lsbf_planar,dsd_msbf_planar 46 | 47 | 48 | [generators] 49 | CMakeDeps 50 | CMakeToolchain 51 | 52 | [layout] 53 | cmake_layout 54 | -------------------------------------------------------------------------------- /core/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(core STATIC) 2 | 3 | target_sources( 4 | core 5 | PUBLIC FILE_SET 6 | all 7 | TYPE 8 | CXX_MODULES 9 | BASE_DIRS 10 | src 11 | FILES 12 | src/mod.cppm 13 | src/random.cppm 14 | src/type_list.cppm 15 | src/log.cppm 16 | src/core.cppm 17 | src/mem.cppm 18 | src/lambda.cppm 19 | src/fmt.cppm 20 | src/helper/mod.cppm 21 | src/helper/str.cppm 22 | src/helper/container.cppm) 23 | target_sources( 24 | core 25 | PRIVATE include/core/core.h include/core/callable.h 26 | include/core/expected_helper.h include/core/log.h 27 | include/core/clangd.h src/log.cpp src/mem.cpp) 28 | 29 | target_compile_definitions( 30 | core 31 | PUBLIC ${WIN_DEFS}) 32 | target_compile_features(core PUBLIC cxx_std_23) 33 | target_include_directories(core PUBLIC include) 34 | target_link_libraries( 35 | core 36 | PUBLIC random rstd::rstd 37 | PRIVATE platform) 38 | 39 | # core.asio 40 | add_library(core.asio STATIC src/asio/asio.cpp) 41 | target_compile_definitions( 42 | core.asio 43 | PUBLIC ASIO_DISABLE_VISIBILITY 44 | PRIVATE QCM_ASIO_EXPORT) 45 | target_link_libraries(core.asio PUBLIC core asio) 46 | set_target_properties(core.asio PROPERTIES CXX_VISIBILITY_PRESET hidden 47 | OUTPUT_NAME qcm_asio) 48 | 49 | # core.qasio 50 | find_package(Qt6 REQUIRED COMPONENTS Core) 51 | add_library(core.qasio STATIC src/qasio/qt_execution_context.cpp 52 | src/qasio/qt_executor.cpp) 53 | 54 | target_include_directories(core.qasio PRIVATE include/core/qasio) 55 | target_link_libraries( 56 | core.qasio 57 | PUBLIC core.asio Qt6::Core 58 | PRIVATE ncrequest::ncrequest) 59 | -------------------------------------------------------------------------------- /core/include/core/asio/basic.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "core/asio/detached_log.h" 11 | #include "core/asio/task.h" 12 | -------------------------------------------------------------------------------- /core/include/core/asio/detached_log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include "core/asio/export.h" 5 | 6 | namespace helper 7 | { 8 | class asio_detached_log_t { 9 | public: 10 | asio_detached_log_t(const std::source_location = std::source_location::current()); 11 | void operator()(std::exception_ptr); 12 | 13 | std::source_location loc; 14 | }; 15 | 16 | } // namespace helper -------------------------------------------------------------------------------- /core/include/core/asio/error.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "core/asio/export.h" 9 | 10 | namespace helper 11 | { 12 | 13 | void 14 | handle_asio_exception(std::exception_ptr ptr, 15 | asio::any_completion_handler on_error = {}, 16 | const std::source_location loc = std::source_location::current()); 17 | 18 | } -------------------------------------------------------------------------------- /core/include/core/asio/export.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "core/macro.h" 3 | 4 | #if defined(QCM_ASIO_EXPORT) 5 | # define QCM_ASIO_API C_DECL_EXPORT 6 | #else 7 | # define QCM_ASIO_API C_DECL_IMPORT 8 | #endif -------------------------------------------------------------------------------- /core/include/core/asio/helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include "core/core.h" 13 | #include "core/expected_helper.h" 14 | #include "core/helper.h" 15 | 16 | DEFINE_CONVERT(std::vector, asio::streambuf) { 17 | out.clear(); 18 | std::transform(asio::buffers_begin(in.data()), 19 | asio::buffers_end(in.data()), 20 | std::back_inserter(out), 21 | [](unsigned char c) { 22 | return byte { c }; 23 | }); 24 | } 25 | 26 | template<> 27 | struct std::formatter : std::formatter { 28 | auto format(const asio::streambuf& buf, format_context& ctx) const -> format_context::iterator { 29 | std::string out { asio::buffers_begin(buf.data()), asio::buffers_end(buf.data()) }; 30 | return std::formatter::format(out, ctx); 31 | } 32 | }; 33 | 34 | namespace helper 35 | { 36 | template 37 | concept is_awaitable = ycore::is_specialization_of_v; 38 | 39 | template 40 | concept is_sync_stream = requires(T s, asio::const_buffer buf) { 41 | { s.write_some(buf) } -> std::convertible_to; 42 | }; 43 | 44 | template 45 | void post_via(const Ex& exec, const ExWork& work_exec, F&& handler, Args&&... args) { 46 | asio::post(exec, 47 | asio::bind_executor( 48 | work_exec, std::bind(std::forward(handler), std::forward(args)...))); 49 | } 50 | 51 | template 52 | void post(const Ex& exec, F&& handler, Args&&... args) { 53 | post_via(exec, 54 | asio::get_associated_executor(handler, exec), 55 | std::forward(handler), 56 | std::forward(args)...); 57 | } 58 | } // namespace helper -------------------------------------------------------------------------------- /core/include/core/asio/sync_file.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | #include "core/core.h" 8 | 9 | namespace helper 10 | { 11 | 12 | template 13 | class SyncFile : NoCopy { 14 | public: 15 | SyncFile(F&& f): m_f(std::forward(f)) {} 16 | 17 | SyncFile(SyncFile&& o) noexcept: m_f(std::exchange(o.m_f, {})) {} 18 | SyncFile& operator=(SyncFile&& o) noexcept { 19 | m_f = std::exchange(o.m_f, {}); 20 | return *this; 21 | } 22 | 23 | F& handle() { return m_f; } 24 | 25 | template 26 | requires asio::is_mutable_buffer_sequence::value 27 | auto read_some(MB buffer) { 28 | auto size = buffer.size(); 29 | m_f.read((char*)buffer.data(), size); 30 | return size; 31 | } 32 | 33 | template 34 | requires asio::is_const_buffer_sequence::value 35 | auto write_some(const MB& buffer) { 36 | auto size = buffer.size(); 37 | m_f.write((const char*)buffer.data(), size); 38 | return size; 39 | } 40 | 41 | private: 42 | F m_f; 43 | }; 44 | 45 | } // namespace helper 46 | -------------------------------------------------------------------------------- /core/include/core/asio/task.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace qcm 7 | { 8 | template 9 | using task = asio::awaitable; 10 | 11 | template
Material3 cloud music player
Music Service:
Jellyfin(wip)
Netease Cloud Music(网易云音乐)