├── .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 12 | auto as_tuple(CompletionToken&& t) { 13 | return asio::as_tuple(std::forward(t)); 14 | }; 15 | 16 | constexpr asio::use_awaitable_t<> use_task; 17 | } // namespace qcm -------------------------------------------------------------------------------- /core/include/core/asio/watch_dog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "core/core.h" 13 | #include "core/log.h" 14 | 15 | namespace helper 16 | { 17 | 18 | class WatchDog { 19 | public: 20 | WatchDog() {} 21 | ~WatchDog() { cancel(); } 22 | 23 | using clock = asio::steady_timer::clock_type; 24 | using duration = asio::steady_timer::duration; 25 | 26 | bool is_running() const { 27 | if (m_timer) { 28 | return m_timer->expiry() > clock::now(); 29 | } 30 | return false; 31 | } 32 | 33 | template> 34 | requires(! std::same_as>) 35 | auto watch(Ex&& ex, F&& f, const duration& t = asio::chrono::minutes(5), 36 | Allocator&& alloc = {}) -> asio::awaitable { 37 | cancel(); 38 | m_timer = std::make_shared(ex); 39 | m_timer->expires_after(t); 40 | return watch_impl>(m_timer, std::move(f), alloc); 41 | } 42 | 43 | void cancel() { 44 | if (m_timer) { 45 | m_timer->cancel(); 46 | m_timer->expires_after(asio::chrono::seconds(0)); 47 | } 48 | } 49 | 50 | template> 51 | auto spawn(Ex&& ex, F&& f, CT&& ct, const duration& t = asio::chrono::minutes(5), 52 | Allocator&& alloc = {}) { 53 | asio::co_spawn(ex, watch(ex, std::forward(f), t, alloc), std::forward(ct)); 54 | } 55 | 56 | private: 57 | template 58 | static auto watch_impl(rc timer, F f, 59 | Allocator alloc) -> asio::awaitable { 60 | auto ex = co_await asio::this_coro::executor; 61 | auto [order, exp, ec] = 62 | co_await asio::experimental::make_parallel_group( 63 | asio::co_spawn(ex, std::move(f), asio::bind_allocator(alloc, asio::deferred)), 64 | asio::co_spawn(ex, 65 | timer->async_wait(asio::use_awaitable), 66 | asio::bind_allocator(alloc, asio::deferred))) 67 | .async_wait(asio::experimental::wait_for_one(), asio::deferred); 68 | 69 | timer->expires_after(asio::chrono::seconds(0)); 70 | 71 | if (exp) std::rethrow_exception(exp); 72 | co_return; 73 | } 74 | 75 | rc m_timer; 76 | }; 77 | } // namespace helper 78 | -------------------------------------------------------------------------------- /core/include/core/bit_flags.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | template 5 | requires std::is_enum_v 6 | class BitFlags { 7 | using UnderlyingT = typename std::make_unsigned_t>; 8 | 9 | public: 10 | constexpr BitFlags() noexcept: bits_(0u) {} 11 | constexpr BitFlags(UnderlyingT val) noexcept: bits_(val) {} 12 | BitFlags& operator=(UnderlyingT val) noexcept { bits_ = val; } 13 | 14 | template 15 | requires(std::same_as && ...) 16 | constexpr BitFlags(Ts... es) noexcept { 17 | std::array arr_es { es... }; 18 | for (auto e : arr_es) { 19 | set(e); 20 | } 21 | } 22 | 23 | BitFlags& set(EnumT e, bool value = true) noexcept { 24 | bits_.set(underlying(e), value); 25 | return *this; 26 | } 27 | 28 | BitFlags& reset(EnumT e) noexcept { 29 | set(e, false); 30 | return *this; 31 | } 32 | 33 | BitFlags& reset() noexcept { 34 | bits_.reset(); 35 | return *this; 36 | } 37 | 38 | [[nodiscard]] bool all() const noexcept { return bits_.all(); } 39 | 40 | [[nodiscard]] bool any() const noexcept { return bits_.any(); } 41 | 42 | [[nodiscard]] bool none() const noexcept { return bits_.none(); } 43 | 44 | [[nodiscard]] constexpr std::size_t size() const noexcept { return bits_.size(); } 45 | 46 | [[nodiscard]] std::size_t count() const noexcept { return bits_.count(); } 47 | 48 | constexpr bool operator[](EnumT e) const { return bits_[underlying(e)]; } 49 | 50 | constexpr bool operator[](UnderlyingT t) const { return bits_[t]; } 51 | 52 | auto to_string() const { return bits_.to_string(); } 53 | 54 | private: 55 | static constexpr UnderlyingT underlying(EnumT e) { return static_cast(e); } 56 | 57 | private: 58 | std::bitset bits_; 59 | }; 60 | -------------------------------------------------------------------------------- /core/include/core/core.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | import qcm.core; 4 | 5 | #ifdef __clangd__ 6 | # include "core/clangd.h" 7 | #endif 8 | 9 | #include "core/macro.h" -------------------------------------------------------------------------------- /core/include/core/expected_helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "core/core.h" 6 | #include "core/log.h" 7 | #include "core/fmt.h" 8 | #include "core/optional_helper.h" 9 | namespace helper 10 | { 11 | 12 | } // namespace helper -------------------------------------------------------------------------------- /core/include/core/fmt.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef __clangd__ 4 | # include "core/clangd.h" 5 | #else 6 | import qcm.core; 7 | #endif -------------------------------------------------------------------------------- /core/include/core/helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | import qcm.helper; -------------------------------------------------------------------------------- /core/include/core/math.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace ycore 5 | { 6 | template 7 | requires(! std::numeric_limits::is_integer) 8 | constexpr auto equal_within_ulps(T x, T y, std::size_t n) -> bool { 9 | // Since `epsilon()` is the gap size (ULP, unit in the last place) 10 | // of floating-point numbers in interval [1, 2), we can scale it to 11 | // the gap size in interval [2^e, 2^{e+1}), where `e` is the exponent 12 | // of `x` and `y`. 13 | 14 | // If `x` and `y` have different gap sizes (which means they have 15 | // different exponents), we take the smaller one. Taking the bigger 16 | // one is also reasonable, I guess. 17 | const T m = std::min(std::fabs(x), std::fabs(y)); 18 | 19 | // Subnormal numbers have fixed exponent, which is `min_exponent - 1`. 20 | const int exp = m < std::numeric_limits::min() ? std::numeric_limits::min_exponent - 1 21 | : std::ilogb(m); 22 | 23 | // We consider `x` and `y` equal if the difference between them is 24 | // within `n` ULPs. 25 | return std::fabs(x - y) <= n * std::ldexp(std::numeric_limits::epsilon(), exp); 26 | } 27 | } // namespace ycore -------------------------------------------------------------------------------- /core/include/core/optional_helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "core/core.h" 5 | 6 | namespace helper 7 | { 8 | template 9 | concept is_optional = ycore::is_specialization_of_v, std::optional>; 10 | 11 | template 12 | auto to_optional(T* pointer) -> std::optional> { 13 | if (pointer) { 14 | return { *pointer }; 15 | } else { 16 | return std::nullopt; 17 | } 18 | } 19 | 20 | template 21 | requires ycore::MapConcept> 22 | auto to_optional(T& container, const TKey& key) -> std::optional { 23 | if (auto it = container.find(key); it != container.end()) return it->second; 24 | return std::nullopt; 25 | } 26 | 27 | } // namespace helper -------------------------------------------------------------------------------- /core/include/core/qasio/qt_execution_context.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "core/core.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | class QtExecEvent : public QEvent { 13 | public: 14 | QtExecEvent(QEvent::Type t): QEvent(t) {} 15 | 16 | virtual ~QtExecEvent() = default; 17 | 18 | virtual void invoke() = 0; 19 | }; 20 | 21 | template 22 | struct QtExecFuncEvent : QtExecEvent { 23 | QtExecFuncEvent(F&& f, QEvent::Type t): QtExecEvent(t), m_f(std::forward(f)) {} 24 | 25 | void invoke() override { m_f(); } 26 | 27 | private: 28 | F m_f; 29 | }; 30 | 31 | struct QtExecutionEventRunner : QObject { 32 | QtExecutionEventRunner(QEvent::Type t): event_type(t) {} 33 | QEvent::Type event_type; 34 | auto event(QEvent* event) -> bool override; 35 | }; 36 | 37 | class QtExecutor; 38 | 39 | class QtExecutionContext : public asio::execution_context, NoCopy { 40 | public: 41 | QtExecutionContext(QObject*, QEvent::Type); 42 | QtExecutionContext(QThread*, QEvent::Type); 43 | virtual ~QtExecutionContext(); 44 | 45 | QtExecutionContext(const QtExecutionContext&) = delete; 46 | QtExecutionContext(QtExecutionContext&&) = delete; 47 | 48 | auto get_executor() -> QtExecutor&; 49 | 50 | template 51 | void post(F&& f) { 52 | auto event = new QtExecFuncEvent(std::forward(f), event_type()); 53 | QCoreApplication::postEvent(m_target, event); 54 | } 55 | 56 | auto event_type() const -> QEvent::Type; 57 | 58 | private: 59 | QtExecutionEventRunner* m_target; 60 | Box m_ex; 61 | }; 62 | 63 | 64 | #include "core/qasio/qt_executor.h" 65 | -------------------------------------------------------------------------------- /core/include/core/qasio/qt_executor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "core/qasio/qt_execution_context.h" 6 | #include "core/core.h" 7 | #include "core/log.h" 8 | 9 | class QtExecutor { 10 | public: 11 | explicit QtExecutor(Arc ctx): m_ctx(ctx.get()) { _assert_(m_ctx); } 12 | explicit QtExecutor(QtExecutionContext* ctx): m_ctx(ctx) { _assert_(m_ctx); } 13 | 14 | QtExecutor(const QtExecutor& o) noexcept: m_ctx(o.m_ctx) { _assert_(m_ctx); } 15 | // same as copy as no need to drop 16 | QtExecutor(QtExecutor&& o) noexcept: m_ctx(o.m_ctx) { _assert_(m_ctx); } 17 | 18 | QtExecutor& operator=(const QtExecutor& o) noexcept { 19 | m_ctx = o.m_ctx; 20 | return *this; 21 | } 22 | QtExecutor& operator=(QtExecutor&& o) noexcept { 23 | m_ctx = o.m_ctx; 24 | return *this; 25 | } 26 | 27 | QtExecutionContext& query(asio::execution::context_t) const noexcept { return *m_ctx; } 28 | 29 | static constexpr asio::execution::blocking_t query(asio::execution::blocking_t) noexcept { 30 | return asio::execution::blocking.never; 31 | } 32 | 33 | static constexpr asio::execution::relationship_t 34 | query(asio::execution::relationship_t) noexcept { 35 | return asio::execution::relationship.fork; 36 | } 37 | 38 | static constexpr asio::execution::outstanding_work_t 39 | query(asio::execution::outstanding_work_t) noexcept { 40 | return asio::execution::outstanding_work.tracked; 41 | } 42 | 43 | template 44 | void execute(F f) const { 45 | m_ctx->post(std::move(f)); 46 | } 47 | 48 | bool operator==(QtExecutor const& o) const noexcept { return m_ctx == o.m_ctx; } 49 | bool operator!=(QtExecutor const& o) const noexcept { return ! (*this == o); } 50 | 51 | private: 52 | QtExecutionContext* m_ctx; 53 | }; 54 | 55 | static_assert(asio::execution::is_executor_v); 56 | -------------------------------------------------------------------------------- /core/include/core/qasio/qt_holder.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace helper 6 | { 7 | template 8 | requires(std::is_base_of_v && ...) 9 | class QHolder : public QObject { 10 | public: 11 | QHolder(bool override_parent, T*... args): QObject(nullptr), m_datas(args...) { 12 | ( 13 | [override_parent](auto arg, auto self) { 14 | if (arg != nullptr) { 15 | if (override_parent || arg->parent() == nullptr) { 16 | arg->setParent(self); 17 | } 18 | } 19 | }(args, this), 20 | ...); 21 | }; 22 | QHolder(T*... args): QHolder(true, args...) {}; 23 | ~QHolder() {}; 24 | template 25 | auto data() { 26 | return std::get(m_datas); 27 | } 28 | 29 | template 30 | auto pointer() { 31 | return QPointer { data() }; 32 | } 33 | 34 | private: 35 | std::tuple m_datas; 36 | }; 37 | template 38 | auto create_qholder(T* data) -> std::shared_ptr> { 39 | return std::make_shared>(data); 40 | } 41 | template 42 | auto create_qholder(bool override_parent, T* data) -> std::shared_ptr> { 43 | return std::make_shared>(override_parent, data); 44 | } 45 | } // namespace helper -------------------------------------------------------------------------------- /core/include/core/qasio/qt_watcher.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "core/core.h" 10 | #include "core/qasio/qt_executor.h" 11 | 12 | namespace helper 13 | { 14 | template 15 | class QWatcher { 16 | public: 17 | QWatcher(): m_ptr(nullptr) {} 18 | QWatcher(T* t): QWatcher() { 19 | if (t != nullptr) { 20 | auto thread = t->thread(); 21 | m_ptr = rc(new helper(t, thread), [](helper* h) { 22 | if (QThread::isMainThread() && QThread::currentThread() == h->thread) { 23 | auto exec = QThread::currentThread()->property("exec"); 24 | if (! exec.isNull() && ! exec.value()) { 25 | // thread is not in exec, delete directly 26 | delete h; 27 | return; 28 | } 29 | } 30 | h->deleteLater(); 31 | }); 32 | m_ptr->moveToThread(thread); 33 | } 34 | } 35 | // QWatcher(QPointer t): m_ptr(make_rc>(t)) {} 36 | 37 | ~QWatcher() {} 38 | 39 | QWatcher(const QWatcher&) = default; 40 | QWatcher& operator=(const QWatcher&) = default; 41 | QWatcher(QWatcher&&) = default; 42 | QWatcher& operator=(QWatcher&&) = default; 43 | 44 | T* operator->() const { return get(); } 45 | operator bool() const { return m_ptr && m_ptr->pointer; } 46 | auto get() const -> T* { 47 | if (m_ptr) { 48 | return m_ptr->pointer.load(); 49 | } 50 | return nullptr; 51 | } 52 | 53 | void take_owner() const { 54 | if (*this) { 55 | this->get()->setParent(this->m_ptr.get()); 56 | } 57 | } 58 | 59 | operator T*() const { return get(); } 60 | 61 | auto thread() const -> QThread* { 62 | if (*this) { 63 | return m_ptr->thread(); 64 | }; 65 | return nullptr; 66 | } 67 | 68 | private: 69 | struct helper : QObject { 70 | std::atomic pointer; 71 | QThread* thread; 72 | helper(T* p, QThread* t): pointer(p), thread(t) { 73 | connect( 74 | p, 75 | &QObject::destroyed, 76 | this, 77 | [this] { 78 | pointer = nullptr; 79 | }, 80 | Qt::DirectConnection); 81 | } 82 | }; 83 | 84 | rc m_ptr; 85 | }; 86 | } // namespace helper -------------------------------------------------------------------------------- /core/include/core/qlist_helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "core/helper.h" 4 | 5 | #include 6 | 7 | template 8 | requires std::ranges::range && convertable> 9 | struct Convert, F> { 10 | static void from(QList& out, const F& f) { 11 | using from_value_type = std::ranges::range_value_t; 12 | out.clear(); 13 | std::transform(std::ranges::begin(f), 14 | std::ranges::end(f), 15 | std::back_inserter(out), 16 | [](const from_value_type& v) { 17 | return convert_from(v); 18 | }); 19 | } 20 | }; -------------------------------------------------------------------------------- /core/include/core/qmeta_helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace helper 6 | { 7 | 8 | inline bool is_floating_point_metatype_id(int id) { 9 | switch (id) { 10 | case QMetaType::Float16: 11 | case QMetaType::Float: 12 | case QMetaType::Double: return true; 13 | default: return false; 14 | } 15 | } 16 | 17 | inline bool is_integer_metatype_id(int id) { 18 | switch (id) { 19 | case QMetaType::Int: 20 | case QMetaType::UInt: 21 | case QMetaType::LongLong: 22 | case QMetaType::ULongLong: 23 | case QMetaType::Short: 24 | case QMetaType::UShort: 25 | case QMetaType::Char: 26 | case QMetaType::SChar: 27 | case QMetaType::UChar: 28 | case QMetaType::Long: 29 | case QMetaType::ULong: 30 | case QMetaType::Bool: return true; 31 | default: return false; 32 | } 33 | } 34 | 35 | inline bool is_numeric_metatype_id(int id) { 36 | switch (id) { 37 | case QMetaType::Float: 38 | case QMetaType::Double: return true; 39 | default: return is_integer_metatype_id(id); 40 | } 41 | } 42 | 43 | inline bool is_numeric_metatype(QMetaType type) { return is_numeric_metatype_id(type.id()); } 44 | 45 | } // namespace helper -------------------------------------------------------------------------------- /core/include/core/qvariant_helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "core/core.h" 7 | 8 | template 9 | struct Convert, QVariant> { 10 | using out_type = std::optional; 11 | using in_type = QVariant; 12 | static void from(out_type& o, const in_type& in) { 13 | o = in.canConvert() ? out_type(in.value()) : std::nullopt; 14 | } 15 | }; 16 | 17 | template 18 | struct Convert> { 19 | using out_type = QVariant; 20 | using in_type = std::optional; 21 | static void from(out_type& o, const in_type& in) { 22 | if (in) { 23 | o = out_type::fromValue(in.value()); 24 | } 25 | } 26 | }; -------------------------------------------------------------------------------- /core/include/core/sender.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "core/core.h" 4 | 5 | namespace qcm 6 | { 7 | 8 | namespace detail 9 | { 10 | 11 | template 12 | class Sender { 13 | public: 14 | Sender() = default; 15 | virtual ~Sender() = default; 16 | virtual bool try_send(T) = 0; 17 | virtual std::future send(T) = 0; 18 | virtual void reset() = 0; 19 | }; 20 | 21 | } // namespace detail 22 | 23 | template 24 | class Sender { 25 | public: 26 | using Inner = detail::Sender; 27 | Sender() = default; 28 | ~Sender() = default; 29 | Sender(rc in): m_impl(in) {} 30 | 31 | bool try_send(T t) { return m_impl->try_send(t); } 32 | std::future send(T t) { return m_impl->send(t); } 33 | void reset() { m_impl->reset(); } 34 | 35 | private: 36 | rc m_impl; 37 | }; 38 | 39 | } // namespace qcm 40 | -------------------------------------------------------------------------------- /core/src/asio/asio.cpp: -------------------------------------------------------------------------------- 1 | #include "core/core.h" 2 | 3 | // #ifdef __linux__ 4 | // # define ASIO_DECL C_DECL_EXPORT 5 | // #endif 6 | // 7 | // #undef ASIO_DISABLE_VISIBILITY 8 | // 9 | // #include 10 | #include 11 | 12 | #include "core/log.h" 13 | #include "core/asio/detached_log.h" 14 | #include "core/asio/error.h" 15 | 16 | namespace helper 17 | { 18 | 19 | asio_detached_log_t::asio_detached_log_t(const std::source_location loc): loc(loc) {} 20 | 21 | void asio_detached_log_t::operator()(std::exception_ptr ptr) { 22 | handle_asio_exception(ptr, {}, loc); 23 | } 24 | } // namespace helper 25 | void helper::handle_asio_exception(std::exception_ptr eptr, 26 | asio::any_completion_handler on_error, 27 | const std::source_location loc) { 28 | try { 29 | if (eptr) { 30 | std::rethrow_exception(eptr); 31 | } 32 | } catch (const asio::system_error& ex) { 33 | auto level = qcm::LogLevel::ERROR; 34 | auto code = ex.code().value(); 35 | const auto& category = ex.code().category(); 36 | if (category == asio::error::get_system_category()) { 37 | if (code == ECANCELED) { 38 | level = qcm::LogLevel::WARN; 39 | } 40 | } 41 | 42 | qcm::log::log(level, loc, "[{}] {}", category.name(), ex.what()); 43 | if (on_error) on_error(ex.what()); 44 | } catch (const std::exception& ex) { 45 | qcm::log::log(qcm::LogLevel::ERROR, loc, "{}", ex.what()); 46 | if (on_error) on_error(ex.what()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/src/helper/mod.cppm: -------------------------------------------------------------------------------- 1 | export module qcm.helper; 2 | export import :container; 3 | export import :str; -------------------------------------------------------------------------------- /core/src/lambda.cppm: -------------------------------------------------------------------------------- 1 | module; 2 | #include 3 | export module qcm.core:lambda; 4 | 5 | namespace ycore 6 | { 7 | 8 | template 9 | struct y_combinator { 10 | F f; // the lambda will be stored here 11 | 12 | // a forwarding operator(): 13 | template 14 | decltype(auto) operator()(Args&&... args) const { 15 | // we pass ourselves to f, then the arguments. 16 | return f(*this, std::forward(args)...); 17 | } 18 | }; 19 | 20 | export template 21 | y_combinator(F) -> y_combinator; 22 | 23 | export template 24 | struct function_traits_base { 25 | using ret_type = R; 26 | using arg_types = std::tuple; 27 | static constexpr std::size_t arg_count = sizeof...(Args); 28 | template 29 | using nth_arg = std::tuple_element_t; 30 | }; 31 | 32 | export template 33 | struct function_traits; 34 | 35 | export template 36 | struct function_traits : function_traits_base { 37 | using pointer = R (*)(Args...); 38 | }; 39 | 40 | } // namespace ycore -------------------------------------------------------------------------------- /core/src/mem.cpp: -------------------------------------------------------------------------------- 1 | module; 2 | #include 3 | #include 4 | module qcm.core; 5 | import :mem; 6 | 7 | namespace qcm 8 | { 9 | 10 | MemoryStatResource::MemoryStatResource(std::pmr::memory_resource* source) 11 | : m_source(source), 12 | m_current_bytes(0), 13 | m_peak_bytes(0), 14 | m_current_blocks(0), 15 | m_current_largest_block(0) {} 16 | 17 | usize MemoryStatResource::current_bytes() const { return m_current_bytes.load(); } 18 | usize MemoryStatResource::peak_bytes() const { return m_peak_bytes.load(); } 19 | usize MemoryStatResource::current_block_count() const { return m_current_blocks.load(); } 20 | usize MemoryStatResource::current_largest_block() const { return m_current_largest_block.load(); } 21 | 22 | void* MemoryStatResource::do_allocate(usize bytes, usize alignment) { 23 | void* ptr = nullptr; 24 | if (m_source) { 25 | ptr = m_source->allocate(bytes, alignment); 26 | } else { 27 | ptr = ::operator new(bytes, std::align_val_t(alignment)); 28 | } 29 | 30 | m_current_bytes.fetch_add(bytes, std::memory_order_relaxed); 31 | m_current_blocks.fetch_add(1, std::memory_order_relaxed); 32 | 33 | usize prev_peak = m_peak_bytes.load(std::memory_order_relaxed); 34 | usize new_total = m_current_bytes.load(std::memory_order_relaxed); 35 | while (new_total > prev_peak && 36 | ! m_peak_bytes.compare_exchange_weak(prev_peak, new_total, std::memory_order_relaxed)); 37 | 38 | usize prev_largest = m_current_largest_block.load(std::memory_order_relaxed); 39 | while (bytes > prev_largest && ! m_current_largest_block.compare_exchange_weak( 40 | prev_largest, bytes, std::memory_order_relaxed)); 41 | 42 | return ptr; 43 | } 44 | 45 | void MemoryStatResource::do_deallocate(void* ptr, usize bytes, usize alignment) { 46 | m_current_bytes.fetch_sub(bytes, std::memory_order_relaxed); 47 | m_current_blocks.fetch_sub(1, std::memory_order_relaxed); 48 | 49 | if (m_source) { 50 | m_source->deallocate(ptr, bytes, alignment); 51 | } else { 52 | ::operator delete(ptr, bytes, std::align_val_t(alignment)); 53 | } 54 | } 55 | 56 | bool MemoryStatResource::do_is_equal(const std::pmr::memory_resource& other) const noexcept { 57 | return this == &other; 58 | } 59 | 60 | } // namespace qcm -------------------------------------------------------------------------------- /core/src/mem.cppm: -------------------------------------------------------------------------------- 1 | module; 2 | #include 3 | #include 4 | export module qcm.core:mem; 5 | import :basic; 6 | 7 | namespace qcm 8 | { 9 | export class MemoryStatResource : public std::pmr::memory_resource { 10 | public: 11 | MemoryStatResource(std::pmr::memory_resource* source = nullptr); 12 | 13 | usize current_bytes() const; 14 | usize peak_bytes() const; 15 | usize current_block_count() const; 16 | usize current_largest_block() const; 17 | 18 | protected: 19 | void* do_allocate(usize bytes, usize alignment) override; 20 | void do_deallocate(void* ptr, usize bytes, usize alignment) override; 21 | bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override; 22 | 23 | private: 24 | std::pmr::memory_resource* m_source; 25 | std::atomic m_current_bytes; 26 | std::atomic m_peak_bytes; 27 | std::atomic m_current_blocks; 28 | std::atomic m_current_largest_block; 29 | }; 30 | } // namespace qcm -------------------------------------------------------------------------------- /core/src/mod.cppm: -------------------------------------------------------------------------------- 1 | export module qcm.core; 2 | export import :basic; 3 | export import :type_list; 4 | export import :lambda; 5 | export import :fmt; 6 | export import :mem; 7 | -------------------------------------------------------------------------------- /core/src/path.cpp: -------------------------------------------------------------------------------- 1 | #include "core/path.h" 2 | #include "core/log.h" 3 | 4 | #include 5 | 6 | using path = std::filesystem::path; 7 | 8 | namespace xdg 9 | { 10 | constexpr std::string_view AppName { "Qcm" }; 11 | 12 | path home() { 13 | static path home_path = []() { 14 | char* env_home = std::getenv("HOME"); 15 | _assert_msg_(env_home != NULL, "no HOME env"); 16 | return env_home; 17 | }(); 18 | return home_path; 19 | } 20 | 21 | namespace config 22 | { 23 | path home() { 24 | static path home_path = []() { 25 | char* env_path = std::getenv("XDG_CONFIG_HOME"); 26 | return env_path != NULL ? env_path : xdg::home() / ".config"; 27 | }(); 28 | return home_path; 29 | } 30 | } // namespace config 31 | 32 | namespace data 33 | { 34 | path home() { 35 | static path home_path = []() { 36 | char* env_path = std::getenv("XDG_DATA_HOME"); 37 | return env_path != NULL ? env_path : xdg::home() / ".data"; 38 | }(); 39 | return home_path; 40 | } 41 | } // namespace data 42 | 43 | namespace cache 44 | { 45 | path home() { 46 | static path home_path = []() { 47 | char* env_path = std::getenv("XDG_CACHE_HOME"); 48 | return env_path != NULL ? env_path : xdg::home() / ".cache"; 49 | }(); 50 | return home_path; 51 | } 52 | } // namespace cache 53 | 54 | } // namespace xdg 55 | 56 | std::filesystem::path qcm::config_path() { return xdg::config::home() / xdg::AppName; } 57 | 58 | std::filesystem::path qcm::data_path() { return xdg::data::home() / xdg::AppName; } 59 | 60 | std::filesystem::path qcm::cache_path() { return xdg::cache::home() / xdg::AppName; } 61 | 62 | bool qcm::init_path(std::span pathes) { 63 | for (auto& p : pathes) { 64 | std::error_code ec; 65 | std::filesystem::create_directories(p, ec); 66 | _assert_msg_(! ec, "path: {}, info: {}({})", p.native(), ec.message(), ec.value()) 67 | } 68 | return true; 69 | } 70 | -------------------------------------------------------------------------------- /core/src/qasio/qt_execution_context.cpp: -------------------------------------------------------------------------------- 1 | #include "qt_execution_context.h" 2 | #include "qt_executor.h" 3 | 4 | QtExecutionContext::QtExecutionContext(QThread* thread, QEvent::Type t) 5 | : m_target(new QtExecutionEventRunner(t)), m_ex(make_box(this)) { 6 | // move to target thread 7 | if (thread != m_target->thread()) { 8 | m_target->moveToThread(thread); 9 | } 10 | } 11 | QtExecutionContext::QtExecutionContext(QObject* target, QEvent::Type t) 12 | : QtExecutionContext(target->thread(), t) {} 13 | 14 | QtExecutionContext::~QtExecutionContext() { m_target->deleteLater(); } 15 | 16 | auto QtExecutionContext::event_type() const -> QEvent::Type { return m_target->event_type; } 17 | 18 | auto QtExecutionContext::get_executor() -> QtExecutor& { return *m_ex; } 19 | 20 | auto QtExecutionEventRunner::event(QEvent* event) -> bool { 21 | if (event->type() == event_type) { 22 | auto p = static_cast(event); 23 | p->accept(); 24 | p->invoke(); 25 | return true; 26 | } else { 27 | return QObject::event(event); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/qasio/qt_executor.cpp: -------------------------------------------------------------------------------- 1 | #include "qt_executor.h" 2 | -------------------------------------------------------------------------------- /core/src/random.cppm: -------------------------------------------------------------------------------- 1 | module; 2 | #include "effolkronium/random.hpp" 3 | export module qcm.random; 4 | 5 | namespace qcm { 6 | 7 | export using Random = effolkronium::random_thread_local; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /crypto/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(OpenSSL REQUIRED) 2 | 3 | add_library(crypto STATIC include/crypto/crypto.h crypto.cpp) 4 | 5 | target_include_directories( 6 | crypto 7 | PUBLIC include 8 | PRIVATE include/crypto) 9 | target_link_libraries( 10 | crypto 11 | PUBLIC core 12 | PRIVATE OpenSSL::SSL OpenSSL::Crypto) 13 | target_compile_definitions(crypto PRIVATE OPENSSL_API_COMPAT=30000) 14 | -------------------------------------------------------------------------------- /crypto/include/crypto/crypto.h: -------------------------------------------------------------------------------- 1 | #include "core/core.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | struct evp_cipher_st; 8 | struct evp_md_st; 9 | 10 | namespace qcm 11 | { 12 | 13 | namespace crypto 14 | { 15 | using reader = std::function)>; 16 | using bytes_view = std::span; 17 | using cipher = evp_cipher_st; 18 | using md = evp_md_st; 19 | 20 | const cipher* aes_128_ecb() noexcept; 21 | const cipher* aes_128_cbc() noexcept; 22 | const md* md5() noexcept; 23 | 24 | auto encrypt(const cipher* cipher, bytes_view key, bytes_view iv, bytes_view data) 25 | -> Result, int>; 26 | auto decrypt(const cipher* cipher, bytes_view key, bytes_view iv, bytes_view data) 27 | -> Result, int>; 28 | auto encode(bytes_view data) -> Result, int>; 29 | auto decode(bytes_view data) -> Result, int>; 30 | auto digest(const md* type, bytes_view data) -> Result, int>; 31 | auto digest(const md* type, usize buf_size, const reader&) -> Result, int>; 32 | 33 | namespace hex 34 | { 35 | std::vector encode_low(bytes_view data); 36 | std::vector encode_up(bytes_view data); 37 | } // namespace hex 38 | 39 | struct Pkey; 40 | 41 | namespace rsa 42 | { 43 | enum class Padding 44 | { 45 | NONE, 46 | PKCS1, 47 | PKCS1_OAEP, 48 | X931, 49 | }; 50 | 51 | class Rsa : NoCopy { 52 | public: 53 | Rsa(); 54 | ~Rsa(); 55 | 56 | Rsa(Rsa&&) noexcept; 57 | Rsa& operator=(Rsa&&) noexcept; 58 | 59 | static auto from_pem(bytes_view data, bytes_view pass) -> std::optional; 60 | auto encrypt(Padding, bytes_view data) -> Result, int>; 61 | 62 | private: 63 | up key; 64 | }; 65 | 66 | } // namespace rsa 67 | } // namespace crypto 68 | } // namespace qcm 69 | -------------------------------------------------------------------------------- /error/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(error include/error/error.h) 2 | 3 | target_include_directories(error PUBLIC include) 4 | target_link_libraries(error PUBLIC core) 5 | -------------------------------------------------------------------------------- /message/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(Qt6 REQUIRED COMPONENTS Core Quick Protobuf ProtobufWellKnownTypes ProtobufQuick) 2 | set(CMAKE_AUTOMOC ON) 3 | 4 | set(output_inc_dir "${CMAKE_CURRENT_BINARY_DIR}/gen") 5 | set(output_dir "${CMAKE_CURRENT_BINARY_DIR}/gen/Qcm/message") 6 | 7 | qt_add_protobuf( 8 | message 9 | PROTO_FILES 10 | proto/message.proto 11 | proto/model.proto 12 | OUTPUT_DIRECTORY 13 | "${output_dir}" 14 | OUTPUT_TARGETS 15 | message_targets 16 | QML 17 | QML_URI 18 | "Qcm.Msg") 19 | target_include_directories(message PUBLIC "${output_inc_dir}") 20 | target_link_libraries(message PUBLIC Qt6::ProtobufWellKnownTypes Qt6::ProtobufQuick) -------------------------------------------------------------------------------- /mpris/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if(NOT ANDROID AND NOT WIN32) 2 | find_package(Qt6 REQUIRED COMPONENTS DBus) 3 | 4 | add_library( 5 | mpris STATIC 6 | include/mpris/mpris.h include/mpris/mediaplayer2.h 7 | include/mpris/mediaplayer2_adaptor.h mpris.cpp mediaplayer2.cpp 8 | mediaplayer2_adaptor.cpp) 9 | 10 | target_include_directories(mpris PUBLIC include) 11 | target_link_libraries(mpris PUBLIC core Qt6::Core Qt6::DBus) 12 | set_target_properties(mpris PROPERTIES AUTOMOC ON) 13 | endif() 14 | -------------------------------------------------------------------------------- /mpris/include/mpris/mpris.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace mpris 6 | { 7 | 8 | class MediaPlayer2; 9 | class Mpris : public QObject { 10 | Q_OBJECT 11 | public: 12 | Mpris(QObject* parent = nullptr); 13 | virtual ~Mpris(); 14 | 15 | void registerService(QString serviceName); 16 | void unregisterService(); 17 | 18 | MediaPlayer2* mediaplayer2() const; 19 | 20 | private: 21 | MediaPlayer2* m_mp; 22 | QString m_serviceName; 23 | }; 24 | } // namespace mpris 25 | -------------------------------------------------------------------------------- /mpris/mpris.cpp: -------------------------------------------------------------------------------- 1 | #include "mpris/mpris.h" 2 | #include "mpris/mediaplayer2.h" 3 | 4 | #include 5 | 6 | using namespace mpris; 7 | using namespace Qt::Literals::StringLiterals; 8 | 9 | namespace 10 | { 11 | static const QString ServiceNamePrefix { u"org.mpris.MediaPlayer2."_s }; 12 | } 13 | 14 | Mpris::Mpris(QObject* parent): QObject(parent), m_mp(new MediaPlayer2(this)) {}; 15 | 16 | Mpris::~Mpris() { unregisterService(); } 17 | 18 | mpris::MediaPlayer2* Mpris::mediaplayer2() const { return m_mp; } 19 | 20 | void Mpris::registerService(QString serviceName) { 21 | QString mspris2Name { ServiceNamePrefix + serviceName }; 22 | 23 | if (! QDBusConnection::sessionBus().registerService(mspris2Name)) { 24 | return; 25 | } 26 | m_serviceName = serviceName; 27 | } 28 | 29 | void Mpris::unregisterService() { 30 | if (! m_serviceName.isEmpty()) { 31 | QDBusConnection::sessionBus().unregisterService(ServiceNamePrefix + m_serviceName); 32 | } 33 | } 34 | 35 | #include 36 | -------------------------------------------------------------------------------- /platform/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(platform STATIC src/platform.cpp src/win.cpp src/linux.cpp) 2 | 3 | target_sources( 4 | platform 5 | PUBLIC FILE_SET 6 | all 7 | TYPE 8 | CXX_MODULES 9 | BASE_DIRS 10 | src 11 | FILES 12 | src/platform.cppm) 13 | 14 | target_link_libraries(platform PUBLIC rstd::rstd) 15 | -------------------------------------------------------------------------------- /platform/src/linux.cpp: -------------------------------------------------------------------------------- 1 | #ifdef __linux__ 2 | module; 3 | # include 4 | # include 5 | # include 6 | # include 7 | # include 8 | # include 9 | module platform; 10 | 11 | namespace 12 | { 13 | auto get_env_var(std::string_view var_name) -> std::optional { 14 | if (const char* value = std::getenv(var_name.data())) { 15 | return std::string_view(value); 16 | } 17 | return std::nullopt; 18 | } 19 | } // namespace 20 | 21 | namespace plt 22 | { 23 | void set_thread_name(const char* name) { pthread_setname_np(pthread_self(), name); } 24 | 25 | auto is_terminal() -> bool { return isatty(fileno(stdout)); } 26 | 27 | auto support_color() -> bool { 28 | // Check if the standard output is a terminal 29 | if (! plt::is_terminal()) { 30 | return false; 31 | } 32 | 33 | // Get the TERM environment variable 34 | auto term = get_env_var("TERM"); 35 | if (! term) { 36 | return false; 37 | } 38 | 39 | // Check if the TERM value contains keywords indicating color support 40 | return term->find("color") != std::string_view::npos || 41 | term->find("xterm") != std::string_view::npos || 42 | term->find("screen") != std::string_view::npos || 43 | term->find("tmux") != std::string_view::npos; 44 | } 45 | } // namespace plt 46 | #endif -------------------------------------------------------------------------------- /platform/src/platform.cpp: -------------------------------------------------------------------------------- 1 | module; 2 | #include 3 | #include 4 | #include 5 | 6 | #if defined(__GLIBC__) 7 | # include 8 | #endif 9 | 10 | module platform; 11 | 12 | namespace plt 13 | { 14 | 15 | void malloc_init() { 16 | // glibc fragmentation 17 | // https://github.com/prestodb/presto/issues/8993 18 | #if defined(__GLIBC__) 19 | putenv((char*)"MALLOC_ARENA_MAX=2"); 20 | mallopt(M_ARENA_MAX, 2); 21 | #endif 22 | } 23 | 24 | std::size_t malloc_trim(std::size_t pad) { 25 | #if defined(__GLIBC__) 26 | return ::malloc_trim(pad); 27 | #else 28 | return 1; 29 | #endif 30 | } 31 | 32 | void malloc_trim_count(std::size_t pad, std::size_t count) { 33 | static std::atomic size { 0 }; 34 | if (size = (size + 1) % count; size == 0) { 35 | malloc_trim(pad); 36 | } 37 | } 38 | 39 | auto mem_info() -> MemInfo { 40 | #if defined(__GLIBC__) 41 | auto info = mallinfo2(); 42 | return MemInfo { 43 | .totle_in_use = info.uordblks, 44 | .heap = info.arena, 45 | .mmap = info.hblkhd, 46 | .mmap_num = info.hblks, 47 | }; 48 | #else 49 | return {}; 50 | #endif 51 | } 52 | 53 | } // namespace plt -------------------------------------------------------------------------------- /platform/src/platform.cppm: -------------------------------------------------------------------------------- 1 | module; 2 | #include 3 | #include 4 | export module platform; 5 | 6 | export namespace plt 7 | { 8 | 9 | void malloc_init(); 10 | std::size_t malloc_trim(std::size_t pad); 11 | void malloc_trim_count(std::size_t pad, std::size_t count); 12 | 13 | struct MemInfo { 14 | std::size_t totle_in_use; 15 | std::size_t heap; 16 | std::size_t mmap; 17 | std::size_t mmap_num; 18 | }; 19 | 20 | auto mem_info() -> MemInfo; 21 | 22 | void set_thread_name(const char* name); 23 | 24 | auto is_terminal() -> bool; 25 | auto support_color() -> bool; 26 | 27 | } // namespace plt -------------------------------------------------------------------------------- /platform/src/win.cpp: -------------------------------------------------------------------------------- 1 | #ifdef _WIN32 2 | module; 3 | 4 | # include 5 | # include 6 | # include 7 | 8 | module platform; 9 | 10 | std::wstring utf8_to_wstring(const std::string& str) { 11 | int size_needed = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.length(), NULL, 0); 12 | std::wstring wstr(size_needed, 0); 13 | MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.length(), &wstr[0], size_needed); 14 | return wstr; 15 | } 16 | 17 | std::string wstring_to_utf8(const std::wstring& wstr) { 18 | int size_needed = 19 | WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int)wstr.length(), NULL, 0, NULL, NULL); 20 | std::string str(size_needed, 0); 21 | WideCharToMultiByte( 22 | CP_UTF8, 0, wstr.c_str(), (int)wstr.length(), &str[0], size_needed, NULL, NULL); 23 | return str; 24 | } 25 | 26 | namespace plt 27 | { 28 | void set_thread_name(const char* name) { 29 | auto wname = utf8_to_wstring(name); 30 | HRESULT hr = SetThreadDescription(GetCurrentThread(), wname.c_str()); 31 | if (FAILED(hr)) { 32 | // Handle error 33 | } 34 | } 35 | 36 | auto is_terminal() -> bool { return _isatty(_fileno(stdout)); } 37 | 38 | auto support_color() -> bool { 39 | // Get the handle to the standard output 40 | HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE); 41 | if (hStdout == INVALID_HANDLE_VALUE) { 42 | return false; 43 | } 44 | 45 | // Retrieve the current console mode 46 | DWORD consoleMode = 0; 47 | if (! GetConsoleMode(hStdout, &consoleMode)) { 48 | return false; 49 | } 50 | 51 | // Check if the console supports virtual terminal sequences (ANSI colors) 52 | return (consoleMode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0; 53 | } 54 | } // namespace plt 55 | 56 | #endif -------------------------------------------------------------------------------- /player/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(FFMPEG_TARGETS) 2 | if(ANDROID) 3 | find_package(ffmpeg REQUIRED) 4 | set(FFMPEG_TARGETS ffmpeg::avformat ffmpeg::avcodec ffmpeg::avutil 5 | ffmpeg::swresample) 6 | else() 7 | pkg_check_modules( 8 | LIBAV 9 | REQUIRED 10 | IMPORTED_TARGET 11 | libavformat 12 | libavcodec 13 | libavutil 14 | libswresample) 15 | set(FFMPEG_TARGETS PkgConfig::LIBAV) 16 | endif() 17 | 18 | add_library( 19 | player STATIC 20 | include/player/player.h 21 | include/player/player_p.h 22 | player.cpp 23 | context.h 24 | audio_device.h 25 | audio_decoder.h 26 | stream_reader.h) 27 | 28 | target_include_directories( 29 | player 30 | PUBLIC include 31 | PRIVATE .) 32 | 33 | target_link_libraries( 34 | player 35 | PUBLIC core error core.asio ${FFMPEG_TARGETS} rstd::rstd 36 | PRIVATE ctre::ctre cubeb::cubeb 37 | "$<$:winmm>" 38 | ) 39 | -------------------------------------------------------------------------------- /player/audio_frame.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "ffmpeg_frame.h" 6 | #include "audio_stream_params.h" 7 | #include "ffmpeg_error.h" 8 | 9 | #include "core/core.h" 10 | 11 | namespace player 12 | { 13 | using namespace std::chrono; 14 | 15 | struct AudioFrame : NoCopy { 16 | using Self = AudioFrame; 17 | 18 | AudioFrame() = default; 19 | ~AudioFrame() = default; 20 | AudioFrame(Self&& o) noexcept { *this = std::move(o); } 21 | Self& operator=(Self&& o) noexcept { 22 | ff = std::move(o.ff); 23 | return *this; 24 | } 25 | 26 | static Self from(FFmpegFrame&& ff) { return Self(std::move(ff)); } 27 | static FFmpegResult from(const FFmpegFrame& ff) { 28 | auto ff_ref = ff.ref(); 29 | return RECORD(ff_ref.map([](FFmpegFrame&& ff) { 30 | return from(std::move(ff)); 31 | })); 32 | } 33 | 34 | bool is_planar() const { return av_sample_fmt_is_planar((AVSampleFormat)ff->format); } 35 | 36 | AudioParams params() const { 37 | AudioParams out; 38 | out.format = (AVSampleFormat)ff->format; 39 | out.sample_rate = ff->sample_rate; 40 | av_channel_layout_copy(&out.ch_layout, &(ff->ch_layout)); 41 | return out; 42 | } 43 | 44 | void set_params(const AudioParams& in) { 45 | ff->format = in.format; 46 | ff->sample_rate = in.sample_rate; 47 | av_channel_layout_copy(&(ff->ch_layout), &in.ch_layout); 48 | } 49 | 50 | FFmpegResult buffer_size() const { 51 | int ret = av_samples_get_buffer_size( 52 | NULL, ff->ch_layout.nb_channels, ff->nb_samples, (AVSampleFormat)ff->format, 1); 53 | if (ret < 0) return rstd::Err(ret); 54 | return rstd::Ok(ret); 55 | } 56 | 57 | auto channel_data(i32 idx) const { 58 | _assert_msg_(idx < ff->ch_layout.nb_channels, "not such channel: {}", idx); 59 | usize buffer_size_ = buffer_size().unwrap_or(0); 60 | return std::span((const byte*)ff->extended_data[idx], buffer_size_); 61 | } 62 | 63 | void set_eof() { ff->pts = -2; } 64 | bool eof() const { return ff->pts == -2; } 65 | 66 | auto pts_duration() { 67 | return microseconds( 68 | av_rescale_q(ff->pts, ff->time_base, av_make_q(std::micro::num, std::micro::den))); 69 | } 70 | 71 | FFmpegFrame ff; 72 | 73 | private: 74 | AudioFrame(FFmpegFrame&& ff_) noexcept: ff(std::move(ff_)) {} 75 | }; 76 | 77 | } // namespace player 78 | -------------------------------------------------------------------------------- /player/audio_frame_queue.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "ffmpeg_error.h" 9 | #include "audio_frame.h" 10 | #include "audio_stream_params.h" 11 | 12 | #include "core/core.h" 13 | #include "core/log.h" 14 | #include "core/queue_concurrent.h" 15 | 16 | namespace player 17 | { 18 | namespace detail 19 | { 20 | using AudioFrameQueue = qcm::QueueWithSize; 21 | } // namespace detail 22 | 23 | class AudioFrameQueue : public qcm::QueueConcurrent { 24 | public: 25 | using Self = AudioFrameQueue; 26 | AudioFrameQueue(usize max_size, std::pmr::memory_resource* mem) 27 | : qcm::QueueConcurrent(max_size, mem), m_serial(0) {} 28 | ~AudioFrameQueue() {} 29 | 30 | void prepare_item(AudioFrame& out) { 31 | with_lock([this, &out](lock_type&) { 32 | out.set_params(m_audio_params); 33 | }); 34 | } 35 | 36 | void set_audio_params(AudioParams params) { 37 | with_lock([this, ¶ms](lock_type&) { 38 | m_audio_params = params; 39 | }); 40 | } 41 | 42 | bool aborted() { 43 | return with_lock([this](lock_type&) { 44 | return queue().aborted; 45 | }); 46 | } 47 | 48 | void set_aborted(bool v) { 49 | with_lock([this, v](lock_type&) { 50 | queue().aborted = v; 51 | if (v) clear_wait(); 52 | }); 53 | } 54 | void refresh_serial() { 55 | with_lock([this](lock_type&) { 56 | m_serial++; 57 | }); 58 | } 59 | auto serial() { 60 | return with_lock([this](lock_type&) { 61 | return m_serial; 62 | }); 63 | } 64 | void set_serial(usize v) { 65 | return with_lock([this, v](lock_type&) { 66 | m_serial = v; 67 | }); 68 | } 69 | 70 | private: 71 | AudioParams m_audio_params; 72 | usize m_serial; 73 | }; 74 | 75 | } // namespace player 76 | -------------------------------------------------------------------------------- /player/audio_stream_params.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern "C" { 4 | #include 5 | } 6 | 7 | #include "core/core.h" 8 | 9 | namespace player 10 | { 11 | 12 | struct AudioParams { 13 | AudioParams() = default; 14 | AudioParams(const AudioParams& o) { *this = o; } 15 | AudioParams& operator=(const AudioParams& o) { 16 | format = o.format; 17 | sample_rate = o.sample_rate; 18 | av_channel_layout_copy(&ch_layout, &o.ch_layout); 19 | return *this; 20 | } 21 | 22 | void set_ch_layout(const AVChannelLayout& in) { av_channel_layout_copy(&ch_layout, &in); } 23 | auto bytes_per_sample() const { return av_get_bytes_per_sample(format); } 24 | 25 | AVSampleFormat format { AV_SAMPLE_FMT_NONE }; 26 | i32 sample_rate { 1000 }; 27 | AVChannelLayout ch_layout {}; 28 | }; 29 | } // namespace player 30 | -------------------------------------------------------------------------------- /player/context.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "packet_queue.h" 4 | #include "audio_frame_queue.h" 5 | 6 | namespace player 7 | { 8 | 9 | struct Context { 10 | Context(std::pmr::memory_resource* mem) 11 | : audio_pkt_queue(make_rc(2 * 1024 * 1024, mem)), // 2 MB 12 | audio_frame_queue(make_rc(32, mem)) {} 13 | 14 | ~Context() { set_aborted(true); } 15 | 16 | void set_aborted(bool v) { 17 | audio_pkt_queue->set_aborted(v); 18 | audio_frame_queue->set_aborted(v); 19 | } 20 | 21 | void clear() { 22 | audio_pkt_queue->clear(); 23 | audio_frame_queue->clear(); 24 | } 25 | 26 | rc audio_pkt_queue; 27 | rc audio_frame_queue; 28 | }; 29 | 30 | } // namespace player 31 | -------------------------------------------------------------------------------- /player/ffmpeg_dict.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern "C" { 4 | #include 5 | } 6 | #include 7 | #include 8 | #include 9 | 10 | #include "core/core.h" 11 | #include "core/helper.h" 12 | 13 | namespace player 14 | { 15 | class FFmpegDict : NoCopy { 16 | public: 17 | FFmpegDict(): m_raw(NULL) {} 18 | ~FFmpegDict() { av_dict_free(&m_raw); } 19 | FFmpegDict(FFmpegDict&& o): m_raw(std::exchange(o.m_raw, nullptr)) {} 20 | FFmpegDict& operator=(FFmpegDict&& o) { 21 | m_raw = std::exchange(o.m_raw, nullptr); 22 | return *this; 23 | } 24 | 25 | template 26 | requires std::convertible_to 27 | int set(const char* key, T&& value, int flag = 0) { 28 | return av_dict_set(&m_raw, key, value, flag); 29 | } 30 | 31 | template 32 | requires convertable && (! std::convertible_to) 33 | int set(const char* key, T&& value, int flag = 0) { 34 | auto str_val = convert_from(std::forward(value)); 35 | return av_dict_set(&m_raw, key, str_val.c_str(), flag); 36 | } 37 | 38 | auto get(const char* key, int flag = 0) { return av_dict_get(m_raw, key, NULL, flag); } 39 | 40 | FFmpegDict clone(int flag = 0) { 41 | FFmpegDict out; 42 | av_dict_copy(&out.m_raw, m_raw, flag); 43 | return out; 44 | } 45 | auto raw() { return m_raw; } 46 | auto praw() { return &m_raw; } 47 | 48 | private: 49 | AVDictionary* m_raw; 50 | }; 51 | 52 | } // namespace player 53 | -------------------------------------------------------------------------------- /player/ffmpeg_error.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | extern "C" { 7 | #include 8 | } 9 | 10 | #include 11 | #include "error/error.h" 12 | #include "core/core.h" 13 | 14 | namespace player 15 | { 16 | 17 | namespace log = qcm::log; 18 | 19 | struct FFmpegError : public error::ErrorBase { 20 | constexpr static int EABORTED { std::numeric_limits::min() }; 21 | FFmpegError(): code(0) {} 22 | FFmpegError(int e): code(e) {} 23 | FFmpegError& operator=(int e) noexcept { 24 | code = e; 25 | return *this; 26 | } 27 | bool operator==(int e) const noexcept { return e == code; } 28 | operator bool() const { return code < 0; } 29 | 30 | std::string what() const { 31 | std::array err_buf; 32 | av_make_error_string(err_buf.data(), err_buf.size(), code); 33 | return std::format("{}", err_buf.data()); 34 | } 35 | 36 | int code; 37 | }; 38 | 39 | template 40 | using FFmpegResult = rstd::Result; 41 | 42 | } // namespace player 43 | 44 | template<> 45 | struct std::formatter : std::formatter { 46 | template 47 | auto format(const player::FFmpegError& e, FormatContext& ctx) const { 48 | return std::formatter::format(e.what(), ctx); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /player/ffmpeg_format_context.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern "C" { 4 | #include 5 | } 6 | #include 7 | 8 | #include "core/core.h" 9 | #include "ffmpeg_error.h" 10 | #include "ffmpeg_dict.h" 11 | 12 | namespace player 13 | { 14 | 15 | class FFmpegFormatContext : NoCopy { 16 | public: 17 | using Self = FFmpegFormatContext; 18 | FFmpegFormatContext(): m_d(avformat_alloc_context()), m_aborted(false) { 19 | m_d->interrupt_callback.opaque = this; 20 | m_d->interrupt_callback.callback = decode_interrupt_cb; 21 | } 22 | ~FFmpegFormatContext() { 23 | avformat_close_input(&m_d); 24 | avformat_free_context(m_d); 25 | } 26 | 27 | FFmpegFormatContext(Self&& o): FFmpegFormatContext() { *this = std::move(o); } 28 | Self& operator=(Self&& o) { 29 | std::swap(m_d, o.m_d); 30 | m_aborted.store(o.m_aborted); 31 | return *this; 32 | } 33 | constexpr auto operator->() { return m_d; } 34 | constexpr auto operator->() const { return m_d; } 35 | 36 | FFmpegError open_input(const char* url, std::optional opt = std::nullopt) noexcept { 37 | // TODO: timeout 38 | return avformat_open_input(&m_d, url, NULL, opt ? opt->praw() : nullptr); 39 | } 40 | 41 | FFmpegError find_stream_info(AVDictionary** options) noexcept { 42 | return avformat_find_stream_info(m_d, options); 43 | } 44 | 45 | void dump_format(int idx, const char* url, bool is_output) { 46 | av_dump_format(m_d, idx, url, is_output); 47 | } 48 | 49 | FFmpegError read_frame(AVPacket* pkt) { 50 | FFmpegError err = av_read_frame(m_d, pkt); 51 | if (! err) pkt->time_base = m_d->streams[pkt->stream_index]->time_base; 52 | return err; 53 | } 54 | 55 | int find_best_stream(enum AVMediaType type, int wanted_stream_nb, int related_stream, 56 | const AVCodec** decoder_ret, int flags) { 57 | return av_find_best_stream(m_d, type, wanted_stream_nb, related_stream, decoder_ret, flags); 58 | } 59 | 60 | FFmpegError seek_file(int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags) { 61 | return avformat_seek_file(m_d, stream_index, min_ts, ts, max_ts, flags); 62 | } 63 | 64 | void set_aborted(bool v) { m_aborted = v; } 65 | 66 | static int decode_interrupt_cb(void* self_) { 67 | auto self = static_cast(self_); 68 | return self->m_aborted; 69 | } 70 | 71 | private: 72 | AVFormatContext* m_d; 73 | std::atomic m_aborted; 74 | }; 75 | 76 | } // namespace player 77 | -------------------------------------------------------------------------------- /player/ffmpeg_frame.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern "C" { 4 | #include 5 | } 6 | 7 | #include 8 | 9 | #include "core/core.h" 10 | #include "ffmpeg_error.h" 11 | 12 | namespace player 13 | { 14 | 15 | class FFmpegFrame : NoCopy { 16 | public: 17 | using Self = FFmpegFrame; 18 | 19 | FFmpegFrame(): m_frame(av_frame_alloc()) {} 20 | ~FFmpegFrame() { av_frame_free(&m_frame); } 21 | 22 | FFmpegFrame(Self&& o): FFmpegFrame() { *this = std::move(o); } 23 | Self& operator=(Self&& o) noexcept { 24 | std::swap(m_frame, o.m_frame); 25 | return *this; 26 | } 27 | 28 | static FFmpegResult ref(const AVFrame* in) { 29 | Self out; 30 | FFmpegError err = av_frame_ref(out.m_frame, in); 31 | if (err) return rstd::Err(err); 32 | return rstd::Ok(std::move(out)); 33 | } 34 | 35 | auto operator->() { return m_frame; } 36 | auto operator->() const { return m_frame; } 37 | 38 | void set_ch_layout(const AVChannelLayout& in) { 39 | av_channel_layout_copy(&(m_frame->ch_layout), &in); 40 | } 41 | 42 | void unref() { av_frame_unref(m_frame); } 43 | 44 | Self move_ref() { 45 | Self out; 46 | av_frame_move_ref(out.m_frame, m_frame); 47 | return out; 48 | } 49 | 50 | auto ref() const { return Self::ref(m_frame); } 51 | auto raw() const { return m_frame; } 52 | auto praw() { return &m_frame; } 53 | 54 | FFmpegError copy_props(const Self& o) { 55 | return FFmpegError(av_frame_copy_props(m_frame, o.m_frame)).record(); 56 | } 57 | 58 | private: 59 | AVFrame* m_frame; 60 | }; 61 | 62 | } // namespace player 63 | -------------------------------------------------------------------------------- /player/include/player/metadata.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace player 8 | { 9 | struct Metadata { 10 | struct Stream { 11 | std::int64_t bitrate; 12 | std::string mime; 13 | }; 14 | std::map> tags; 15 | std::vector streams; 16 | }; 17 | 18 | } // namespace player -------------------------------------------------------------------------------- /player/include/player/notify.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "core/sender.h" 5 | #include "core/helper.h" 6 | 7 | namespace player 8 | { 9 | enum class PlayState 10 | { 11 | Playing = 0, 12 | Paused, 13 | Stopped 14 | }; 15 | 16 | namespace notify 17 | { 18 | template 19 | struct base { 20 | T value {}; 21 | }; 22 | 23 | struct position : base {}; 24 | struct duration : base {}; 25 | struct playstate : base {}; 26 | struct busy : base {}; 27 | struct cache { 28 | float begin; 29 | float end; 30 | }; 31 | 32 | using info = std::variant; 33 | } // namespace notify 34 | // 35 | using Notifier = qcm::Sender; 36 | 37 | } // namespace player 38 | -------------------------------------------------------------------------------- /player/include/player/player.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "core/core.h" 11 | #include "player/notify.h" 12 | #include "player/metadata.h" 13 | 14 | namespace player 15 | { 16 | 17 | auto get_metadata(const std::filesystem::path&) -> Metadata; 18 | 19 | class Player { 20 | public: 21 | using executor_type = asio::thread_pool::executor_type; 22 | 23 | class Private; 24 | Player(std::string_view name, Notifier, executor_type exc, 25 | std::pmr::memory_resource* mem = std::pmr::get_default_resource()); 26 | ~Player(); 27 | 28 | void play(); 29 | void pause(); 30 | void stop(); 31 | void seek(i32); 32 | 33 | void set_source(std::string_view); 34 | 35 | auto volume() const -> float; 36 | void set_volume(float); 37 | 38 | auto fade_time() const -> u32; 39 | void set_fade_time(u32); 40 | 41 | private: 42 | C_DECLARE_PRIVATE(Player, m_d) 43 | }; 44 | 45 | } // namespace player 46 | -------------------------------------------------------------------------------- /player/include/player/player_p.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "player/player.h" 4 | 5 | // private header 6 | #include "context.h" 7 | #include "stream_reader.h" 8 | #include "audio_decoder.h" 9 | #include "audio_device.h" 10 | 11 | namespace player 12 | { 13 | namespace action 14 | { 15 | template 16 | struct base { 17 | T value {}; 18 | u32 id {}; 19 | }; 20 | template<> 21 | struct base { 22 | u32 id {}; 23 | }; 24 | 25 | struct source : base {}; 26 | struct play : base {}; 27 | struct pause : base {}; 28 | struct stop : base {}; 29 | struct seek : base {}; 30 | 31 | using info = std::variant; 32 | } // namespace action 33 | // 34 | using Action = qcm::Sender; 35 | 36 | class Player::Private { 37 | public: 38 | C_DECLARE_PUBLIC(Player, m_q) 39 | using action_channel_type = 40 | asio::experimental::concurrent_channel; 42 | 43 | Private(std::string_view name, Notifier notifier, asio::thread_pool::executor_type exc, 44 | std::pmr::memory_resource* mem); 45 | ~Private(); 46 | 47 | void play(); 48 | void pause(); 49 | void stop(); 50 | void seek(i32); 51 | 52 | void set_source(std::string_view); 53 | 54 | private: 55 | Player* m_q; 56 | Notifier m_notifier; 57 | 58 | rc m_action_channel; 59 | std::atomic m_action_id; 60 | bool m_end; 61 | 62 | rc m_reader; 63 | up m_dec; 64 | up m_dev; 65 | rc m_ctx; 66 | }; 67 | 68 | } // namespace player 69 | -------------------------------------------------------------------------------- /player/resampler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ffmpeg_frame.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | extern "C" { 9 | #include 10 | #include 11 | } 12 | 13 | #include "audio_stream_params.h" 14 | #include "audio_frame.h" 15 | #include "ffmpeg_error.h" 16 | 17 | namespace player 18 | { 19 | 20 | class Resampler { 21 | public: 22 | using Self = Resampler; 23 | Resampler(): m_ctx(swr_alloc()) {} 24 | ~Resampler() { swr_free(&m_ctx); } 25 | 26 | FFmpegError configure(AudioFrame& out, const AudioFrame& in) { 27 | FFmpegError err = swr_config_frame(m_ctx, out.ff.raw(), in.ff.raw()); 28 | if (err) return err.record(); 29 | err = swr_init(m_ctx); 30 | return err.record(); 31 | } 32 | 33 | FFmpegError resampler(AudioFrame& out, const AudioFrame& in) { 34 | if (out.ff->pts != AV_NOPTS_VALUE) { 35 | double multiple_base = out.ff->sample_rate * in.ff->sample_rate; 36 | double inpts = in.ff->pts * av_q2d(in.ff->time_base); 37 | double outpts = next_pts(inpts * multiple_base) / (double)multiple_base; 38 | out.ff->pts = outpts / av_q2d(in.ff->time_base); 39 | } else { 40 | out.ff->pts = AV_NOPTS_VALUE; 41 | } 42 | return FFmpegError(swr_convert_frame(m_ctx, out.ff.raw(), in.ff.raw())).record(); 43 | } 44 | 45 | FFmpegError remaining(AudioFrame& out) { 46 | return FFmpegError(swr_convert_frame(m_ctx, out.ff.raw(), NULL)).record(); 47 | } 48 | 49 | bool is_inited() { return swr_is_initialized(m_ctx); } 50 | 51 | void close() { swr_close(m_ctx); } 52 | 53 | i64 get_delay() { 54 | return swr_get_delay(m_ctx, 1000); 55 | } 56 | 57 | i64 next_pts(i64 t = INT64_MIN) { 58 | if (! is_inited()) t = INT64_MIN; 59 | return swr_next_pts(m_ctx, t); 60 | } 61 | 62 | private: 63 | SwrContext* m_ctx; 64 | }; 65 | 66 | } // namespace player 67 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(gtest_force_shared_crt ON) 2 | find_package(GTest QUIET) 3 | if(NOT GTest_FOUND) 4 | include(FetchContent) 5 | FetchContent_Declare( 6 | googletest 7 | GIT_REPOSITORY https://github.com/google/googletest.git 8 | GIT_TAG release-1.12.1) 9 | FetchContent_MakeAvailable(googletest) 10 | endif() 11 | 12 | add_executable(qcm_test src/asio.cpp src/main.cpp src/log.cpp) 13 | 14 | target_link_libraries(qcm_test PRIVATE core.asio GTest::gtest_main) 15 | 16 | include(GoogleTest) 17 | gtest_discover_tests(qcm_test) 18 | -------------------------------------------------------------------------------- /test/src/asio.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "core/core.h" 4 | using namespace rstd; 5 | 6 | TEST(Asio, Basic) {} -------------------------------------------------------------------------------- /test/src/log.cpp: -------------------------------------------------------------------------------- 1 | module; 2 | #include "core/log.h" 3 | module qcm.log; 4 | 5 | QCM_LOG_IMPL 6 | -------------------------------------------------------------------------------- /test/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(int argc, char** argv) { 4 | testing::InitGoogleTest(); 5 | return RUN_ALL_TESTS(); 6 | } -------------------------------------------------------------------------------- /third_party/qr_code/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(qr_code STATIC include/qr_code/qrcodegen.hpp qrcodegen.cpp) 2 | 3 | target_include_directories( 4 | qr_code 5 | PUBLIC include 6 | PRIVATE include/qr_code) 7 | --------------------------------------------------------------------------------