├── android
└── app
│ ├── .gitignore
│ ├── src
│ └── main
│ │ ├── res
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ ├── arrays.xml
│ │ │ └── theme.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── values-night
│ │ │ └── theme.xml
│ │ └── drawable
│ │ │ ├── ic_indeterminate_check_box_24dp.xml
│ │ │ ├── ic_highlight_24dp.xml
│ │ │ ├── ic_check_box_24dp.xml
│ │ │ ├── ic_error_outline_24dp.xml
│ │ │ ├── ic_palette_24dp.xml
│ │ │ └── ic_menu.xml
│ │ ├── kotlin
│ │ └── net
│ │ │ └── subroh0508
│ │ │ └── colormaster
│ │ │ └── androidapp
│ │ │ ├── themes
│ │ │ ├── Shape.kt
│ │ │ ├── Color.kt
│ │ │ ├── Type.kt
│ │ │ └── Theme.kt
│ │ │ ├── ColorMasterApplication.kt
│ │ │ ├── routes.kt
│ │ │ ├── components
│ │ │ ├── atoms
│ │ │ │ ├── DrawerHeader.kt
│ │ │ │ ├── Tab.kt
│ │ │ │ ├── DrawerButton.kt
│ │ │ │ └── ColorItemContent.kt
│ │ │ ├── molecules
│ │ │ │ └── ScrollableTabs.kt
│ │ │ └── organisms
│ │ │ │ └── StaticColorLists.kt
│ │ │ └── pages
│ │ │ └── activity
│ │ │ └── HomeActivity.kt
│ │ └── AndroidManifest.xml
│ ├── proguard-rules.pro
│ ├── build.gradle.kts
│ └── google-services.json
├── .dockerignore
├── core
├── common
│ ├── src
│ │ ├── androidMain
│ │ │ ├── AndroidManifest.xml
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── common
│ │ │ │ ├── extensions
│ │ │ │ └── AndroidColor.kt
│ │ │ │ └── ui
│ │ │ │ └── AndroidAppPreference.kt
│ │ ├── commonMain
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── common
│ │ │ │ ├── ui
│ │ │ │ ├── ThemeType.kt
│ │ │ │ ├── Languages.kt
│ │ │ │ └── AppPreference.kt
│ │ │ │ ├── LocalKoinApp.kt
│ │ │ │ └── model
│ │ │ │ └── LoadState.kt
│ │ ├── jsMain
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── common
│ │ │ │ ├── external
│ │ │ │ ├── Function.kt
│ │ │ │ └── CommonJs.kt
│ │ │ │ └── firebase.kt
│ │ └── commonTest
│ │ │ └── kotlin
│ │ │ └── net
│ │ │ └── subroh0508
│ │ │ └── colormaster
│ │ │ └── common
│ │ │ └── DateNumSpec.kt
│ └── build.gradle.kts
├── data
│ ├── src
│ │ ├── androidMain
│ │ │ ├── AndroidManifest.xml
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── data
│ │ │ │ ├── di
│ │ │ │ └── AuthRepositories.android.kt
│ │ │ │ └── DefaultAuthRepository.android.kt
│ │ ├── commonMain
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── data
│ │ │ │ ├── di
│ │ │ │ ├── AuthRepositories.kt
│ │ │ │ ├── LiveRepositories.kt
│ │ │ │ ├── PreviewRepositories.kt
│ │ │ │ ├── MyIdolsRepositories.kt
│ │ │ │ ├── IdolColorsRepositories.kt
│ │ │ │ └── DataModule.kt
│ │ │ │ ├── extension
│ │ │ │ ├── ImasparqlClient.kt
│ │ │ │ └── Converter.kt
│ │ │ │ ├── DefaultAuthRepository.kt
│ │ │ │ ├── DefaultPreviewRepository.kt
│ │ │ │ ├── DefaultLiveRepository.kt
│ │ │ │ └── DefaultMyIdolsRepository.kt
│ │ ├── commonTest
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── data
│ │ │ │ ├── extension
│ │ │ │ ├── AuthClient.kt
│ │ │ │ └── UserDocument.kt
│ │ │ │ ├── mock
│ │ │ │ ├── headers.kt
│ │ │ │ ├── MockIdolSearchApi.kt
│ │ │ │ └── MockSuggestLiveApi.kt
│ │ │ │ └── module
│ │ │ │ ├── LiveRepositoryModule.kt
│ │ │ │ ├── AuthRepositoryModule.kt
│ │ │ │ ├── PreviewRepositoryModule.kt
│ │ │ │ ├── MyIdolsRepositoryModule.kt
│ │ │ │ └── IdolColorsRepositoryModule.kt
│ │ ├── jsTest
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── data
│ │ │ │ ├── extension
│ │ │ │ ├── AuthClient.js.kt
│ │ │ │ └── UserDocument.js.kt
│ │ │ │ └── spec
│ │ │ │ └── DefaultAuthRepositorySpec.js.kt
│ │ ├── androidUnitTest
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── data
│ │ │ │ ├── extension
│ │ │ │ ├── AuthClient.android.kt
│ │ │ │ └── UserDocument.android.kt
│ │ │ │ └── spec
│ │ │ │ └── DefaultAuthRepositorySpec.android.kt
│ │ └── jsMain
│ │ │ └── kotlin
│ │ │ └── net
│ │ │ └── subroh0508
│ │ │ └── colormaster
│ │ │ └── data
│ │ │ ├── di
│ │ │ └── AuthRepositories.js.kt
│ │ │ └── DefaultAuthRepository.js.kt
│ └── build.gradle.kts
├── model
│ ├── src
│ │ ├── androidMain
│ │ │ ├── AndroidManifest.xml
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── model
│ │ │ │ └── auth
│ │ │ │ └── AuthRepository.android.kt
│ │ ├── commonMain
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── model
│ │ │ │ ├── auth
│ │ │ │ ├── AuthRepository.kt
│ │ │ │ ├── CredentialProvider.kt
│ │ │ │ └── CurrentUser.kt
│ │ │ │ ├── MyIdolsRepository.kt
│ │ │ │ ├── PreviewRepository.kt
│ │ │ │ ├── LiveRepository.kt
│ │ │ │ ├── Inlines.kt
│ │ │ │ ├── Types.kt
│ │ │ │ ├── Brands.kt
│ │ │ │ ├── IdolColor.kt
│ │ │ │ └── IdolColorsRepository.kt
│ │ └── jsMain
│ │ │ └── kotlin
│ │ │ └── net
│ │ │ └── subroh0508
│ │ │ └── colormaster
│ │ │ └── model
│ │ │ └── auth
│ │ │ └── AuthRepository.js.kt
│ └── build.gradle.kts
├── test
│ ├── src
│ │ ├── androidMain
│ │ │ ├── AndroidManifest.xml
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── test
│ │ │ │ └── fake
│ │ │ │ └── FakeAuthClient.android.kt
│ │ ├── commonMain
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── test
│ │ │ │ ├── MockHttpClient.kt
│ │ │ │ ├── model
│ │ │ │ └── CurrentUser.kt
│ │ │ │ ├── data
│ │ │ │ └── FirebaseUser.kt
│ │ │ │ ├── fake
│ │ │ │ ├── FakeAuthClient.kt
│ │ │ │ └── FakeFirestoreClient.kt
│ │ │ │ └── extension
│ │ │ │ └── Flow.kt
│ │ └── jsMain
│ │ │ └── kotlin
│ │ │ └── net
│ │ │ └── subroh0508
│ │ │ └── colormaster
│ │ │ └── test
│ │ │ └── fake
│ │ │ └── FakeAuthClient.js.kt
│ └── build.gradle.kts
├── features
│ ├── home
│ │ ├── src
│ │ │ ├── androidMain
│ │ │ │ ├── AndroidManifest.xml
│ │ │ │ └── kotlin
│ │ │ │ │ └── net
│ │ │ │ │ └── subroh0508
│ │ │ │ │ └── colormaster
│ │ │ │ │ └── features
│ │ │ │ │ └── home
│ │ │ │ │ ├── viewmodel
│ │ │ │ │ └── AuthViewModel.android.kt
│ │ │ │ │ └── AndroidSignInUseCase.kt
│ │ │ ├── commonMain
│ │ │ │ └── kotlin
│ │ │ │ │ └── net
│ │ │ │ │ └── subroh0508
│ │ │ │ │ └── colormaster
│ │ │ │ │ └── features
│ │ │ │ │ └── home
│ │ │ │ │ ├── viewmodel
│ │ │ │ │ ├── AuthViewModel.kt
│ │ │ │ │ ├── AuthUiState.kt
│ │ │ │ │ └── CommonAuthViewModel.kt
│ │ │ │ │ ├── SignInUseCase.kt
│ │ │ │ │ └── SignOutUseCase.kt
│ │ │ └── jsMain
│ │ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── features
│ │ │ │ └── home
│ │ │ │ ├── viewmodel
│ │ │ │ └── AuthViewModel.js.kt
│ │ │ │ ├── SubscribeCurrentUserUseCase.kt
│ │ │ │ └── JsSignInUseCase.kt
│ │ └── build.gradle.kts
│ ├── myidols
│ │ ├── src
│ │ │ ├── androidMain
│ │ │ │ └── AndroidManifest.xml
│ │ │ └── commonMain
│ │ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── features
│ │ │ │ └── myidols
│ │ │ │ └── viewmodel
│ │ │ │ ├── MyIdolsUiState.kt
│ │ │ │ └── MyIdolsViewModel.kt
│ │ └── build.gradle.kts
│ ├── preview
│ │ ├── src
│ │ │ ├── androidMain
│ │ │ │ └── AndroidManifest.xml
│ │ │ └── commonMain
│ │ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── features
│ │ │ │ └── preview
│ │ │ │ ├── viewmodel
│ │ │ │ ├── PenlightUiState.kt
│ │ │ │ └── PenlightViewModel.kt
│ │ │ │ └── FetchIdolsUseCase.kt
│ │ └── build.gradle.kts
│ └── search
│ │ ├── src
│ │ ├── androidMain
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── res
│ │ │ │ └── values
│ │ │ │ │ └── strings.xml
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── features
│ │ │ │ └── search
│ │ │ │ └── model
│ │ │ │ └── SearchByTab.kt
│ │ ├── commonMain
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── features
│ │ │ │ └── search
│ │ │ │ ├── model
│ │ │ │ ├── SearchByTab.kt
│ │ │ │ ├── SearchState.kt
│ │ │ │ └── LiveNameQuery.kt
│ │ │ │ └── viewmodel
│ │ │ │ ├── SuggestLiveNameUiState.kt
│ │ │ │ ├── SearchIdolsUiState.kt
│ │ │ │ └── SuggestLiveNameViewModel.kt
│ │ └── jsMain
│ │ │ └── kotlin
│ │ │ └── net
│ │ │ └── subroh0508
│ │ │ └── colormaster
│ │ │ └── features
│ │ │ └── search
│ │ │ └── model
│ │ │ └── SearchByTab.kt
│ │ └── build.gradle.kts
└── network
│ ├── auth
│ ├── src
│ │ ├── androidMain
│ │ │ ├── AndroidManifest.xml
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── network
│ │ │ │ └── auth
│ │ │ │ ├── AuthClient.android.kt
│ │ │ │ └── internal
│ │ │ │ └── AuthClientImpl.android.kt
│ │ ├── commonMain
│ │ │ └── kotlin
│ │ │ │ └── net
│ │ │ │ └── subroh0508
│ │ │ │ └── colormaster
│ │ │ │ └── network
│ │ │ │ └── auth
│ │ │ │ ├── model
│ │ │ │ ├── FirebaseUser.kt
│ │ │ │ └── Provider.kt
│ │ │ │ ├── di
│ │ │ │ └── Auth.kt
│ │ │ │ └── AuthClient.kt
│ │ └── jsMain
│ │ │ └── kotlin
│ │ │ └── net
│ │ │ └── subroh0508
│ │ │ └── colormaster
│ │ │ └── network
│ │ │ └── auth
│ │ │ └── AuthClient.js.kt
│ └── build.gradle.kts
│ ├── firestore
│ ├── src
│ │ ├── androidMain
│ │ │ └── AndroidManifest.xml
│ │ └── commonMain
│ │ │ └── kotlin
│ │ │ └── net
│ │ │ └── subroh0508
│ │ │ └── colormaster
│ │ │ └── network
│ │ │ └── firestore
│ │ │ ├── collections.kt
│ │ │ ├── document
│ │ │ └── UserDocument.kt
│ │ │ ├── FirestoreClient.kt
│ │ │ ├── di
│ │ │ └── Firestore.kt
│ │ │ └── internal
│ │ │ └── FirestoreClientImpl.kt
│ └── build.gradle.kts
│ └── imasparql
│ ├── src
│ ├── androidMain
│ │ ├── AndroidManifest.xml
│ │ └── kotlin
│ │ │ └── net
│ │ │ └── subroh0508
│ │ │ └── colormaster
│ │ │ └── network
│ │ │ └── imasparql
│ │ │ ├── internal
│ │ │ ├── AndroidURLEncoder.kt
│ │ │ └── AndroidConstants.kt
│ │ │ └── di
│ │ │ └── AndroidHttpClient.kt
│ ├── commonMain
│ │ └── kotlin
│ │ │ └── net
│ │ │ └── subroh0508
│ │ │ └── colormaster
│ │ │ └── network
│ │ │ └── imasparql
│ │ │ ├── CommonConstants.kt
│ │ │ ├── internal
│ │ │ ├── URLEncoder.kt
│ │ │ ├── ContentType.kt
│ │ │ ├── CommonConstants.kt
│ │ │ └── ImasparqlApiClient.kt
│ │ │ ├── json
│ │ │ ├── LiveNameJson.kt
│ │ │ └── IdolColorJson.kt
│ │ │ ├── ImasparqlClient.kt
│ │ │ ├── serializer
│ │ │ └── Response.kt
│ │ │ ├── query
│ │ │ ├── SuggestLiveQuery.kt
│ │ │ ├── RandomQuery.kt
│ │ │ ├── SearchByIdQuery.kt
│ │ │ ├── SearchByLiveQuery.kt
│ │ │ ├── ImasparqlQuery.kt
│ │ │ └── SearchByNameQuery.kt
│ │ │ └── di
│ │ │ └── Api.kt
│ └── jsMain
│ │ └── kotlin
│ │ └── net
│ │ └── subroh0508
│ │ └── colormaster
│ │ └── network
│ │ └── imasparql
│ │ ├── internal
│ │ └── JsURLEncoder.kt
│ │ └── di
│ │ └── JsHttpClient.kt
│ └── build.gradle.kts
├── .firebaserc
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── js
├── app
│ ├── webpack.config.d
│ │ ├── 01.resources.js
│ │ ├── 00.path.js
│ │ ├── 04.devServer.js
│ │ ├── 02.sass.js
│ │ └── 03.constants.js
│ └── src
│ │ └── jsMain
│ │ ├── resources
│ │ └── sign_in_with_google.png
│ │ └── kotlin
│ │ ├── utilities
│ │ ├── events.kt
│ │ ├── mobile.kt
│ │ ├── constants.kt
│ │ ├── ResizeObserver.kt
│ │ ├── Error.kt
│ │ └── decompose.kt
│ │ ├── components
│ │ ├── atoms
│ │ │ ├── tooltip
│ │ │ │ └── Tooltip.kt
│ │ │ ├── topappbar
│ │ │ │ └── TopAppActionIcon.kt
│ │ │ ├── chip
│ │ │ │ └── ChipGroup.kt
│ │ │ ├── checkbox
│ │ │ │ └── CheckBoxGroup.kt
│ │ │ ├── textfield
│ │ │ │ ├── DebouncedTextForm.kt
│ │ │ │ └── OutlinedTextField.kt
│ │ │ ├── menu
│ │ │ │ └── MenuButton.kt
│ │ │ └── list
│ │ │ │ └── ListGroupSubHeader.kt
│ │ ├── organisms
│ │ │ ├── list
│ │ │ │ └── common.kt
│ │ │ ├── drawer
│ │ │ │ └── DrawerHeader.kt
│ │ │ └── box
│ │ │ │ └── form
│ │ │ │ └── BrandForm.kt
│ │ ├── molecules
│ │ │ ├── TopAppBar.kt
│ │ │ └── icon
│ │ │ │ └── ActionIcons.kt
│ │ └── templates
│ │ │ └── Routing.kt
│ │ ├── page
│ │ ├── MyIdolsPage.kt
│ │ ├── about
│ │ │ ├── TermsPage.kt
│ │ │ └── DevelopmentPage.kt
│ │ └── SearchIdolPage.kt
│ │ └── main.kt
└── material
│ └── src
│ └── jsMain
│ └── kotlin
│ └── material
│ ├── externals
│ ├── MDCTab.kt
│ ├── MDCList.kt
│ ├── MDCTabBar.kt
│ ├── MDCTextField.kt
│ ├── MDCMenu.kt
│ ├── MDCTopAppBar.kt
│ ├── MDCTextFieldIcon.kt
│ ├── MDCTooltip.kt
│ ├── MDCDrawer.kt
│ ├── MDCFormField.kt
│ ├── MDCRipple.kt
│ └── MDCCheckbox.kt
│ ├── utilities
│ ├── TagElementBuilder.kt
│ └── MediaQuery.kt
│ └── components
│ ├── Ripple.kt
│ ├── Tooltip.kt
│ ├── Icon.kt
│ ├── Menu.kt
│ └── Card.kt
├── renovate.json
├── .gitignore
├── gradle.properties
├── plugins
├── settings.gradle.kts
└── src
│ └── main
│ └── kotlin
│ └── net
│ └── subroh0508
│ └── colormaster
│ ├── primitive
│ ├── compose
│ │ └── ComposeDsl.kt
│ ├── android
│ │ ├── Android.kt
│ │ └── AndroidDsl.kt
│ └── kmp
│ │ ├── KmpDsl.kt
│ │ ├── KmpJsPlugin.kt
│ │ ├── KmpAndroidPlugin.kt
│ │ └── KmpPlugin.kt
│ ├── VersionCatalogDsl.kt
│ └── convention
│ ├── ModelModulePlugin.kt
│ ├── ApiModulePlugin.kt
│ ├── DataModulePlugin.kt
│ ├── AndroidAppModulePlugin.kt
│ └── CommonModulePlugin.kt
├── backend
├── cli
│ ├── src
│ │ └── main
│ │ │ └── kotlin
│ │ │ └── net
│ │ │ └── subroh0508
│ │ │ └── colormaster
│ │ │ └── backend
│ │ │ └── cli
│ │ │ └── imasparql
│ │ │ ├── URLEncoder.kt
│ │ │ ├── Constants.kt
│ │ │ ├── ImasparqlClient.kt
│ │ │ ├── json
│ │ │ └── IdolColorJson.kt
│ │ │ ├── serializer
│ │ │ └── Response.kt
│ │ │ ├── ImasparqlApiClient.kt
│ │ │ └── query
│ │ │ └── ImasparqlQuery.kt
│ └── build.gradle.kts
└── server
│ ├── src
│ └── main
│ │ ├── resources
│ │ └── logback.xml
│ │ ├── kotlin
│ │ └── net
│ │ │ └── subroh0508
│ │ │ └── colormaster
│ │ │ └── backend
│ │ │ └── model
│ │ │ └── IdolDto.kt
│ │ └── sqldelight
│ │ └── net
│ │ └── subroh0508
│ │ └── colormaster
│ │ └── backend
│ │ └── database
│ │ └── Idol.sq
│ ├── README.md
│ └── build.gradle.kts
├── firebase.json
├── .github
└── workflows
│ └── web-build-and-deploy.yml
├── Dockerfile
└── settings.gradle.kts
/android/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | build
3 | *.db
4 | data/
5 |
--------------------------------------------------------------------------------
/core/common/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/data/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/model/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/test/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/features/home/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/features/myidols/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/features/preview/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/features/search/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/network/auth/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/network/firestore/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "imas-colormaster"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subroh0508/colormaster/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/js/app/webpack.config.d/01.resources.js:
--------------------------------------------------------------------------------
1 | config.resolve.modules.unshift(path.resolve(webAppPath, 'build/processedResources/js/main'));
2 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/core/model/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("colormaster.convention.model")
3 | }
4 |
5 | android { namespace = "net.subroh0508.colormaster.model" }
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subroh0508/colormaster/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subroh0508/colormaster/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/js/app/src/jsMain/resources/sign_in_with_google.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subroh0508/colormaster/HEAD/js/app/src/jsMain/resources/sign_in_with_google.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subroh0508/colormaster/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subroh0508/colormaster/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subroh0508/colormaster/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subroh0508/colormaster/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subroh0508/colormaster/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subroh0508/colormaster/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subroh0508/colormaster/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/core/features/home/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("colormaster.convention.feature")
3 | }
4 |
5 | android { namespace = "net.subroh0508.colormaster.features.home" }
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subroh0508/colormaster/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/core/features/myidols/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("colormaster.convention.feature")
3 | }
4 |
5 | android { namespace = "net.subroh0508.colormaster.features.myidols" }
6 |
--------------------------------------------------------------------------------
/core/features/preview/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("colormaster.convention.feature")
3 | }
4 |
5 | android { namespace = "net.subroh0508.colormaster.features.preview" }
6 |
--------------------------------------------------------------------------------
/core/features/search/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("colormaster.convention.feature")
3 | }
4 |
5 | android { namespace = "net.subroh0508.colormaster.features.search" }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | *.db
3 | .gradle
4 | .idea
5 | .kotlin
6 | /local.properties
7 | .DS_Store
8 | build
9 | /public
10 | /captures
11 | .externalNativeBuild
12 | /node_modules
13 |
--------------------------------------------------------------------------------
/core/common/src/commonMain/kotlin/net/subroh0508/colormaster/common/ui/ThemeType.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.common.ui
2 |
3 | enum class ThemeType {
4 | DAY, NIGHT
5 | }
6 |
--------------------------------------------------------------------------------
/core/features/search/src/androidMain/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | アイドル名
3 | 公演名
4 |
5 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/utilities/events.kt:
--------------------------------------------------------------------------------
1 | package utilities
2 |
3 | import org.w3c.dom.HTMLInputElement
4 | import org.w3c.dom.events.Event
5 |
6 | fun Event.inputTarget() = target as HTMLInputElement
7 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/CommonConstants.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql
2 |
3 | const val HOSTNAME = "sparql.crssnky.xyz"
4 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/utilities/mobile.kt:
--------------------------------------------------------------------------------
1 | package utilities
2 |
3 | import kotlinx.browser.window
4 |
5 | val isMobile: Boolean get() = """(iPhone|iPad|Android)""".toRegex().matches(window.navigator.userAgent)
6 |
--------------------------------------------------------------------------------
/core/network/firestore/src/commonMain/kotlin/net/subroh0508/colormaster/network/firestore/collections.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.firestore
2 |
3 | internal const val COLLECTION_USERS = "users"
4 |
5 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xms2048m -Xmx4096m
2 |
3 | kotlin.code.style=official
4 |
5 | android.useAndroidX=true
6 | android.enableJetifier=true
7 |
8 | org.jetbrains.compose.experimental.jscanvas.enabled=true
9 |
--------------------------------------------------------------------------------
/core/features/home/src/commonMain/kotlin/net/subroh0508/colormaster/features/home/viewmodel/AuthViewModel.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.home.viewmodel
2 |
3 | expect class AuthViewModel : CommonAuthViewModel
4 |
--------------------------------------------------------------------------------
/core/features/search/src/commonMain/kotlin/net/subroh0508/colormaster/features/search/model/SearchByTab.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.search.model
2 |
3 | expect enum class SearchByTab {
4 | BY_NAME, BY_LIVE
5 | }
6 |
--------------------------------------------------------------------------------
/core/features/search/src/commonMain/kotlin/net/subroh0508/colormaster/features/search/model/SearchState.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.search.model
2 |
3 | enum class SearchState {
4 | RANDOM, SEARCHED, WAITING, ERROR
5 | }
6 |
--------------------------------------------------------------------------------
/core/data/src/commonMain/kotlin/net/subroh0508/colormaster/data/di/AuthRepositories.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.di
2 |
3 | import org.koin.core.module.Module
4 |
5 | expect object AuthRepositories {
6 | val Module: Module
7 | }
8 |
--------------------------------------------------------------------------------
/core/network/auth/src/commonMain/kotlin/net/subroh0508/colormaster/network/auth/model/FirebaseUser.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.auth.model
2 |
3 | data class FirebaseUser(
4 | val uid: String,
5 | val providers: List,
6 | )
7 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/internal/URLEncoder.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.internal
2 |
3 | internal expect object URLEncoder {
4 | fun encode(s: String): String
5 | }
6 |
--------------------------------------------------------------------------------
/js/app/webpack.config.d/00.path.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const rootPath = path.resolve(__dirname, '../../../../')
4 |
5 | const webAppPath = path.resolve(rootPath, 'js/app');
6 | const nodeModulePath = path.resolve(rootPath, 'build/js/node_modules');
7 |
--------------------------------------------------------------------------------
/core/common/src/androidMain/kotlin/net/subroh0508/colormaster/common/extensions/AndroidColor.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.common.extensions
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | fun Triple.toColor() = let { (r, g, b) -> Color(r, g, b) }
6 |
--------------------------------------------------------------------------------
/core/data/src/commonTest/kotlin/net/subroh0508/colormaster/data/extension/AuthClient.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.extension
2 |
3 | import net.subroh0508.colormaster.network.auth.AuthClient
4 |
5 | expect suspend fun signInWithGoogle(
6 | client: AuthClient,
7 | )
8 |
--------------------------------------------------------------------------------
/plugins/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositories {
3 | mavenCentral()
4 | }
5 |
6 | versionCatalogs {
7 | create("libs") {
8 | from(files("../gradle/libs.versions.toml"))
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/backend/cli/src/main/kotlin/net/subroh0508/colormaster/backend/cli/imasparql/URLEncoder.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.backend.cli.imasparql
2 |
3 | import java.net.URLEncoder
4 |
5 | object URLEncoder {
6 | fun encode(s: String): String = URLEncoder.encode(s, "UTF-8")
7 | }
8 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/components/atoms/tooltip/Tooltip.kt:
--------------------------------------------------------------------------------
1 | package components.atoms.tooltip
2 |
3 | import androidx.compose.runtime.Composable
4 | import material.components.Tooltip as MaterialTooltip
5 |
6 | @Composable
7 | fun Tooltip(id: String, text: String) = MaterialTooltip(id, text, 0L, 0L)
8 |
--------------------------------------------------------------------------------
/core/data/src/jsTest/kotlin/net/subroh0508/colormaster/data/extension/AuthClient.js.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.extension
2 |
3 | import net.subroh0508.colormaster.network.auth.AuthClient
4 |
5 | actual suspend fun signInWithGoogle(
6 | client: AuthClient,
7 | ) = client.signInWithGoogle()
8 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/backend/cli/src/main/kotlin/net/subroh0508/colormaster/backend/cli/imasparql/Constants.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.backend.cli.imasparql
2 |
3 | const val HOSTNAME = "sparql.crssnky.xyz"
4 | const val APP_NAME = "ColorM@ster"
5 | const val ESCAPED_ENDPOINT_RDFS_DETAIL = """https://$HOSTNAME/imasrdf/RDFs/detail/"""
6 |
--------------------------------------------------------------------------------
/core/model/src/commonMain/kotlin/net/subroh0508/colormaster/model/auth/AuthRepository.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.model.auth
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | expect interface AuthRepository {
6 | fun getCurrentUserStream(): Flow
7 |
8 | suspend fun signOut()
9 | }
10 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #ffcc80
4 |
5 | #bf360c
6 |
7 | #121212
8 | #FAFAFA
9 |
10 |
--------------------------------------------------------------------------------
/core/data/src/androidUnitTest/kotlin/net/subroh0508/colormaster/data/extension/AuthClient.android.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.extension
2 |
3 | import net.subroh0508.colormaster.network.auth.AuthClient
4 |
5 | actual suspend fun signInWithGoogle(
6 | client: AuthClient,
7 | ) = client.signInWithGoogle("idToken")
8 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/json/LiveNameJson.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.json
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class LiveNameJson internal constructor(
7 | val name: Map,
8 | )
9 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/androidMain/kotlin/net/subroh0508/colormaster/network/imasparql/internal/AndroidURLEncoder.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.internal
2 |
3 | import java.net.URLEncoder
4 |
5 | internal actual object URLEncoder {
6 | actual fun encode(s: String): String = URLEncoder.encode(s, "UTF-8")
7 | }
8 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/components/organisms/list/common.kt:
--------------------------------------------------------------------------------
1 | package components.organisms.list
2 |
3 | const val GRID_MIN_WIDTH = 216
4 | const val GRID_MARGIN_HORIZONTAL = 8
5 |
6 | fun buildSelections(
7 | selections: List,
8 | id: String,
9 | selected: Boolean,
10 | ) = if (selected) selections + id else selections - id
11 |
--------------------------------------------------------------------------------
/core/data/src/commonTest/kotlin/net/subroh0508/colormaster/data/mock/headers.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.mock
2 |
3 | import io.ktor.http.*
4 | import net.subroh0508.colormaster.network.imasparql.internal.ContentType
5 |
6 | internal val headers = headersOf(
7 | "Content-Type" to listOf(ContentType.Application.SparqlJson.toString())
8 | )
9 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/jsMain/kotlin/net/subroh0508/colormaster/network/imasparql/internal/JsURLEncoder.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.internal
2 |
3 | private external fun encodeURIComponent(s: String): String
4 |
5 | internal actual object URLEncoder {
6 | actual fun encode(s: String): String = encodeURIComponent(s)
7 | }
8 |
--------------------------------------------------------------------------------
/plugins/src/main/kotlin/net/subroh0508/colormaster/primitive/compose/ComposeDsl.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.primitive.compose
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.kotlin.dsl.get
5 | import org.jetbrains.compose.ComposeExtension
6 |
7 | val Project.compose: ComposeExtension
8 | get() = extensions["compose"] as ComposeExtension
9 |
--------------------------------------------------------------------------------
/core/model/src/commonMain/kotlin/net/subroh0508/colormaster/model/MyIdolsRepository.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.model
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface MyIdolsRepository {
6 | fun getInChargeOfIdolsStream(lang: String): Flow>
7 |
8 | fun getFavoriteIdolsStream(lang: String): Flow>
9 | }
10 |
--------------------------------------------------------------------------------
/core/model/src/commonMain/kotlin/net/subroh0508/colormaster/model/PreviewRepository.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.model
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface PreviewRepository {
6 | fun getPreviewColorsStream(): Flow>
7 |
8 | fun clear()
9 |
10 | suspend fun show(ids: List, lang: String)
11 | }
12 |
--------------------------------------------------------------------------------
/core/model/src/commonMain/kotlin/net/subroh0508/colormaster/model/auth/CredentialProvider.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.model.auth
2 |
3 | sealed class CredentialProvider {
4 | data object Anonymous : CredentialProvider()
5 | data class Google(val email: String) : CredentialProvider()
6 | data class Twitter(val displayName: String) : CredentialProvider()
7 | }
8 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/internal/ContentType.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.internal
2 |
3 | import io.ktor.http.ContentType
4 |
5 | abstract class ContentType {
6 | object Application {
7 | val SparqlJson = ContentType("application", "sparql-results+json")
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/core/network/firestore/src/commonMain/kotlin/net/subroh0508/colormaster/network/firestore/document/UserDocument.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.firestore.document
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class UserDocument(
7 | val inCharges: List = listOf(),
8 | val favorites: List = listOf(),
9 | )
10 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/internal/CommonConstants.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.internal
2 |
3 | import net.subroh0508.colormaster.network.imasparql.HOSTNAME
4 |
5 | internal const val APP_NAME = "ColorM@ster"
6 | internal const val ESCAPED_ENDPOINT_RDFS_DETAIL = """https://$HOSTNAME/imasrdf/RDFs/detail/"""
7 |
--------------------------------------------------------------------------------
/core/test/src/commonMain/kotlin/net/subroh0508/colormaster/test/MockHttpClient.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.test
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.engine.mock.*
5 | import io.ktor.client.request.*
6 |
7 | fun mockHttpClient(
8 | handler: MockRequestHandler = { respondOk() },
9 | ) = HttpClient(MockEngine) {
10 | engine { addHandler(handler) }
11 | }
12 |
--------------------------------------------------------------------------------
/core/features/home/src/commonMain/kotlin/net/subroh0508/colormaster/features/home/viewmodel/AuthUiState.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.home.viewmodel
2 |
3 | import net.subroh0508.colormaster.model.auth.CurrentUser
4 |
5 | sealed interface AuthUiState {
6 | data object NotSignedIn : AuthUiState
7 | data class SignedIn(
8 | val user: CurrentUser,
9 | ) : AuthUiState
10 | }
11 |
--------------------------------------------------------------------------------
/core/network/firestore/src/commonMain/kotlin/net/subroh0508/colormaster/network/firestore/FirestoreClient.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.firestore
2 |
3 | import net.subroh0508.colormaster.network.firestore.document.UserDocument
4 |
5 | interface FirestoreClient {
6 | suspend fun setUserDocument(uid: String, userDocument: UserDocument)
7 | suspend fun getUserDocument(uid: String?): UserDocument
8 | }
9 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/json/IdolColorJson.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.json
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class IdolColorJson internal constructor(
7 | val id: Map,
8 | val name: Map,
9 | val color: Map
10 | )
11 |
--------------------------------------------------------------------------------
/core/model/src/jsMain/kotlin/net/subroh0508/colormaster/model/auth/AuthRepository.js.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.model.auth
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | actual interface AuthRepository {
6 | actual fun getCurrentUserStream(): Flow
7 | actual suspend fun signOut()
8 |
9 | suspend fun signInWithGoogle()
10 | suspend fun signInWithGoogleForMobile()
11 | }
12 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/externals/MDCTab.kt:
--------------------------------------------------------------------------------
1 | package material.externals
2 |
3 | import org.w3c.dom.Element
4 |
5 | @JsModule("@material/tab")
6 | @JsNonModule
7 | private external object MDCTabModule {
8 | class MDCTab(root: Element?) : material.externals.MDCTab
9 | }
10 |
11 | external interface MDCTab
12 |
13 | @Suppress("FunctionName")
14 | fun MDCTab(root: Element?): MDCTab = MDCTabModule.MDCTab(root)
15 |
--------------------------------------------------------------------------------
/core/features/search/src/androidMain/kotlin/net/subroh0508/colormaster/features/search/model/SearchByTab.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.search.model
2 |
3 | import androidx.annotation.StringRes
4 | import net.subroh0508.colormaster.features.search.R
5 |
6 | actual enum class SearchByTab(@StringRes val labelRes: Int) {
7 | BY_NAME(R.string.search_by_tab_name),
8 | BY_LIVE(R.string.search_by_tab_live),
9 | }
10 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "public",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ],
9 | "rewrites": [
10 | {
11 | "source": "**",
12 | "destination": "/index.html"
13 | }
14 | ]
15 | },
16 | "emulators": {
17 | "hosting": {
18 | "port": 8081,
19 | "host": "0.0.0.0"
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/js/app/webpack.config.d/04.devServer.js:
--------------------------------------------------------------------------------
1 | if (config.mode === 'development') {
2 | config.devServer.historyApiFallback = true;
3 | config.devServer.host = '0.0.0.0';
4 | config.devServer.allowedHosts = 'all';
5 |
6 | // @see: https://github.com/JetBrains/kotlin/commit/9baa24e626b48ccfaacbf5724f58337cd29e80ab
7 | config.devServer.static = config.devServer.contentBase
8 | delete config.devServer.contentBase
9 | }
10 |
--------------------------------------------------------------------------------
/core/features/home/src/androidMain/kotlin/net/subroh0508/colormaster/features/home/viewmodel/AuthViewModel.android.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.home.viewmodel
2 |
3 | import net.subroh0508.colormaster.model.auth.AuthRepository
4 |
5 | actual class AuthViewModel(
6 | repository: AuthRepository,
7 | ) : CommonAuthViewModel(repository) {
8 | suspend fun signInWithGoogle(idToken: String) = repository.signInWithGoogle(idToken)
9 | }
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/externals/MDCList.kt:
--------------------------------------------------------------------------------
1 | package material.externals
2 |
3 | import org.w3c.dom.Element
4 |
5 | @JsModule("@material/list")
6 | @JsNonModule
7 | private external object MDCListModule {
8 | class MDCList(root: Element?) : material.externals.MDCList
9 | }
10 |
11 | external interface MDCList
12 |
13 | @Suppress("FunctionName")
14 | fun MDCList(root: Element?): MDCList = MDCListModule.MDCList(root)
15 |
--------------------------------------------------------------------------------
/backend/cli/src/main/kotlin/net/subroh0508/colormaster/backend/cli/imasparql/ImasparqlClient.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.backend.cli.imasparql
2 |
3 | import kotlinx.serialization.KSerializer
4 | import net.subroh0508.colormaster.backend.cli.imasparql.serializer.Response
5 |
6 | interface ImasparqlClient {
7 | suspend fun search(
8 | query: String,
9 | serializer: KSerializer,
10 | ): Response
11 | }
12 |
--------------------------------------------------------------------------------
/core/data/src/commonMain/kotlin/net/subroh0508/colormaster/data/di/LiveRepositories.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.di
2 |
3 | import net.subroh0508.colormaster.data.DefaultLiveRepository
4 | import net.subroh0508.colormaster.model.LiveRepository
5 | import org.koin.dsl.module
6 |
7 | object LiveRepositories {
8 | val Module get() = module {
9 | single { DefaultLiveRepository(get()) }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/utilities/constants.kt:
--------------------------------------------------------------------------------
1 | package utilities
2 |
3 | external val APP_NAME: String
4 | external val APP_VERSION: String
5 | external val API_KEY: String
6 | external val AUTH_DOMAIN: String
7 | external val DATABASE_URL: String
8 | external val PROJECT_ID: String
9 | external val STORAGE_BUCKET: String
10 | external val MESSAGING_SENDER_ID: String
11 | external val APP_ID: String
12 | external val MEASUREMENT_ID: String
13 |
--------------------------------------------------------------------------------
/core/common/src/commonMain/kotlin/net/subroh0508/colormaster/common/LocalKoinApp.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.common
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.compositionLocalOf
5 | import org.koin.dsl.koinApplication
6 |
7 | val koinApp = koinApplication { }
8 |
9 | val LocalKoinApp = compositionLocalOf { koinApp }
10 |
11 | @Composable
12 | fun CurrentLocalKoinApp() = LocalKoinApp.current
13 |
--------------------------------------------------------------------------------
/core/model/src/androidMain/kotlin/net/subroh0508/colormaster/model/auth/AuthRepository.android.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.model.auth
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | actual interface AuthRepository {
6 | actual fun getCurrentUserStream(): Flow
7 | actual suspend fun signOut()
8 |
9 | suspend fun fetchCurrentUser(): CurrentUser?
10 | suspend fun signInWithGoogle(idToken: String)
11 | }
12 |
--------------------------------------------------------------------------------
/core/model/src/commonMain/kotlin/net/subroh0508/colormaster/model/LiveRepository.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.model
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface LiveRepository {
6 | fun getLiveNamesStream(): Flow>
7 |
8 | suspend fun refresh()
9 |
10 | suspend fun suggest(dateRange: Pair): List
11 |
12 | suspend fun suggest(name: String?): List
13 | }
14 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/ImasparqlClient.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql
2 |
3 | import kotlinx.serialization.KSerializer
4 | import net.subroh0508.colormaster.network.imasparql.serializer.Response
5 |
6 | interface ImasparqlClient {
7 | suspend fun search(
8 | query: String,
9 | serializer: KSerializer,
10 | ): Response
11 | }
12 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/androidMain/kotlin/net/subroh0508/colormaster/network/imasparql/internal/AndroidConstants.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.internal
2 |
3 | import android.os.Build
4 | import net.subroh0508.colormaster.network.imasparql.BuildConfig
5 | import java.util.*
6 |
7 | internal val UserAgent = "$APP_NAME/${BuildConfig.VERSION_CODE} (Android ${Build.VERSION.SDK_INT}; ${Locale.getDefault().language}; ${Build.PRODUCT})"
8 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/subroh0508/colormaster/androidapp/themes/Shape.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.androidapp.themes
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
12 |
--------------------------------------------------------------------------------
/core/data/src/commonMain/kotlin/net/subroh0508/colormaster/data/di/PreviewRepositories.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.di
2 |
3 | import net.subroh0508.colormaster.data.DefaultPreviewRepository
4 | import net.subroh0508.colormaster.model.PreviewRepository
5 | import org.koin.dsl.module
6 |
7 | object PreviewRepositories {
8 | val Module get() = module {
9 | single { DefaultPreviewRepository(get()) }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/externals/MDCTabBar.kt:
--------------------------------------------------------------------------------
1 | package material.externals
2 |
3 | import org.w3c.dom.Element
4 |
5 | @JsModule("@material/tab-bar")
6 | @JsNonModule
7 | private external object MDCTabBarModule {
8 | class MDCTabBar(root: Element?) : material.externals.MDCTabBar
9 | }
10 |
11 | external interface MDCTabBar
12 |
13 | @Suppress("FunctionName")
14 | fun MDCTabBar(root: Element?): MDCTabBar = MDCTabBarModule.MDCTabBar(root)
15 |
--------------------------------------------------------------------------------
/core/features/preview/src/commonMain/kotlin/net/subroh0508/colormaster/features/preview/viewmodel/PenlightUiState.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.preview.viewmodel
2 |
3 | import net.subroh0508.colormaster.model.IdolColor
4 |
5 | sealed interface PenlightUiState {
6 | data object Loading : PenlightUiState
7 | data class Loaded(
8 | val idols: List,
9 | val withDescription: Boolean,
10 | ) : PenlightUiState
11 | }
--------------------------------------------------------------------------------
/backend/server/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/core/data/src/commonMain/kotlin/net/subroh0508/colormaster/data/di/MyIdolsRepositories.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.di
2 |
3 | import net.subroh0508.colormaster.data.DefaultMyIdolsRepository
4 | import net.subroh0508.colormaster.model.MyIdolsRepository
5 | import org.koin.dsl.module
6 |
7 | object MyIdolsRepositories {
8 | val Module get() = module {
9 | single { DefaultMyIdolsRepository(get(), get(), get()) }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/core/test/src/commonMain/kotlin/net/subroh0508/colormaster/test/model/CurrentUser.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.test.model
2 |
3 | import net.subroh0508.colormaster.model.auth.CredentialProvider
4 | import net.subroh0508.colormaster.model.auth.CurrentUser
5 | import net.subroh0508.colormaster.test.data.fromGoogle
6 |
7 | val GoogleUser = CurrentUser(
8 | fromGoogle.uid,
9 | listOf(CredentialProvider.Google(fromGoogle.providers.first().email ?: "")),
10 | )
11 |
--------------------------------------------------------------------------------
/core/common/src/commonMain/kotlin/net/subroh0508/colormaster/common/ui/Languages.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.common.ui
2 |
3 | enum class Languages(val code: String, val label: String) {
4 | JAPANESE("ja", "日本語"),
5 | ENGLISH("en", "ENGLISH");
6 |
7 | operator fun component1() = code
8 | operator fun component2() = label
9 |
10 | companion object {
11 | fun valueOfCode(code: String) = values().find { it.code == code }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/backend/server/README.md:
--------------------------------------------------------------------------------
1 | # COLOR M@STER バックエンドモジュール
2 |
3 | このモジュールは、COLOR M@STERアプリケーションのバックエンドサーバーを提供します。
4 |
5 | ## 機能
6 |
7 | - REST APIエンドポイント
8 | - SQLiteデータベース連携
9 | - ビジネスロジック処理
10 |
11 | ## 技術スタック
12 |
13 | - Ktor: サーバーフレームワーク
14 | - SQLDelight: タイプセーフなSQLクエリ
15 | - Kotlinx Serialization: JSONシリアライゼーション
16 | - Kotlinx Coroutines: 非同期処理
17 | - Logback: ロギング
18 | - Docker: コンテナ化
19 |
20 | ## 開発方法
21 |
22 | ### ローカルでのサーバー起動
23 |
24 | ```bash
25 | ./gradlew :backend:run
26 |
--------------------------------------------------------------------------------
/core/data/src/commonTest/kotlin/net/subroh0508/colormaster/data/extension/UserDocument.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.extension
2 |
3 | import net.subroh0508.colormaster.network.auth.AuthClient
4 | import net.subroh0508.colormaster.network.firestore.FirestoreClient
5 |
6 | expect suspend fun setUserDocument(
7 | auth: AuthClient,
8 | firestore: FirestoreClient,
9 | inChargeIds: List = listOf(),
10 | favoriteIds: List = listOf(),
11 | )
12 |
--------------------------------------------------------------------------------
/core/network/auth/src/commonMain/kotlin/net/subroh0508/colormaster/network/auth/model/Provider.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.auth.model
2 |
3 | data class Provider(
4 | val id: String,
5 | val email: String?,
6 | val displayName: String?,
7 | ) {
8 | companion object {
9 | const val PROVIDER_ANONYMOUS = "anonymous"
10 | const val PROVIDER_GOOGLE = "google.com"
11 | const val PROVIDER_TWITTER = "twitter.com"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/externals/MDCTextField.kt:
--------------------------------------------------------------------------------
1 | package material.externals
2 |
3 | import org.w3c.dom.Element
4 |
5 | @JsModule("@material/textfield")
6 | @JsNonModule
7 | private external object MDCTextFieldModule {
8 | class MDCTextField(root: Element?): material.externals.MDCTextField
9 | }
10 |
11 | external interface MDCTextField
12 |
13 | @Suppress("FunctionName")
14 | fun MDCTextField(root: Element?): MDCTextField = MDCTextFieldModule.MDCTextField(root)
15 |
--------------------------------------------------------------------------------
/core/data/src/commonMain/kotlin/net/subroh0508/colormaster/data/di/IdolColorsRepositories.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.di
2 |
3 | import net.subroh0508.colormaster.data.DefaultIdolColorsRepository
4 | import net.subroh0508.colormaster.model.IdolColorsRepository
5 | import org.koin.dsl.module
6 |
7 | object IdolColorsRepositories {
8 | val Module get() = module {
9 | single { DefaultIdolColorsRepository(get(), get(), get()) }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/core/network/auth/src/commonMain/kotlin/net/subroh0508/colormaster/network/auth/di/Auth.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.auth.di
2 |
3 | import dev.gitlive.firebase.Firebase
4 | import dev.gitlive.firebase.auth.auth
5 | import net.subroh0508.colormaster.network.auth.AuthClient
6 | import org.koin.core.module.Module
7 | import org.koin.dsl.module
8 |
9 | object Auth {
10 | val Module: Module get() = module {
11 | single { AuthClient(Firebase.auth) }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/core/features/search/src/jsMain/kotlin/net/subroh0508/colormaster/features/search/model/SearchByTab.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.search.model
2 |
3 | actual enum class SearchByTab(
4 | val query: String,
5 | val labelKey: String,
6 | ) {
7 | BY_NAME("", "searchBox.tabs.name"),
8 | BY_LIVE("live", "searchBox.tabs.live");
9 |
10 | companion object {
11 | fun findByQuery(query: String?) = values().find { it.query == query } ?: BY_NAME
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/theme.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/core/data/src/jsMain/kotlin/net/subroh0508/colormaster/data/di/AuthRepositories.js.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.di
2 |
3 | import net.subroh0508.colormaster.data.DefaultAuthRepository
4 | import net.subroh0508.colormaster.model.auth.AuthRepository
5 | import org.koin.core.module.Module
6 | import org.koin.dsl.module
7 |
8 | actual object AuthRepositories {
9 | actual val Module: Module = module {
10 | single { DefaultAuthRepository(get()) }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/plugins/src/main/kotlin/net/subroh0508/colormaster/primitive/android/Android.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("HardcodedStringLiteral")
2 |
3 | package net.subroh0508.colormaster.primitive.android
4 |
5 | object Android {
6 | const val applicationId = "net.subroh0508.colormaster"
7 | const val versionCode = 1
8 | const val versionName = "0.0.1"
9 |
10 | object Versions {
11 | const val compileSdk = 35
12 | const val minSdk = 23
13 | const val targetSdk = 35
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/page/MyIdolsPage.kt:
--------------------------------------------------------------------------------
1 | package page
2 |
3 | import androidx.compose.runtime.Composable
4 | import components.templates.StaticPageFrame
5 | import components.templates.myidols.FavoriteIdolsCard
6 | import components.templates.myidols.InChargeIdolsCard
7 |
8 | @Composable
9 | fun MyIdolsPage(
10 | topAppBarVariant: String,
11 | isSignedIn: Boolean,
12 | ) = StaticPageFrame(topAppBarVariant) {
13 | InChargeIdolsCard(isSignedIn)
14 | FavoriteIdolsCard(isSignedIn)
15 | }
16 |
--------------------------------------------------------------------------------
/core/data/src/androidMain/kotlin/net/subroh0508/colormaster/data/di/AuthRepositories.android.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.di
2 |
3 | import net.subroh0508.colormaster.data.DefaultAuthRepository
4 | import net.subroh0508.colormaster.model.auth.AuthRepository
5 | import org.koin.core.module.Module
6 | import org.koin.dsl.module
7 |
8 | actual object AuthRepositories {
9 | actual val Module: Module = module {
10 | single { DefaultAuthRepository(get()) }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/externals/MDCMenu.kt:
--------------------------------------------------------------------------------
1 | package material.externals
2 |
3 | import org.w3c.dom.Element
4 |
5 | @JsModule("@material/menu")
6 | @JsNonModule
7 | private external object MDCMenuModule {
8 | object MDCMenu {
9 | fun attachTo(root: Element?) : material.externals.MDCMenu
10 | }
11 | }
12 |
13 | external interface MDCMenu {
14 | var open: Boolean
15 | }
16 |
17 | @Suppress("FunctionName")
18 | fun MDCMenu(root: Element?) = MDCMenuModule.MDCMenu.attachTo(root)
19 |
--------------------------------------------------------------------------------
/backend/cli/src/main/kotlin/net/subroh0508/colormaster/backend/cli/imasparql/json/IdolColorJson.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.backend.cli.imasparql.json
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class IdolColorJson(
7 | val id: Map,
8 | val nameJa: Map,
9 | val nameKanaJa: Map,
10 | val nameEn: Map,
11 | val color: Map,
12 | val brandName: Map
13 | )
14 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_indeterminate_check_box_24dp.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/externals/MDCTopAppBar.kt:
--------------------------------------------------------------------------------
1 | package material.externals
2 |
3 | import org.w3c.dom.Element
4 |
5 | @JsModule("@material/top-app-bar")
6 | @JsNonModule
7 | private external object MDCTopAppBarModule {
8 | object MDCTopAppBar {
9 | fun attachTo(root: Element?): material.externals.MDCTopAppBar
10 | }
11 | }
12 |
13 | external interface MDCTopAppBar
14 |
15 | @Suppress("FunctionName")
16 | fun MDCTopAppBar(root: Element?) = MDCTopAppBarModule.MDCTopAppBar.attachTo(root)
17 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/externals/MDCTextFieldIcon.kt:
--------------------------------------------------------------------------------
1 | package material.externals
2 |
3 | import org.w3c.dom.Element
4 |
5 | @JsModule("@material/textfield/icon")
6 | @JsNonModule
7 | private external object MDCTextFieldIconModule {
8 | class MDCTextFieldIcon(root: Element?): material.externals.MDCTextFieldIcon
9 | }
10 |
11 | external interface MDCTextFieldIcon
12 |
13 | @Suppress("FunctionName")
14 | fun MDCTextFieldIcon(root: Element?): MDCTextFieldIcon = MDCTextFieldIconModule.MDCTextFieldIcon(root)
15 |
--------------------------------------------------------------------------------
/core/model/src/commonMain/kotlin/net/subroh0508/colormaster/model/auth/CurrentUser.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.model.auth
2 |
3 | data class CurrentUser(
4 | val id: String,
5 | val credentialProviders: List,
6 | ) {
7 | val isAnonymous = credentialProviders.isEmpty() || credentialProviders.find { it is CredentialProvider.Anonymous } != null
8 |
9 | val providerByGoogle = credentialProviders
10 | .filterIsInstance()
11 | .firstOrNull()
12 | }
13 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/utilities/TagElementBuilder.kt:
--------------------------------------------------------------------------------
1 | package material.utilities
2 |
3 | import kotlinx.browser.document
4 | import org.jetbrains.compose.web.dom.ElementBuilder
5 | import org.w3c.dom.Element
6 | import org.w3c.dom.HTMLElement
7 |
8 | class TagElementBuilder(private val tagName: String) : ElementBuilder {
9 | private val el: Element by lazy { document.createElement(tagName) }
10 | @Suppress("UNCHECKED_CAST")
11 | override fun create(): HTMLElement = el.cloneNode() as HTMLElement
12 | }
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_highlight_24dp.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/backend/cli/src/main/kotlin/net/subroh0508/colormaster/backend/cli/imasparql/serializer/Response.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.backend.cli.imasparql.serializer
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class Response(
7 | val head: Vars,
8 | val results: Results
9 | ) {
10 | @Serializable
11 | data class Vars(
12 | val vars: List
13 | )
14 |
15 | @Serializable
16 | data class Results(
17 | val bindings: List
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/core/features/search/src/commonMain/kotlin/net/subroh0508/colormaster/features/search/viewmodel/SuggestLiveNameUiState.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.search.viewmodel
2 |
3 | import net.subroh0508.colormaster.model.LiveName
4 |
5 | sealed interface SuggestLiveNameUiState {
6 | data object Loading : SuggestLiveNameUiState
7 | data class Loaded(
8 | val suggests: List,
9 | ) : SuggestLiveNameUiState
10 | data class Error(
11 | val error: Throwable,
12 | ) : SuggestLiveNameUiState
13 | }
14 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_check_box_24dp.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/serializer/Response.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.serializer
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class Response(
7 | val head: Vars,
8 | val results: Results
9 | ) {
10 | @Serializable
11 | data class Vars(
12 | val vars: List
13 | )
14 |
15 | @Serializable
16 | data class Results(
17 | val bindings: List
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/core/common/src/jsMain/kotlin/net/subroh0508/colormaster/common/external/Function.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.common.external
2 |
3 | external interface JsFunction {
4 | fun call(ctx: C, vararg args: Any?): O
5 | fun apply(ctx: C, args: Array): O
6 | fun bind(ctx: C, vararg args: Any?): JsFunction
7 |
8 | val length: Int
9 | }
10 |
11 | external interface JsFunction1 : JsFunction
12 |
13 | operator fun JsFunction1.invoke(arg: I) =
14 | asDynamic()(arg)
15 |
--------------------------------------------------------------------------------
/core/features/home/src/jsMain/kotlin/net/subroh0508/colormaster/features/home/viewmodel/AuthViewModel.js.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.home.viewmodel
2 |
3 | import net.subroh0508.colormaster.model.auth.AuthRepository
4 |
5 | actual class AuthViewModel(
6 | repository: AuthRepository,
7 | ) : CommonAuthViewModel(repository) {
8 | suspend fun signInWithGoogle(
9 | isMobile: Boolean,
10 | ) = if (isMobile)
11 | repository.signInWithGoogleForMobile()
12 | else
13 | repository.signInWithGoogle()
14 | }
--------------------------------------------------------------------------------
/core/model/src/commonMain/kotlin/net/subroh0508/colormaster/model/Inlines.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.model
2 |
3 | import kotlin.jvm.JvmInline
4 |
5 | @JvmInline
6 | value class IdolName(val value: String)
7 | @JvmInline
8 | value class LiveName(val value: String)
9 | @JvmInline
10 | value class UnitName(val value: String)
11 | @JvmInline
12 | value class SongName(val value: String)
13 |
14 | fun String?.toIdolName() = this?.takeIf(String::isNotBlank)?.let(::IdolName)
15 | fun String?.toLiveName() = this?.takeIf(String::isNotBlank)?.let(::LiveName)
16 |
--------------------------------------------------------------------------------
/core/test/src/commonMain/kotlin/net/subroh0508/colormaster/test/data/FirebaseUser.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.test.data
2 |
3 | import net.subroh0508.colormaster.network.auth.model.FirebaseUser
4 | import net.subroh0508.colormaster.network.auth.model.Provider
5 |
6 | val anonymous = FirebaseUser(
7 | "xxx-xxx-xxx",
8 | listOf(Provider(Provider.PROVIDER_ANONYMOUS, null, null)),
9 | )
10 |
11 | val fromGoogle = FirebaseUser(
12 | "yyy-yyy-yyy",
13 | listOf(Provider(Provider.PROVIDER_GOOGLE, "example@gmail.com", "Google User")),
14 | )
15 |
--------------------------------------------------------------------------------
/core/features/myidols/src/commonMain/kotlin/net/subroh0508/colormaster/features/myidols/viewmodel/MyIdolsUiState.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.myidols.viewmodel
2 |
3 | import net.subroh0508.colormaster.model.IdolColor
4 |
5 | sealed interface MyIdolsUiState {
6 | data object Loading : MyIdolsUiState
7 | data class Loaded(
8 | val inChargeIdols: List,
9 | val favoriteIdols: List,
10 | ) : MyIdolsUiState
11 | data class Error(
12 | val error: Throwable,
13 | ) : MyIdolsUiState
14 | }
15 |
--------------------------------------------------------------------------------
/core/test/src/commonMain/kotlin/net/subroh0508/colormaster/test/fake/FakeAuthClient.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.test.fake
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import net.subroh0508.colormaster.network.auth.AuthClient
5 | import net.subroh0508.colormaster.network.auth.model.FirebaseUser
6 |
7 | expect class FakeAuthClient() : AuthClient {
8 | override val currentUser: FirebaseUser?
9 |
10 | override suspend fun signInAnonymously()
11 | override suspend fun signOut()
12 |
13 | override fun subscribeAuthState(): Flow
14 | }
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/components/organisms/drawer/DrawerHeader.kt:
--------------------------------------------------------------------------------
1 | package components.organisms.drawer
2 |
3 | import androidx.compose.runtime.Composable
4 | import material.components.Divider
5 | import org.jetbrains.compose.web.dom.Text
6 | import utilities.APP_NAME
7 | import utilities.APP_VERSION
8 | import material.components.DrawerHeader as MaterialDrawerHeader
9 |
10 | @Composable
11 | fun DrawerHeader() {
12 | MaterialDrawerHeader(
13 | title = { Text(APP_NAME) },
14 | subtitle = { Text(APP_VERSION) },
15 | )
16 | Divider()
17 | }
18 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_error_outline_24dp.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/core/data/src/commonMain/kotlin/net/subroh0508/colormaster/data/di/DataModule.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.di
2 |
3 | import net.subroh0508.colormaster.network.auth.di.Auth
4 | import net.subroh0508.colormaster.network.firestore.di.Firestore
5 | import net.subroh0508.colormaster.network.imasparql.di.Api
6 |
7 | val DataModule get() = Api.Module() + Auth.Module + Firestore.Module +
8 | AuthRepositories.Module +
9 | IdolColorsRepositories.Module +
10 | LiveRepositories.Module +
11 | MyIdolsRepositories.Module +
12 | PreviewRepositories.Module
13 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/externals/MDCTooltip.kt:
--------------------------------------------------------------------------------
1 | package material.externals
2 |
3 | import org.w3c.dom.Element
4 |
5 | @JsModule("@material/tooltip")
6 | @JsNonModule
7 | private external object MDCTooltipModule {
8 | object MDCTooltip {
9 | fun attachTo(root: Element?) : material.externals.MDCTooltip
10 | }
11 | }
12 |
13 | external interface MDCTooltip {
14 | fun setHideDelay(delayMs: Number)
15 | fun setShowDelay(delayMs: Number)
16 | }
17 |
18 | @Suppress("FunctionName")
19 | fun MDCTooltip(root: Element?) = MDCTooltipModule.MDCTooltip.attachTo(root)
20 |
--------------------------------------------------------------------------------
/core/common/src/commonMain/kotlin/net/subroh0508/colormaster/common/model/LoadState.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.common.model
2 |
3 | sealed class LoadState {
4 | data object Initialize : LoadState()
5 | data object Loading : LoadState()
6 | data class Loaded(val value: T) : LoadState()
7 | data class Error(val error: Throwable) : LoadState()
8 |
9 | val isLoading get() = this is Loading
10 |
11 | @Suppress("UNCHECKED_CAST")
12 | fun getValueOrNull(): T? = (this as? Loaded)?.value
13 | fun getErrorOrNull(): Throwable? = (this as? Error)?.error
14 | }
15 |
--------------------------------------------------------------------------------
/core/model/src/commonMain/kotlin/net/subroh0508/colormaster/model/Types.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.model
2 |
3 | interface Types {
4 | val queryStr: String
5 |
6 | enum class CinderellaGirls(override val queryStr: String) : Types {
7 | CU("Cu"), CO("Co"), PA("Pa")
8 | }
9 |
10 | enum class MillionLive(override val queryStr: String) : Types {
11 | PRINCESS("Princess"), FAIRY("Fairy"), ANGEL("Angel")
12 | }
13 |
14 | enum class SideM(override val queryStr: String) : Types {
15 | PHYSICAL("フィジカル"), INTELLIGENT("インテリ"), MENTAL("メンタル")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/externals/MDCDrawer.kt:
--------------------------------------------------------------------------------
1 | package material.externals
2 |
3 | import org.w3c.dom.Element
4 |
5 | @JsModule("@material/drawer")
6 | @JsNonModule
7 | private external object MDCDrawerModule {
8 | object MDCDrawer {
9 | fun attachTo(root: Element?): material.externals.MDCDrawer
10 | }
11 | }
12 |
13 | external interface MDCDrawer {
14 | var open: Boolean
15 | }
16 |
17 | fun MDCDrawer.open() { open = true }
18 | fun MDCDrawer.close() { open = false }
19 |
20 | @Suppress("FunctionName")
21 | fun MDCDrawer(root: Element?) = MDCDrawerModule.MDCDrawer.attachTo(root)
22 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/subroh0508/colormaster/androidapp/ColorMasterApplication.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.androidapp
2 |
3 | import android.app.Application
4 | import net.subroh0508.colormaster.data.di.DataModule
5 | import net.subroh0508.colormaster.common.koinApp
6 | import org.koin.dsl.module
7 |
8 | class ColorMasterApplication : Application() {
9 | override fun onCreate() {
10 | super.onCreate()
11 |
12 | koinApp.modules(
13 | DataModule + module {
14 | single { this@ColorMasterApplication }
15 | }
16 | )
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/plugins/src/main/kotlin/net/subroh0508/colormaster/primitive/kmp/KmpDsl.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.primitive.kmp
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.api.plugins.PluginContainer
5 | import org.gradle.kotlin.dsl.configure
6 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
7 |
8 | fun Project.kotlin(action: KotlinMultiplatformExtension.() -> Unit) = extensions.configure(action)
9 |
10 | fun PluginContainer.applyKmpPlugins() {
11 | apply("colormaster.primitive.kmp")
12 | apply("colormaster.primitive.kmp.android")
13 | apply("colormaster.primitive.kmp.js")
14 | }
15 |
--------------------------------------------------------------------------------
/core/features/home/src/commonMain/kotlin/net/subroh0508/colormaster/features/home/SignInUseCase.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.home
2 |
3 | import androidx.compose.runtime.*
4 | import net.subroh0508.colormaster.common.CurrentLocalKoinApp
5 | import org.koin.core.KoinApplication
6 |
7 | expect class SignInUseCase
8 |
9 | // @see: https://youtrack.jetbrains.com/issue/KT-49563
10 | @Composable
11 | expect fun rememberSignInUseCase(
12 | koinApp: KoinApplication /* = CurrentLocalKoinApp() */,
13 | ): SignInUseCase
14 |
15 | @Composable
16 | fun rememberSignInUseCase() = rememberSignInUseCase(CurrentLocalKoinApp())
17 |
--------------------------------------------------------------------------------
/core/network/auth/src/commonMain/kotlin/net/subroh0508/colormaster/network/auth/AuthClient.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.auth
2 |
3 | import dev.gitlive.firebase.auth.FirebaseAuth
4 | import kotlinx.coroutines.flow.Flow
5 | import net.subroh0508.colormaster.network.auth.model.FirebaseUser
6 |
7 | expect interface AuthClient {
8 | companion object {
9 | internal operator fun invoke(auth: FirebaseAuth): AuthClient
10 | }
11 |
12 | val currentUser: FirebaseUser?
13 |
14 | suspend fun signInAnonymously()
15 | suspend fun signOut()
16 |
17 | fun subscribeAuthState(): Flow
18 | }
19 |
--------------------------------------------------------------------------------
/core/test/src/commonMain/kotlin/net/subroh0508/colormaster/test/fake/FakeFirestoreClient.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.test.fake
2 |
3 | import net.subroh0508.colormaster.network.firestore.FirestoreClient
4 | import net.subroh0508.colormaster.network.firestore.document.UserDocument
5 |
6 | class FakeFirestoreClient : FirestoreClient {
7 | private val store = mutableMapOf()
8 |
9 | override suspend fun setUserDocument(uid: String, userDocument: UserDocument) {
10 | store[uid] = userDocument
11 | }
12 |
13 | override suspend fun getUserDocument(uid: String?) = store[uid] ?: UserDocument()
14 | }
--------------------------------------------------------------------------------
/core/data/src/commonTest/kotlin/net/subroh0508/colormaster/data/module/LiveRepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.module
2 |
3 | import io.ktor.client.HttpClient
4 | import net.subroh0508.colormaster.data.di.LiveRepositories
5 | import net.subroh0508.colormaster.model.LiveRepository
6 | import net.subroh0508.colormaster.network.imasparql.di.Api
7 | import org.koin.dsl.koinApplication
8 |
9 | internal fun buildLiveRepository(
10 | block: () -> HttpClient,
11 | ): LiveRepository = koinApplication {
12 | modules(
13 | Api.Module(block()) + LiveRepositories.Module
14 | )
15 | }.koin.get(LiveRepository::class)
16 |
--------------------------------------------------------------------------------
/core/network/firestore/src/commonMain/kotlin/net/subroh0508/colormaster/network/firestore/di/Firestore.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.firestore.di
2 |
3 | import dev.gitlive.firebase.Firebase
4 | import dev.gitlive.firebase.firestore.firestore
5 | import net.subroh0508.colormaster.network.firestore.FirestoreClient
6 | import net.subroh0508.colormaster.network.firestore.internal.FirestoreClientImpl
7 | import org.koin.core.module.Module
8 | import org.koin.dsl.module
9 |
10 | object Firestore {
11 | val Module: Module get() = module {
12 | single { FirestoreClientImpl(Firebase.firestore) }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - @string/main_tabs_search_idol_name
5 | - @string/main_tabs_search_unit_name
6 | - @string/main_tabs_search_camera
7 |
8 |
9 |
10 | - 765PRO ALLSTARS
11 | - MILLIONSTARS
12 | - CINDERELLA GIRLS
13 | - SHINY COLORS
14 | - 315 STARS
15 | - 876PRO
16 |
17 |
18 |
--------------------------------------------------------------------------------
/core/model/src/commonMain/kotlin/net/subroh0508/colormaster/model/Brands.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.model
2 |
3 | @Suppress("EnumEntryName")
4 | enum class Brands(
5 | val displayName: String,
6 | val queryStr: String
7 | ) {
8 | _765("765PRO ALLSTARS","765AS"),
9 | _ML("MILLIONSTARS", "MillionLive"),
10 | _CG("CINDERELLA GIRLS", "CinderellaGirls"),
11 | _SC("SHINY COLORS", "ShinyColors"),
12 | _315("315 STARS", "SideM"),
13 | _876("876PRO", "DearlyStars");
14 |
15 | override fun toString() = displayName
16 |
17 | operator fun component1() = displayName
18 | operator fun component2() = queryStr
19 | }
20 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/externals/MDCFormField.kt:
--------------------------------------------------------------------------------
1 | package material.externals
2 |
3 | import org.w3c.dom.Element
4 |
5 | @JsModule("@material/form-field")
6 | @JsNonModule
7 | private external object MDCFormFieldModule {
8 | class MDCFormField(root: Element?) : material.externals.MDCFormField {
9 | override var input: Any?
10 | get() = definedExternally
11 | set(value) = definedExternally
12 | }
13 | }
14 |
15 | external interface MDCFormField {
16 | var input: Any?
17 | }
18 |
19 | @Suppress("FunctionName")
20 | fun MDCFormField(root: Element?): MDCFormField = MDCFormFieldModule.MDCFormField(root)
21 |
--------------------------------------------------------------------------------
/core/common/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("colormaster.convention.common")
3 | }
4 |
5 | kotlin {
6 | sourceSets {
7 | val commonMain by getting {
8 | dependencies {
9 | implementation(libs.firebase.app)
10 | }
11 | }
12 | val jsMain by getting {
13 | dependencies {
14 | implementation(npm("i18next", libs.versions.npm.i18next.core.get()))
15 | implementation(npm("i18next-http-backend", libs.versions.npm.i18next.http.backend.get()))
16 | }
17 | }
18 | }
19 | }
20 |
21 | android { namespace = "net.subroh0508.colormaster.common" }
22 |
--------------------------------------------------------------------------------
/core/data/src/commonMain/kotlin/net/subroh0508/colormaster/data/extension/ImasparqlClient.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.extension
2 |
3 | import net.subroh0508.colormaster.network.imasparql.ImasparqlClient
4 | import net.subroh0508.colormaster.network.imasparql.json.IdolColorJson
5 | import net.subroh0508.colormaster.network.imasparql.query.SearchByIdQuery
6 |
7 | internal suspend fun ImasparqlClient.search(
8 | ids: List,
9 | lang: String,
10 | ) = if (ids.isEmpty())
11 | listOf()
12 | else
13 | search(
14 | SearchByIdQuery(lang, ids).build(),
15 | IdolColorJson.serializer(),
16 | ).toIdolColors()
17 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/utilities/ResizeObserver.kt:
--------------------------------------------------------------------------------
1 | package utilities
2 |
3 | import org.w3c.dom.Element
4 |
5 | external class BoxSize {
6 | val blockSize: Number
7 | val inlineSize: Number
8 | }
9 |
10 | external class ResizeObserverEntry {
11 | val target: Element
12 | val borderBoxSize: Array
13 | val contentBoxSize: Array
14 | val devicePixelContentBoxSize: Array
15 | }
16 |
17 | external class ResizeObserver(handler: (Array, ResizeObserver) -> Unit) {
18 | fun observe(target: Element, options: dynamic = definedExternally)
19 | fun unobserve(target: Element)
20 | fun disconnect()
21 | }
22 |
--------------------------------------------------------------------------------
/core/data/src/commonTest/kotlin/net/subroh0508/colormaster/data/mock/MockIdolSearchApi.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.mock
2 |
3 | import io.ktor.client.engine.mock.*
4 | import net.subroh0508.colormaster.network.imasparql.query.ImasparqlQuery
5 | import net.subroh0508.colormaster.test.mockHttpClient
6 |
7 | fun mockIdolSearch(
8 | vararg arg: Pair,
9 | ) = mockHttpClient { req ->
10 | arg.forEach { (query, res) ->
11 | if (req.url.parameters["query"] == query.plainQuery) {
12 | return@mockHttpClient respond(res, headers = headers)
13 | }
14 | }
15 |
16 | return@mockHttpClient respondBadRequest()
17 | }
18 |
--------------------------------------------------------------------------------
/core/data/src/commonTest/kotlin/net/subroh0508/colormaster/data/mock/MockSuggestLiveApi.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.mock
2 |
3 | import io.ktor.client.engine.mock.*
4 | import net.subroh0508.colormaster.network.imasparql.query.SuggestLiveQuery
5 | import net.subroh0508.colormaster.test.mockHttpClient
6 |
7 | fun mockSuggestLiveName(
8 | vararg arg: Pair,
9 | ) = mockHttpClient { req ->
10 | arg.forEach { (query, res) ->
11 | if (req.url.parameters["query"] == query.plainQuery) {
12 | return@mockHttpClient respond(res, headers = headers)
13 | }
14 | }
15 |
16 | return@mockHttpClient respondBadRequest()
17 | }
18 |
--------------------------------------------------------------------------------
/core/data/src/jsTest/kotlin/net/subroh0508/colormaster/data/extension/UserDocument.js.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.extension
2 |
3 | import net.subroh0508.colormaster.network.auth.AuthClient
4 | import net.subroh0508.colormaster.network.firestore.FirestoreClient
5 | import net.subroh0508.colormaster.network.firestore.document.UserDocument
6 |
7 | actual suspend fun setUserDocument(
8 | auth: AuthClient,
9 | firestore: FirestoreClient,
10 | inChargeIds: List,
11 | favoriteIds: List,
12 | ) {
13 | auth.signInWithGoogle()
14 | val uid = auth.currentUser?.uid ?: return
15 |
16 | firestore.setUserDocument(uid, UserDocument(inChargeIds, favoriteIds))
17 | }
18 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/subroh0508/colormaster/androidapp/themes/Color.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.androidapp.themes
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val teal200 = Color(0xFF03DAC5)
6 |
7 | val orange200 = Color(0xFFFFCC80)
8 | val orange900 = Color(0xFFE65100)
9 |
10 | val deepOrange200 = Color(0xFFFFAB91)
11 | val deepOrange900 = Color(0xFFE65100)
12 |
13 | val gray800 = Color(0xFF424242)
14 | val gray900 = Color(0xFF212121)
15 |
16 | val lightBackground = Color(0xFFFAFAFA)
17 | val darkBackground = Color(0xFF121212)
18 |
19 | val blue500 = Color(0xFF2196F3)
20 | val green500 = Color(0xFF4CAF50)
21 | val orange500 = Color(0xFFFF9800)
22 | val red500 = Color(0xFFF44336)
23 |
--------------------------------------------------------------------------------
/core/model/src/commonMain/kotlin/net/subroh0508/colormaster/model/IdolColor.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.model
2 |
3 | data class IdolColor(
4 | val id: String,
5 | val name: String,
6 | val intColor: IntColor,
7 | ) {
8 | val isBrighter get() = intColor.isBrighter
9 |
10 | val color get() = intColor.toHex()
11 | }
12 |
13 | typealias IntColor = Triple
14 |
15 | val IntColor.isBrighter get() = let { (red, green, blue) ->
16 | 186 < (red * 0.299 + green * 0.587 + blue * 0.114)
17 | }
18 |
19 | fun IntColor.toHex() = buildString {
20 | append("#")
21 | this@toHex.toList().forEach {
22 | append(it.toString(16).padStart(2, '0').uppercase())
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/components/atoms/topappbar/TopAppActionIcon.kt:
--------------------------------------------------------------------------------
1 | package components.atoms.topappbar
2 |
3 | import androidx.compose.runtime.Composable
4 | import material.components.IconButton
5 | import org.jetbrains.compose.web.attributes.AttrsScope
6 | import org.w3c.dom.HTMLButtonElement
7 |
8 | @Composable
9 | fun TopAppActionIcon(
10 | icon: String,
11 | applyAttrs: (AttrsScope.() -> Unit)? = null,
12 | tooltip: @Composable (String) -> Unit,
13 | ) {
14 | IconButton(icon, applyAttrs = {
15 | classes("mdc-top-app-bar__action-item")
16 | applyAttrs?.invoke(this)
17 | attr("aria-describedby", icon)
18 | })
19 |
20 | tooltip(icon)
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/core/data/src/commonTest/kotlin/net/subroh0508/colormaster/data/module/AuthRepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.module
2 |
3 | import net.subroh0508.colormaster.data.di.AuthRepositories
4 | import net.subroh0508.colormaster.model.auth.AuthRepository
5 | import net.subroh0508.colormaster.network.auth.AuthClient
6 | import net.subroh0508.colormaster.test.fake.FakeAuthClient
7 | import org.koin.dsl.koinApplication
8 | import org.koin.dsl.module
9 |
10 | internal fun buildAuthRepository(): AuthRepository = koinApplication {
11 | modules(
12 | module {
13 | single { FakeAuthClient() }
14 | } + AuthRepositories.Module,
15 | )
16 | }.koin.get(AuthRepository::class)
17 |
--------------------------------------------------------------------------------
/core/data/src/androidUnitTest/kotlin/net/subroh0508/colormaster/data/extension/UserDocument.android.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.extension
2 |
3 | import net.subroh0508.colormaster.network.auth.AuthClient
4 | import net.subroh0508.colormaster.network.firestore.FirestoreClient
5 | import net.subroh0508.colormaster.network.firestore.document.UserDocument
6 |
7 | actual suspend fun setUserDocument(
8 | auth: AuthClient,
9 | firestore: FirestoreClient,
10 | inChargeIds: List,
11 | favoriteIds: List,
12 | ) {
13 | auth.signInWithGoogle("idToken")
14 | val uid = auth.currentUser?.uid ?: return
15 |
16 | firestore.setUserDocument(uid, UserDocument(inChargeIds, favoriteIds))
17 | }
18 |
--------------------------------------------------------------------------------
/core/data/src/jsMain/kotlin/net/subroh0508/colormaster/data/DefaultAuthRepository.js.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data
2 |
3 | import kotlinx.coroutines.flow.map
4 | import net.subroh0508.colormaster.network.auth.AuthClient
5 | import net.subroh0508.colormaster.model.auth.AuthRepository
6 |
7 | internal class DefaultAuthRepository(
8 | private val client: AuthClient,
9 | ) : AuthRepository {
10 | override fun getCurrentUserStream() = client.subscribeAuthState().map { it?.toEntity() }
11 | override suspend fun signOut() = client.signOut()
12 |
13 | override suspend fun signInWithGoogle() = client.signInWithGoogle()
14 | override suspend fun signInWithGoogleForMobile() = client.signInWithGoogleForMobile()
15 | }
16 |
--------------------------------------------------------------------------------
/backend/server/src/main/kotlin/net/subroh0508/colormaster/backend/model/IdolDto.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.backend.model
2 |
3 | import kotlinx.serialization.Serializable
4 | import net.subroh0508.colormaster.backend.database.Idol
5 |
6 | @Serializable
7 | data class IdolDto(
8 | val id: Long,
9 | val nameJa: String,
10 | val nameKanaJa: String,
11 | val nameEn: String,
12 | val color: String,
13 | val contentCategory: String,
14 | val contentTitle: String
15 | )
16 |
17 | fun Idol.toDto() = IdolDto(
18 | id = id,
19 | nameJa = name_ja,
20 | nameKanaJa = name_kana_ja,
21 | nameEn = name_en,
22 | color = color,
23 | contentCategory = content_category,
24 | contentTitle = content_title
25 | )
26 |
--------------------------------------------------------------------------------
/core/data/src/androidMain/kotlin/net/subroh0508/colormaster/data/DefaultAuthRepository.android.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data
2 |
3 | import kotlinx.coroutines.flow.map
4 | import net.subroh0508.colormaster.network.auth.AuthClient
5 | import net.subroh0508.colormaster.model.auth.AuthRepository
6 |
7 | internal class DefaultAuthRepository(
8 | private val client: AuthClient,
9 | ) : AuthRepository {
10 | override fun getCurrentUserStream() = client.subscribeAuthState().map { it?.toEntity() }
11 | override suspend fun signOut() = client.signOut()
12 |
13 | override suspend fun fetchCurrentUser() = client.currentUser?.toEntity()
14 | override suspend fun signInWithGoogle(idToken: String) = client.signInWithGoogle(idToken)
15 | }
16 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/components/molecules/TopAppBar.kt:
--------------------------------------------------------------------------------
1 | package components.molecules
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.web.events.SyntheticMouseEvent
5 | import material.components.TopAppNavigationIcon
6 | import material.components.TopAppBar as MaterialTopAppBar
7 |
8 | @Composable
9 | fun TopAppBar(
10 | variant: String,
11 | onClickNavigation: (SyntheticMouseEvent) -> Unit,
12 | actionContent: @Composable () -> Unit,
13 | content: (@Composable () -> Unit)? = null,
14 | ) = MaterialTopAppBar(
15 | variant,
16 | navigationContent = {
17 | TopAppNavigationIcon("menu", onClick = onClickNavigation)
18 | },
19 | actionContent = actionContent,
20 | mainContent = content,
21 | )
22 |
--------------------------------------------------------------------------------
/core/common/src/jsMain/kotlin/net/subroh0508/colormaster/common/firebase.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.common
2 |
3 | import kotlin.js.json
4 |
5 | fun initializeApp(
6 | apiKey: String,
7 | authDomain: String,
8 | databaseUrl: String,
9 | projectId: String,
10 | storageBucket: String,
11 | messagingSenderId: String,
12 | appId: String,
13 | measurementId: String,
14 | ) {
15 | dev.gitlive.firebase.externals.initializeApp(json(
16 | "apiKey" to apiKey,
17 | "authDomain" to authDomain,
18 | "databaseURL" to databaseUrl,
19 | "projectId" to projectId,
20 | "storageBucket" to storageBucket,
21 | "messagingSenderId" to messagingSenderId,
22 | "appId" to appId,
23 | "measurementId" to measurementId,
24 | ))
25 | }
26 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/utilities/Error.kt:
--------------------------------------------------------------------------------
1 | package utilities
2 |
3 | import io.ktor.client.plugins.*
4 | import net.subroh0508.colormaster.common.external.I18nextText
5 | import net.subroh0508.colormaster.common.external.invoke
6 |
7 | fun buildErrorHeader(error: Throwable) = when (error) {
8 | is ResponseException -> "${error.response.status.value}: ${error.response.status.description}"
9 | else -> error::class.simpleName ?: "UnknownException"
10 | }
11 |
12 | fun buildErrorMessage(t: I18nextText, error: Throwable): String {
13 | if (error !is ResponseException) {
14 | return t("errors.unknown")
15 | }
16 |
17 | return when (error.response.status.value / 100) {
18 | 4 -> t("errors.4xx")
19 | 5 -> t("errors.5xx")
20 | else -> t("errors.unknown")
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/externals/MDCRipple.kt:
--------------------------------------------------------------------------------
1 | package material.externals
2 |
3 | import org.w3c.dom.Element
4 |
5 | @JsModule("@material/ripple")
6 | @JsNonModule
7 | private external object MDCRippleModule {
8 | object MDCRipple {
9 | fun attachTo(root: Element?, opts: RippleOption): material.externals.MDCRipple
10 | }
11 | }
12 |
13 | external interface MDCRipple {
14 | fun activate()
15 | fun deactivate()
16 |
17 | var unbounded: Boolean
18 | }
19 |
20 | external interface RippleOption {
21 | var isUnbounded: Boolean
22 | }
23 |
24 | @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
25 | fun attachRippleTo(
26 | root: Element?,
27 | opts: RippleOption.() -> Unit = {},
28 | ) = MDCRippleModule.MDCRipple.attachTo(root, (js("({})") as RippleOption).apply(opts))
29 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/externals/MDCCheckbox.kt:
--------------------------------------------------------------------------------
1 | package material.externals
2 |
3 | import org.w3c.dom.Element
4 |
5 | @JsModule("@material/checkbox")
6 | @JsNonModule
7 | private external object MDCCheckboxModule {
8 | class MDCCheckbox(root: Element?) : material.externals.MDCCheckbox {
9 | override var checked: Boolean
10 | get() = definedExternally
11 | set(value) = definedExternally
12 |
13 | override var disabled: Boolean
14 | get() = definedExternally
15 | set(value) = definedExternally
16 | }
17 | }
18 |
19 | external interface MDCCheckbox {
20 | var checked: Boolean
21 | var disabled: Boolean
22 | }
23 |
24 | @Suppress("FunctionName")
25 | fun MDCCheckbox(root: Element?): MDCCheckbox = MDCCheckboxModule.MDCCheckbox(root)
26 |
--------------------------------------------------------------------------------
/core/common/src/commonMain/kotlin/net/subroh0508/colormaster/common/ui/AppPreference.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.common.ui
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ProvidableCompositionLocal
5 |
6 | expect class AppPreference {
7 | val lang: Languages
8 | val theme: ThemeType
9 |
10 | fun setThemeType(type: ThemeType)
11 |
12 | class State {
13 | val lang: Languages
14 | val theme: ThemeType
15 |
16 | operator fun component1(): Languages
17 | operator fun component2(): ThemeType
18 | }
19 | }
20 |
21 | expect val LocalApp: ProvidableCompositionLocal
22 |
23 | @Composable
24 | fun CurrentLocalTheme() = LocalApp.current.theme
25 |
26 | @Composable
27 | fun CurrentLocalLanguage() = LocalApp.current.lang
28 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/jsMain/kotlin/net/subroh0508/colormaster/network/imasparql/di/JsHttpClient.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.di
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.engine.js.Js
5 | import io.ktor.client.plugins.*
6 | import io.ktor.client.request.accept
7 | import io.ktor.http.URLProtocol
8 | import kotlinx.serialization.json.Json
9 | import net.subroh0508.colormaster.network.imasparql.HOSTNAME
10 | import net.subroh0508.colormaster.network.imasparql.internal.ContentType
11 |
12 | internal actual fun httpClient(json: Json) = HttpClient(Js) {
13 | defaultRequest {
14 | url {
15 | protocol = URLProtocol.HTTPS
16 | host = HOSTNAME
17 | }
18 | accept(ContentType.Application.SparqlJson)
19 | }
20 | Json(json) {}
21 | }
22 |
--------------------------------------------------------------------------------
/core/data/src/commonMain/kotlin/net/subroh0508/colormaster/data/DefaultAuthRepository.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data
2 |
3 | import net.subroh0508.colormaster.model.auth.CredentialProvider
4 | import net.subroh0508.colormaster.model.auth.CurrentUser
5 | import net.subroh0508.colormaster.network.auth.model.FirebaseUser
6 | import net.subroh0508.colormaster.network.auth.model.Provider
7 |
8 | internal fun FirebaseUser.toEntity() = CurrentUser(
9 | uid,
10 | providers.mapNotNull(Provider::toValueObject),
11 | )
12 |
13 | private fun Provider.toValueObject() = when (id) {
14 | Provider.PROVIDER_ANONYMOUS -> CredentialProvider.Anonymous
15 | Provider.PROVIDER_GOOGLE -> email?.let(CredentialProvider::Google)
16 | Provider.PROVIDER_TWITTER -> displayName?.let(CredentialProvider::Twitter)
17 | else -> null
18 | }
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/theme.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/.github/workflows/web-build-and-deploy.yml:
--------------------------------------------------------------------------------
1 | # This file was auto-generated by the Firebase CLI
2 | # https://github.com/firebase/firebase-tools
3 |
4 | name: "[Web] Deploy"
5 |
6 | on:
7 | push:
8 | tags:
9 | - web/v*
10 |
11 | jobs:
12 | deploy:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | - name: set up JDK 17
17 | uses: actions/setup-java@v4
18 | with:
19 | distribution: zulu
20 | java-version: 17
21 | - run: './gradlew :js:app:jsBrowserDistribution --stacktrace'
22 | - uses: FirebaseExtended/action-hosting-deploy@v0
23 | with:
24 | repoToken: '${{ secrets.GITHUB_TOKEN }}'
25 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_IMAS_COLORMASTER }}'
26 | projectId: imas-colormaster
27 | channelId: live
28 |
--------------------------------------------------------------------------------
/core/network/auth/src/androidMain/kotlin/net/subroh0508/colormaster/network/auth/AuthClient.android.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.auth
2 |
3 | import dev.gitlive.firebase.auth.FirebaseAuth
4 | import kotlinx.coroutines.flow.Flow
5 | import net.subroh0508.colormaster.network.auth.internal.AuthClientImpl
6 | import net.subroh0508.colormaster.network.auth.model.FirebaseUser
7 |
8 | actual interface AuthClient {
9 | actual companion object {
10 | internal actual operator fun invoke(
11 | auth: FirebaseAuth,
12 | ): AuthClient = AuthClientImpl(auth)
13 | }
14 |
15 | actual val currentUser: FirebaseUser?
16 |
17 | actual suspend fun signInAnonymously()
18 |
19 | actual suspend fun signOut()
20 |
21 | actual fun subscribeAuthState(): Flow
22 |
23 | suspend fun signInWithGoogle(idToken: String)
24 | }
25 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Stage 1: Cache Gradle dependencies
2 | FROM gradle:latest AS cache
3 | RUN mkdir -p /home/gradle/cache_home
4 | ENV GRADLE_USER_HOME=/home/gradle/cache_home
5 | COPY . /home/gradle/app/
6 | WORKDIR /home/gradle/app
7 | RUN gradle clean build -i --stacktrace
8 |
9 | # Stage 2: Build Application
10 | FROM gradle:latest AS build
11 | COPY --from=cache /home/gradle/cache_home /home/gradle/.gradle
12 | COPY --chown=gradle:gradle . /home/gradle/src
13 | WORKDIR /home/gradle/src
14 | # Build the fat JAR, Gradle also supports shadow
15 | # and boot JAR by default.
16 | RUN gradle backend:server:buildFatJar --no-daemon
17 |
18 | # Stage 3: Create the Runtime Image
19 | FROM amazoncorretto:22 AS runtime
20 | EXPOSE 8080
21 | RUN mkdir /app
22 | COPY --from=build /home/gradle/src/backend/server/build/libs/*.jar /app/colormaster.jar
23 | ENTRYPOINT ["java","-jar","/app/colormaster.jar"]
24 |
--------------------------------------------------------------------------------
/core/network/auth/src/jsMain/kotlin/net/subroh0508/colormaster/network/auth/AuthClient.js.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.auth
2 |
3 | import dev.gitlive.firebase.auth.FirebaseAuth
4 | import kotlinx.coroutines.flow.Flow
5 | import net.subroh0508.colormaster.network.auth.internal.AuthClientImpl
6 | import net.subroh0508.colormaster.network.auth.model.FirebaseUser
7 |
8 | actual interface AuthClient {
9 | actual companion object {
10 | internal actual operator fun invoke(
11 | auth: FirebaseAuth,
12 | ): AuthClient = AuthClientImpl(auth)
13 | }
14 |
15 | actual val currentUser: FirebaseUser?
16 |
17 | actual suspend fun signInAnonymously()
18 |
19 | actual suspend fun signOut()
20 |
21 | actual fun subscribeAuthState(): Flow
22 |
23 | suspend fun signInWithGoogle()
24 | suspend fun signInWithGoogleForMobile()
25 | }
26 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/query/SuggestLiveQuery.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.query
2 |
3 | class SuggestLiveQuery(
4 | dateRange: Pair? = null,
5 | name: String? = null,
6 | ) : ImasparqlQuery() {
7 | override val rawQuery = """
8 | SELECT ?name WHERE {
9 | ?live rdf:type imas:Live;
10 | schema:name ?name;
11 | schema:startDate ?startDate;
12 | schema:eventStatus ?eventStatus.
13 | FILTER (?eventStatus != schema:EventCancelled)
14 | ${name?.let { "FILTER (regex(?name, '.*$it.*', 'i'))" } ?: ""}
15 | ${dateRange?.let { (start, end) -> "FILTER (xsd:date('$start') <= ?startDate && ?startDate <= xsd:date('$end'))" } ?: ""}
16 | }
17 | ORDER BY ?startDate
18 | LIMIT 5
19 | """.trimIndentAndBr()
20 | }
21 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/components/atoms/chip/ChipGroup.kt:
--------------------------------------------------------------------------------
1 | package components.atoms.chip
2 |
3 | import androidx.compose.runtime.Composable
4 | import material.components.Chip
5 | import org.jetbrains.compose.web.attributes.AttrsScope
6 | import org.jetbrains.compose.web.css.*
7 | import org.jetbrains.compose.web.dom.Div
8 | import org.w3c.dom.HTMLDivElement
9 |
10 | @Composable
11 | fun ChipGroup(
12 | items: List,
13 | selected: T?,
14 | attrsScope: (AttrsScope.() -> Unit)? = null,
15 | onClick: (T?) -> Unit,
16 | ) = Div({
17 | style {
18 | display(DisplayStyle.Flex)
19 | flexWrap(FlexWrap.Wrap)
20 | gap(8.px, 8.px)
21 | }
22 | attrsScope?.invoke(this)
23 | }) {
24 | items.forEach {
25 | Chip(
26 | it.toString(),
27 | it == selected,
28 | ) { onClick(if (it != selected) it else null) }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/main.kt:
--------------------------------------------------------------------------------
1 | import components.templates.MainFrame
2 | import components.templates.RootCompose
3 | import utilities.*
4 | import net.subroh0508.colormaster.common.initializeApp
5 | import net.subroh0508.colormaster.common.koinApp
6 | import net.subroh0508.colormaster.common.ui.ThemeType
7 | import org.jetbrains.compose.web.css.Style
8 | import org.jetbrains.compose.web.renderComposable
9 |
10 | fun main() {
11 | initializeApp(
12 | API_KEY,
13 | AUTH_DOMAIN,
14 | DATABASE_URL,
15 | PROJECT_ID,
16 | STORAGE_BUCKET,
17 | MESSAGING_SENDER_ID,
18 | APP_ID,
19 | MEASUREMENT_ID,
20 | )
21 |
22 | renderComposable(rootElementId = "root") {
23 | RootCompose(koinApp) { preference, theme ->
24 | Style(MaterialTheme(theme == ThemeType.NIGHT))
25 |
26 | MainFrame(preference)
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_palette_24dp.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/plugins/src/main/kotlin/net/subroh0508/colormaster/primitive/kmp/KmpJsPlugin.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.primitive.kmp
2 |
3 | import net.subroh0508.colormaster.library
4 | import net.subroh0508.colormaster.libs
5 | import org.gradle.api.Plugin
6 | import org.gradle.api.Project
7 |
8 | @Suppress("unused")
9 | class KmpJsPlugin : Plugin {
10 | override fun apply(target: Project) {
11 | with (target) {
12 | kotlin {
13 | js { browser() }
14 |
15 | with (sourceSets) {
16 | jsTest {
17 | dependencies {
18 | implementation(dependencies.platform(libs.library("kotlin-wrappers-bom")))
19 | implementation(libs.library("kotlin-wrappers-js"))
20 | }
21 | }
22 | }
23 | }
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/core/common/src/androidMain/kotlin/net/subroh0508/colormaster/common/ui/AndroidAppPreference.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.common.ui
2 |
3 | import androidx.compose.runtime.ProvidableCompositionLocal
4 | import androidx.compose.runtime.compositionLocalOf
5 |
6 | actual class AppPreference {
7 | actual val lang get() = _lang
8 | actual val theme get() = _themeType
9 |
10 | fun setLanguage(lang: Languages) { _lang = lang }
11 | actual fun setThemeType(type: ThemeType) { _themeType = type }
12 |
13 | private var _lang: Languages = Languages.JAPANESE
14 | private var _themeType: ThemeType = ThemeType.DAY
15 |
16 | actual data class State(
17 | actual val lang: Languages = Languages.JAPANESE,
18 | actual val theme: ThemeType = ThemeType.DAY,
19 | )
20 | }
21 |
22 | actual val LocalApp: ProvidableCompositionLocal = compositionLocalOf { AppPreference.State() }
23 |
--------------------------------------------------------------------------------
/core/data/src/commonMain/kotlin/net/subroh0508/colormaster/data/extension/Converter.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.extension
2 |
3 | import net.subroh0508.colormaster.model.IdolColor
4 | import net.subroh0508.colormaster.network.imasparql.json.IdolColorJson
5 | import net.subroh0508.colormaster.network.imasparql.serializer.Response
6 |
7 | internal fun Response.toIdolColors() = results
8 | .bindings
9 | .mapNotNull { (idMap, nameMap, colorMap) ->
10 | val id = idMap["value"] ?: return@mapNotNull null
11 | val name = nameMap["value"] ?: return@mapNotNull null
12 | val color = colorMap["value"] ?: return@mapNotNull null
13 |
14 | val intColor = Triple(
15 | color.substring(0, 2).toInt(16),
16 | color.substring(2, 4).toInt(16),
17 | color.substring(4, 6).toInt(16),
18 | )
19 |
20 | IdolColor(id, name, intColor)
21 | }
22 |
--------------------------------------------------------------------------------
/core/common/src/jsMain/kotlin/net/subroh0508/colormaster/common/external/CommonJs.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.common.external
2 |
3 | import kotlin.js.RegExp
4 |
5 | // @see: https://github.com/JetBrains/kotlin-wrappers/blob/master/kotlin-extensions/src/jsMain/kotlin/kotlinext/js/CommonJS.kt
6 |
7 | external interface Context : JsFunction1 {
8 | fun resolve(module: String): String
9 | fun keys(): Array
10 | val id: Int
11 | }
12 |
13 | fun requireAll(context: Context) = context.keys().forEach(context::invoke)
14 |
15 | external object require {
16 | fun resolve(module: String): String
17 |
18 | // Note: require.context is a webpack-specific function
19 | fun context(directory: String, useSubdirectories: Boolean = definedExternally, regExp: RegExp = definedExternally, mode: String = definedExternally): Context
20 | }
21 |
22 | external fun require(module: String): T
23 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/utilities/decompose.kt:
--------------------------------------------------------------------------------
1 | package utilities
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import androidx.compose.runtime.State
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.remember
8 | import com.arkivanov.decompose.value.Value
9 |
10 | // @see: https://github.com/arkivanov/Decompose/blob/master/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/SubscribeAsState.kt
11 | @Composable
12 | fun Value.subscribeAsState(): State {
13 | val state = remember(this) { mutableStateOf(value) }
14 |
15 | DisposableEffect(this) {
16 | val observer: (T) -> Unit = { state.value = it }
17 |
18 | subscribe(observer)
19 |
20 | onDispose {
21 | unsubscribe(observer)
22 | }
23 | }
24 |
25 | return state
26 | }
27 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | gradlePluginPortal()
5 | mavenCentral()
6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
7 | includeBuild("plugins")
8 | }
9 | }
10 |
11 | dependencyResolutionManagement {
12 | repositories {
13 | google()
14 | mavenCentral()
15 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
16 | }
17 | }
18 |
19 | include(
20 | ":android:app",
21 | ":js:app",
22 | ":js:material",
23 | ":core:common",
24 | ":core:model",
25 | ":core:data",
26 | ":core:network:auth",
27 | ":core:network:firestore",
28 | ":core:network:imasparql",
29 | ":core:features:home",
30 | ":core:features:preview",
31 | ":core:features:search",
32 | ":core:features:myidols",
33 | ":core:test",
34 | ":backend:server",
35 | ":backend:cli"
36 | )
37 |
--------------------------------------------------------------------------------
/core/network/firestore/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("colormaster.convention.api")
3 | }
4 |
5 | kotlin {
6 | sourceSets {
7 | all {
8 | languageSettings.apply {
9 | optIn("kotlin.Experimental")
10 | optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
11 | optIn("kotlinx.serialization.ExperimentalSerializationApi")
12 | optIn("kotlinx.serialization.InternalSerializationApi")
13 | }
14 | }
15 |
16 | val commonMain by getting {
17 | dependencies {
18 | implementation(libs.firebase.firestore)
19 | }
20 | }
21 |
22 | val androidMain by getting {
23 | dependencies {
24 | implementation(libs.kotlinx.coroutines.play.services)
25 | }
26 | }
27 | }
28 | }
29 |
30 | android { namespace = "net.subroh0508.colormaster.network.firestore" }
31 |
--------------------------------------------------------------------------------
/js/app/webpack.config.d/02.sass.js:
--------------------------------------------------------------------------------
1 | const sass = require('sass');
2 | const autoprefixer = require('autoprefixer');
3 |
4 | config.entry.main.push(path.resolve(rootPath, 'js/app/src/jsMain/resources/app.scss'));
5 |
6 | config.module.rules.push({
7 | test: /\.scss$/,
8 | use: [
9 | {
10 | loader: 'file-loader',
11 | options: {
12 | name: 'app.css',
13 | },
14 | },
15 | { loader: 'extract-loader' },
16 | { loader: 'css-loader' },
17 | {
18 | loader: 'postcss-loader',
19 | options: {
20 | postcssOptions: {
21 | plugins: [
22 | autoprefixer(),
23 | ],
24 | },
25 | },
26 | },
27 | {
28 | loader: 'sass-loader',
29 | options: {
30 | implementation: sass,
31 | webpackImporter: false,
32 | sassOptions: {
33 | includePaths: [nodeModulePath],
34 | },
35 | },
36 | },
37 | ],
38 | });
39 |
--------------------------------------------------------------------------------
/core/features/search/src/commonMain/kotlin/net/subroh0508/colormaster/features/search/model/LiveNameQuery.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.search.model
2 |
3 | import net.subroh0508.colormaster.common.model.DateNum
4 | import net.subroh0508.colormaster.model.LiveName
5 |
6 | data class LiveNameQuery(
7 | val query: String? = null,
8 | val isSettled: Boolean = false,
9 | ) {
10 | companion object {
11 | private const val DATE_NUMBER_PATTERN = """^[0-9]+$"""
12 | }
13 |
14 | fun change(query: String?) = if (isSettled) this else copy(query = query?.takeIf(String::isNotBlank))
15 | fun settle(value: LiveName) = LiveNameQuery(value.value, true)
16 | fun clear() = LiveNameQuery()
17 |
18 | fun isNumber() = query?.let { DATE_NUMBER_PATTERN.toRegex().matches(it) } ?: false
19 | fun toDateNum() = query?.toIntOrNull()?.let(::DateNum)
20 | fun toLiveName() = takeIf { isSettled }?.query?.let(::LiveName)
21 | }
22 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/page/about/TermsPage.kt:
--------------------------------------------------------------------------------
1 | package page.about
2 |
3 | import androidx.compose.runtime.Composable
4 | import components.atoms.card.OutlinedCard
5 | import components.templates.StaticPageFrame
6 | import net.subroh0508.colormaster.common.external.invoke
7 | import net.subroh0508.colormaster.common.ui.LocalI18n
8 |
9 | @Composable
10 | fun TermsPage(
11 | topAppBarVariant: String,
12 | ) = StaticPageFrame(topAppBarVariant) {
13 | val t = LocalI18n() ?: return@StaticPageFrame
14 |
15 | OutlinedCard(
16 | header = t("about.terms.top.title"),
17 | rawHtml = t("about.terms.top.description"),
18 | )
19 |
20 | OutlinedCard(
21 | header = t("about.terms.disclaimer.title"),
22 | rawHtml = t("about.terms.disclaimer.description"),
23 | )
24 |
25 | OutlinedCard(
26 | header = t("about.terms.cookie.title"),
27 | rawHtml = t("about.terms.cookie.description"),
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/src/main/kotlin/net/subroh0508/colormaster/VersionCatalogDsl.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.api.artifacts.VersionCatalog
5 | import org.gradle.api.artifacts.VersionCatalogsExtension
6 | import org.gradle.kotlin.dsl.getByType
7 |
8 | internal val VersionCatalog.kotlinxCoroutinesCore get() = library("kotlinx-coroutines-core")
9 | internal val VersionCatalog.kotlinxSerialization get() = library("kotlinx-serialization")
10 | internal val VersionCatalog.koinCore get() = library("koin-core")
11 |
12 | internal val Project.libs: VersionCatalog get() = extensions.getByType().named("libs")
13 |
14 | internal fun VersionCatalog.version(alias: String) = findVersion(alias).get().requiredVersion
15 | internal fun VersionCatalog.library(library: String) = findLibrary(library).get().get()
16 | internal fun VersionCatalog.bundle(bundle: String) = findBundle(bundle).get().get()
17 |
--------------------------------------------------------------------------------
/core/test/src/commonMain/kotlin/net/subroh0508/colormaster/test/extension/Flow.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.test.extension
2 |
3 | import io.kotest.core.test.TestScope
4 | import io.kotest.core.test.testCoroutineScheduler
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.ExperimentalCoroutinesApi
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.onEach
9 | import kotlinx.coroutines.flow.stateIn
10 | import kotlinx.coroutines.test.TestCoroutineScheduler
11 | import kotlinx.coroutines.test.UnconfinedTestDispatcher
12 |
13 | @OptIn(ExperimentalCoroutinesApi::class)
14 | suspend fun flowToList(
15 | flow: Flow,
16 | ): Pair, TestCoroutineScheduler> {
17 | val instances = mutableListOf()
18 | val scheduler = TestCoroutineScheduler()
19 |
20 | flow.onEach(instances::add)
21 | .stateIn(CoroutineScope(UnconfinedTestDispatcher(scheduler)))
22 |
23 | return instances to scheduler
24 | }
25 |
--------------------------------------------------------------------------------
/core/data/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("colormaster.convention.data")
3 | }
4 |
5 | kotlin {
6 | sourceSets {
7 | val commonMain by getting {
8 | dependencies {
9 | implementation(project(":core:model"))
10 | implementation(project(":core:network:auth"))
11 | implementation(project(":core:network:firestore"))
12 | implementation(project(":core:network:imasparql"))
13 |
14 | implementation(libs.firebase.auth)
15 | implementation(libs.firebase.firestore)
16 |
17 | implementation(libs.ktor.client.core)
18 | implementation(libs.kotlinx.serialization)
19 | }
20 | }
21 | val commonTest by getting {
22 | dependencies {
23 | implementation(libs.ktor.client.mock)
24 | }
25 | }
26 | }
27 | }
28 |
29 | android { namespace = "net.subroh0508.colormaster.data" }
30 |
--------------------------------------------------------------------------------
/core/features/search/src/commonMain/kotlin/net/subroh0508/colormaster/features/search/viewmodel/SearchIdolsUiState.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.search.viewmodel
2 |
3 | import net.subroh0508.colormaster.features.search.model.SearchParams
4 | import net.subroh0508.colormaster.model.IdolColor
5 |
6 | sealed interface SearchIdolsUiState {
7 | val params: SearchParams
8 |
9 | data class Loading(
10 | override val params: SearchParams,
11 | ) : SearchIdolsUiState
12 | data class Loaded(
13 | override val params: SearchParams,
14 | val idols: List,
15 | ) : SearchIdolsUiState
16 | data class Error(
17 | override val params: SearchParams,
18 | val error: Throwable,
19 | ) : SearchIdolsUiState
20 | }
21 |
22 | data class IdolColorListItem(
23 | val item: IdolColor,
24 | val selected: Boolean = false,
25 | val inCharge: Boolean = false,
26 | val favorite: Boolean = false,
27 | )
28 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/di/Api.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.di
2 |
3 | import io.ktor.client.HttpClient
4 | import kotlinx.serialization.json.Json
5 | import net.subroh0508.colormaster.network.imasparql.ImasparqlClient
6 | import net.subroh0508.colormaster.network.imasparql.internal.ImasparqlApiClient
7 | import org.koin.dsl.module
8 |
9 | internal expect fun httpClient(json: Json): HttpClient
10 |
11 | object Api {
12 | private val json by lazy {
13 | Json {
14 | isLenient = true
15 | ignoreUnknownKeys = true
16 | allowSpecialFloatingPointValues = true
17 | useArrayPolymorphism = true
18 | }
19 | }
20 |
21 | @Suppress("FunctionName")
22 | fun Module(client: HttpClient = httpClient(json)) = module {
23 | single { client }
24 | single { ImasparqlApiClient(get(), json) }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/backend/cli/src/main/kotlin/net/subroh0508/colormaster/backend/cli/imasparql/ImasparqlApiClient.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.backend.cli.imasparql
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.request.get
5 | import io.ktor.client.statement.*
6 | import io.ktor.utils.io.charsets.Charset
7 | import kotlinx.serialization.KSerializer
8 | import kotlinx.serialization.json.Json
9 | import net.subroh0508.colormaster.backend.cli.imasparql.serializer.Response
10 |
11 | class ImasparqlApiClient(
12 | private val httpClient: HttpClient,
13 | private val json: Json,
14 | ) : ImasparqlClient {
15 | override suspend fun search(
16 | query: String,
17 | serializer: KSerializer,
18 | ): Response {
19 | val response = httpClient.get(query)
20 |
21 | return json.decodeFromString(
22 | Response.serializer(serializer),
23 | response.bodyAsText(Charset.forName("UTF-8"))
24 | )
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/core/network/auth/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import net.subroh0508.colormaster.primitive.android.Android
2 |
3 | plugins {
4 | id("colormaster.primitive.kmp")
5 | id("colormaster.primitive.kmp.android")
6 | id("colormaster.primitive.kmp.js")
7 | }
8 |
9 | kotlin {
10 | sourceSets {
11 | val commonMain by getting {
12 | dependencies {
13 | implementation(libs.kotlinx.coroutines.core)
14 | implementation(libs.firebase.auth)
15 |
16 | implementation(libs.koin.core)
17 | }
18 | }
19 | val androidMain by getting {
20 | dependencies {
21 | implementation(libs.kotlinx.coroutines.play.services)
22 | }
23 | }
24 | }
25 | }
26 |
27 | android {
28 | namespace = "net.subroh0508.colormaster.network.auth"
29 |
30 | buildFeatures.buildConfig = true
31 | defaultConfig {
32 | buildConfigField("String", "VERSION_CODE", "\"${Android.versionCode}\"")
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/page/about/DevelopmentPage.kt:
--------------------------------------------------------------------------------
1 | package page.about
2 |
3 | import androidx.compose.runtime.Composable
4 | import components.atoms.card.OutlinedCard
5 | import components.templates.StaticPageFrame
6 | import net.subroh0508.colormaster.common.external.invoke
7 | import net.subroh0508.colormaster.common.ui.LocalI18n
8 |
9 | @Composable
10 | fun DevelopmentPage(
11 | topAppBarVariant: String,
12 | ) = StaticPageFrame(topAppBarVariant) {
13 | val t = LocalI18n() ?: return@StaticPageFrame
14 |
15 | OutlinedCard(
16 | header = t("about.development.imasparql.title"),
17 | rawHtml = t("about.development.imasparql.description"),
18 | )
19 |
20 | OutlinedCard(
21 | header = t("about.development.frontend.title"),
22 | rawHtml = t("about.development.frontend.description"),
23 | )
24 |
25 | OutlinedCard(
26 | header = t("about.development.requests.title"),
27 | rawHtml = t("about.development.requests.description"),
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/core/data/src/commonMain/kotlin/net/subroh0508/colormaster/data/DefaultPreviewRepository.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data
2 |
3 | import kotlinx.coroutines.flow.MutableStateFlow
4 | import kotlinx.coroutines.flow.onStart
5 | import net.subroh0508.colormaster.data.extension.search
6 | import net.subroh0508.colormaster.model.IdolColor
7 | import net.subroh0508.colormaster.model.PreviewRepository
8 | import net.subroh0508.colormaster.network.imasparql.ImasparqlClient
9 |
10 | internal class DefaultPreviewRepository(
11 | private val imasparqlClient: ImasparqlClient,
12 | ) : PreviewRepository {
13 | private val idolsStateFlow = MutableStateFlow>(listOf())
14 |
15 | override fun getPreviewColorsStream() = idolsStateFlow.onStart { clear() }
16 |
17 | override fun clear() {
18 | idolsStateFlow.value = listOf()
19 | }
20 |
21 | override suspend fun show(ids: List, lang: String) {
22 | idolsStateFlow.value = imasparqlClient.search(ids, lang)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/plugins/src/main/kotlin/net/subroh0508/colormaster/convention/ModelModulePlugin.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.convention
2 |
3 | import net.subroh0508.colormaster.kotlinxCoroutinesCore
4 | import net.subroh0508.colormaster.libs
5 | import net.subroh0508.colormaster.primitive.kmp.applyKmpPlugins
6 | import net.subroh0508.colormaster.primitive.kmp.kotlin
7 | import org.gradle.api.Plugin
8 | import org.gradle.api.Project
9 |
10 | @Suppress("unused")
11 | class ModelModulePlugin : Plugin {
12 | override fun apply(target: Project) {
13 | with (target) {
14 | with (plugins) {
15 | applyKmpPlugins()
16 | }
17 |
18 | kotlin {
19 | with (sourceSets) {
20 | commonMain {
21 | dependencies {
22 | implementation(libs.kotlinxCoroutinesCore)
23 | }
24 | }
25 | }
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/android/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("colormaster.convention.android")
3 | }
4 |
5 | dependencies {
6 | implementation(project(":core:common"))
7 | implementation(project(":core:model"))
8 | implementation(project(":core:data"))
9 | implementation(project(":core:features:preview"))
10 | implementation(project(":core:features:search"))
11 | implementation(project(":core:features:myidols"))
12 |
13 | implementation(libs.kotlinx.coroutines.android)
14 |
15 | implementation(libs.androidx.core)
16 | implementation(libs.androidx.activity.ktx)
17 | implementation(libs.androidx.activity.compose)
18 | implementation(libs.android.material)
19 | implementation(compose.ui)
20 | implementation(compose.material)
21 | implementation(compose.uiTooling)
22 | implementation(libs.androidx.lifecycle.viewmodel)
23 |
24 | implementation(libs.koin.android)
25 | }
26 |
27 | //apply(plugin = "com.google.gms.google-services")
28 |
29 | android { namespace = "net.subroh0508.colormaster.androidapp" }
30 |
--------------------------------------------------------------------------------
/core/network/imasparql/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import net.subroh0508.colormaster.primitive.android.Android
2 |
3 | plugins {
4 | id("colormaster.convention.api")
5 | }
6 |
7 | kotlin {
8 | sourceSets {
9 | val commonMain by getting {
10 | dependencies {
11 | implementation(libs.ktor.client.core)
12 | implementation(libs.ktor.client.json)
13 | implementation(libs.ktor.serialization.core)
14 | }
15 | }
16 | val androidMain by getting {
17 | dependencies {
18 | implementation(libs.ktor.client.okhttp)
19 | implementation(libs.okhttp3.client)
20 | implementation(libs.okhttp3.logging.interceptor)
21 | }
22 | }
23 | }
24 | }
25 |
26 | android {
27 | namespace = "net.subroh0508.colormaster.network.imasparql"
28 |
29 | buildFeatures.buildConfig = true
30 | defaultConfig {
31 | buildConfigField("String", "VERSION_CODE", "\"${Android.versionCode}\"")
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/components/organisms/box/form/BrandForm.kt:
--------------------------------------------------------------------------------
1 | package components.organisms.box.form
2 |
3 | import androidx.compose.runtime.Composable
4 | import components.atoms.chip.ChipGroup
5 | import material.components.TypographySubtitle1
6 | import net.subroh0508.colormaster.common.external.invoke
7 | import net.subroh0508.colormaster.common.ui.LocalI18n
8 | import net.subroh0508.colormaster.model.Brands
9 | import org.jetbrains.compose.web.css.padding
10 | import org.jetbrains.compose.web.css.px
11 | import org.jetbrains.compose.web.dom.Text
12 |
13 | @Composable
14 | fun BrandForm(
15 | brands: Brands?,
16 | onChange: (Brands?) -> Unit,
17 | ) {
18 | val t = LocalI18n() ?: return
19 |
20 | TypographySubtitle1(applyAttrs = {
21 | classes("mdc-theme-text-primary")
22 | style { padding(8.px, 16.px) }
23 | }) { Text(t("searchBox.attributes.brands")) }
24 |
25 | ChipGroup(
26 | Brands.values().toList(),
27 | brands,
28 | { style { padding(8.px, 16.px) } },
29 | ) { onChange(it) }
30 | }
31 |
--------------------------------------------------------------------------------
/core/features/home/src/androidMain/kotlin/net/subroh0508/colormaster/features/home/AndroidSignInUseCase.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.home
2 |
3 | import androidx.compose.runtime.*
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.launch
6 | import net.subroh0508.colormaster.model.auth.AuthRepository
7 | import org.koin.core.KoinApplication
8 |
9 | actual class SignInUseCase(
10 | private val repository: AuthRepository,
11 | private val scope: CoroutineScope,
12 | ) {
13 | operator fun invoke(idToken: String) {
14 | scope.launch {
15 | runCatching {
16 | repository.signInWithGoogle(idToken)
17 | }
18 | }
19 | }
20 | }
21 |
22 | @Composable
23 | actual fun rememberSignInUseCase(
24 | koinApp: KoinApplication,
25 | ): SignInUseCase {
26 | val scope = rememberCoroutineScope()
27 | val repository: AuthRepository by remember(koinApp) { mutableStateOf(koinApp.koin.get()) }
28 |
29 | return remember(koinApp) { SignInUseCase(repository, scope) }
30 | }
31 |
--------------------------------------------------------------------------------
/plugins/src/main/kotlin/net/subroh0508/colormaster/primitive/kmp/KmpAndroidPlugin.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.primitive.kmp
2 |
3 | import net.subroh0508.colormaster.library
4 | import net.subroh0508.colormaster.libs
5 | import net.subroh0508.colormaster.primitive.android.setupAndroid
6 | import org.gradle.api.Plugin
7 | import org.gradle.api.Project
8 |
9 | @Suppress("unused")
10 | class KmpAndroidPlugin : Plugin {
11 | override fun apply(target: Project) {
12 | with (target) {
13 | with (pluginManager) {
14 | apply("com.android.library")
15 | }
16 |
17 | kotlin {
18 | androidTarget()
19 |
20 | with (sourceSets) {
21 | getByName("androidUnitTest") {
22 | dependencies {
23 | implementation(libs.library("kotest-runner-junit5"))
24 | }
25 | }
26 | }
27 | }
28 |
29 | setupAndroid()
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/core/features/home/src/commonMain/kotlin/net/subroh0508/colormaster/features/home/SignOutUseCase.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.home
2 |
3 | import androidx.compose.runtime.*
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.launch
6 | import net.subroh0508.colormaster.common.CurrentLocalKoinApp
7 | import net.subroh0508.colormaster.model.auth.AuthRepository
8 | import org.koin.core.KoinApplication
9 |
10 | class SignOutUseCase(
11 | private val repository: AuthRepository,
12 | private val scope: CoroutineScope,
13 | ) {
14 | operator fun invoke() {
15 | scope.launch {
16 | runCatching { repository.signOut() }
17 | }
18 | }
19 | }
20 |
21 | @Composable
22 | fun rememberSignOutUseCase(
23 | koinApp: KoinApplication = CurrentLocalKoinApp(),
24 | ): SignOutUseCase {
25 | val scope = rememberCoroutineScope()
26 | val repository: AuthRepository by remember(koinApp) { mutableStateOf(koinApp.koin.get()) }
27 |
28 | return remember(koinApp) { SignOutUseCase(repository, scope) }
29 | }
30 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/query/RandomQuery.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.query
2 |
3 | import net.subroh0508.colormaster.network.imasparql.internal.ESCAPED_ENDPOINT_RDFS_DETAIL
4 |
5 | class RandomQuery(lang: String, limit: Int = 10) : ImasparqlQuery() {
6 | override val rawQuery = """
7 | SELECT ?id ?name ?color WHERE {
8 | ?s imas:Color ?color;
9 | imas:Brand ?brand.
10 | OPTIONAL { ?s schema:name ?realName. FILTER(lang(?realName) = '$lang') }
11 | OPTIONAL { ?s schema:alternateName ?altName. FILTER(lang(?altName) = '$lang') }
12 | OPTIONAL { ?s schema:givenName ?givenName. FILTER(lang(?givenName) = '$lang') }
13 | BIND (COALESCE(?altName, ?realName, ?givenName) as ?name)
14 | FILTER (str(?brand) != '1stVision').
15 | BIND (REPLACE(str(?s), '${ESCAPED_ENDPOINT_RDFS_DETAIL}', '') as ?id).
16 | }
17 | ORDER BY rand()
18 | LIMIT $limit
19 | """.trimIndentAndBr()
20 | }
21 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/components/atoms/checkbox/CheckBoxGroup.kt:
--------------------------------------------------------------------------------
1 | package components.atoms.checkbox
2 |
3 | import androidx.compose.runtime.Composable
4 | import material.components.Checkbox
5 | import org.jetbrains.compose.web.attributes.AttrsScope
6 | import org.jetbrains.compose.web.css.*
7 | import org.jetbrains.compose.web.dom.Div
8 | import org.w3c.dom.HTMLDivElement
9 |
10 | @Composable
11 | fun CheckBoxGroup(
12 | items: List>,
13 | selections: List,
14 | attrsScope: (AttrsScope.() -> Unit)? = null,
15 | onClick: (T, Boolean) -> Unit,
16 | ) = Div({
17 | style {
18 | display(DisplayStyle.Flex)
19 | flexWrap(FlexWrap.Wrap)
20 | }
21 | attrsScope?.invoke(this)
22 | }) {
23 | items.forEach{ (id, item) ->
24 | val checked = selections.contains(item)
25 |
26 | Checkbox(
27 | item.toString(),
28 | checked,
29 | id,
30 | attrsScope = { style { marginRight(16.px) } },
31 | ) { onClick(item, !checked) }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/components/atoms/textfield/DebouncedTextForm.kt:
--------------------------------------------------------------------------------
1 | package components.atoms.textfield
2 |
3 | import androidx.compose.runtime.*
4 | import kotlinx.coroutines.flow.debounce
5 | import kotlinx.coroutines.flow.launchIn
6 | import kotlinx.coroutines.flow.onEach
7 |
8 | @Composable
9 | fun DebouncedTextForm(
10 | text: String?,
11 | timeoutMillis: Long,
12 | onDebouncedChange: (String?) -> Unit,
13 | textfield: @Composable (MutableState) -> Unit,
14 | ) {
15 | val textState = remember(text) { mutableStateOf(text) }
16 | val debouncedTextState = remember(text, timeoutMillis) { mutableStateOf(text) }
17 |
18 | LaunchedEffect(text, timeoutMillis) {
19 | snapshotFlow { textState.value }
20 | .onEach { debouncedTextState.value = textState.value }
21 | .launchIn(this)
22 |
23 | snapshotFlow { debouncedTextState.value }
24 | .debounce(timeoutMillis)
25 | .onEach(onDebouncedChange)
26 | .launchIn(this)
27 | }
28 |
29 | textfield(textState)
30 | }
31 |
--------------------------------------------------------------------------------
/core/data/src/commonTest/kotlin/net/subroh0508/colormaster/data/module/PreviewRepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.module
2 |
3 | import io.ktor.client.HttpClient
4 | import net.subroh0508.colormaster.data.di.MyIdolsRepositories
5 | import net.subroh0508.colormaster.data.di.PreviewRepositories
6 | import net.subroh0508.colormaster.model.MyIdolsRepository
7 | import net.subroh0508.colormaster.model.PreviewRepository
8 | import net.subroh0508.colormaster.network.auth.AuthClient
9 | import net.subroh0508.colormaster.network.firestore.FirestoreClient
10 | import net.subroh0508.colormaster.network.imasparql.di.Api
11 | import net.subroh0508.colormaster.test.fake.FakeAuthClient
12 | import net.subroh0508.colormaster.test.fake.FakeFirestoreClient
13 | import org.koin.dsl.koinApplication
14 | import org.koin.dsl.module
15 |
16 | internal fun buildPreviewRepository(
17 | block: () -> HttpClient,
18 | ): PreviewRepository = koinApplication {
19 | modules(
20 | Api.Module(block()) + PreviewRepositories.Module
21 | )
22 | }.koin.get(PreviewRepository::class)
23 |
--------------------------------------------------------------------------------
/core/features/home/src/commonMain/kotlin/net/subroh0508/colormaster/features/home/viewmodel/CommonAuthViewModel.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.home.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import kotlinx.coroutines.flow.SharingStarted
6 | import kotlinx.coroutines.flow.StateFlow
7 | import kotlinx.coroutines.flow.map
8 | import kotlinx.coroutines.flow.stateIn
9 | import net.subroh0508.colormaster.model.auth.AuthRepository
10 |
11 | abstract class CommonAuthViewModel(
12 | protected val repository: AuthRepository,
13 | ) : ViewModel() {
14 | val uiState: StateFlow = repository.getCurrentUserStream()
15 | .map { user ->
16 | if (user == null) AuthUiState.NotSignedIn
17 | else AuthUiState.SignedIn(user)
18 | }.stateIn(
19 | scope = viewModelScope,
20 | started = SharingStarted.WhileSubscribed(5_000),
21 | initialValue = AuthUiState.NotSignedIn,
22 | )
23 |
24 | suspend fun signOut() = repository.signOut()
25 | }
--------------------------------------------------------------------------------
/plugins/src/main/kotlin/net/subroh0508/colormaster/primitive/kmp/KmpPlugin.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.primitive.kmp
2 |
3 | import net.subroh0508.colormaster.library
4 | import net.subroh0508.colormaster.libs
5 | import org.gradle.api.Plugin
6 | import org.gradle.api.Project
7 |
8 | @Suppress("unused")
9 | class KmpPlugin : Plugin {
10 | override fun apply(target: Project) {
11 | with (target) {
12 | with (pluginManager) {
13 | apply("org.jetbrains.kotlin.multiplatform")
14 | }
15 |
16 | kotlin {
17 | with (sourceSets) {
18 | commonTest {
19 | dependencies {
20 | implementation(libs.library("kotlinx-coroutines-test"))
21 | implementation(libs.library("kotest-framework-engine"))
22 | implementation(libs.library("kotest-assertions-core"))
23 | }
24 | }
25 | }
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/internal/ImasparqlApiClient.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.internal
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.request.get
5 | import io.ktor.client.statement.*
6 | import io.ktor.utils.io.charsets.Charset
7 | import kotlinx.serialization.KSerializer
8 | import kotlinx.serialization.json.Json
9 | import net.subroh0508.colormaster.network.imasparql.ImasparqlClient
10 | import net.subroh0508.colormaster.network.imasparql.serializer.Response
11 |
12 | internal class ImasparqlApiClient(
13 | private val httpClient: HttpClient,
14 | private val json: Json,
15 | ) : ImasparqlClient {
16 | override suspend fun search(
17 | query: String,
18 | serializer: KSerializer,
19 | ): Response {
20 | val response = httpClient.get(query)
21 |
22 | return json.decodeFromString(
23 | Response.serializer(serializer),
24 | response.bodyAsText(Charset.forName("UTF-8"))
25 | )
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/subroh0508/colormaster/androidapp/routes.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.androidapp
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import androidx.activity.ComponentActivity
6 | import net.subroh0508.colormaster.androidapp.pages.activity.PreviewActivity
7 | import kotlin.reflect.KClass
8 |
9 | fun Context.intentTo(kClass: KClass) = Intent().also {
10 | it.setClass(this, kClass.java)
11 | }
12 |
13 | const val PREVIEW_IDOL_IDS = "PREVIEW_IDOL_IDS"
14 | enum class ScreenType {
15 | Preview, Penlight;
16 |
17 | companion object {
18 | const val KEY = "ScreenType.KEY"
19 | }
20 | }
21 |
22 | fun Context.intentToPreview(type: ScreenType, ids: List) = intentTo(PreviewActivity::class).also {
23 | it.putExtra(ScreenType.KEY, type)
24 | it.putExtra(PREVIEW_IDOL_IDS, ids.toTypedArray())
25 | }
26 |
27 | val Intent.screenType get() = getSerializableExtra(ScreenType.KEY) as ScreenType
28 | val Intent.previewIdolIds get() = getStringArrayExtra(PREVIEW_IDOL_IDS)?.toList() ?: listOf()
29 |
--------------------------------------------------------------------------------
/core/test/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("colormaster.primitive.kmp")
3 | id("colormaster.primitive.kmp.android")
4 | id("colormaster.primitive.kmp.js")
5 | }
6 |
7 | kotlin {
8 | sourceSets {
9 | val commonMain by getting {
10 | dependencies {
11 | implementation(project(":core:model"))
12 | implementation(project(":core:network:auth"))
13 | implementation(project(":core:network:firestore"))
14 | implementation(libs.ktor.client.core)
15 | implementation(libs.ktor.client.mock)
16 | implementation(libs.firebase.firestore)
17 | implementation(libs.kotest.framework.engine)
18 | implementation(libs.kotest.assertions.core)
19 | }
20 | }
21 | val androidMain by getting {
22 | dependencies {
23 | implementation(libs.androidx.annotation)
24 | implementation(libs.kotlinx.coroutines.test)
25 | }
26 | }
27 | }
28 | }
29 |
30 | android { namespace = "net.subroh0508.colormaster.test" }
31 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/components/atoms/menu/MenuButton.kt:
--------------------------------------------------------------------------------
1 | package components.atoms.menu
2 |
3 | import androidx.compose.runtime.Composable
4 | import components.atoms.button.TextButton
5 | import material.components.Icon
6 | import material.components.Menu
7 | import org.jetbrains.compose.web.attributes.AttrsScope
8 | import org.jetbrains.compose.web.dom.Text
9 | import org.w3c.dom.HTMLButtonElement
10 |
11 | @Composable
12 | fun MenuButton(
13 | id: String,
14 | applyAttrs: (AttrsScope.() -> Unit)? = null,
15 | icon: String,
16 | tooltip: @Composable (String) -> Unit,
17 | menuContent: @Composable () -> Unit,
18 | ) {
19 | Menu(
20 | anchor = { menu ->
21 | TextButton(
22 | {
23 | applyAttrs?.invoke(this)
24 | attr("aria-describedby", id)
25 | onClick { menu?.open = true }
26 | },
27 | label = { Icon(icon) },
28 | trailingIcon = { Text("expand_more") },
29 | )
30 | },
31 | ) { menuContent() }
32 |
33 | tooltip(id)
34 | }
35 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/components/atoms/textfield/OutlinedTextField.kt:
--------------------------------------------------------------------------------
1 | package components.atoms.textfield
2 |
3 | import androidx.compose.runtime.Composable
4 | import material.components.OutlinedTextArea
5 | import org.jetbrains.compose.web.attributes.AttrsScope
6 | import org.jetbrains.compose.web.css.percent
7 | import org.jetbrains.compose.web.css.width
8 | import org.jetbrains.compose.web.dom.Div
9 | import org.jetbrains.compose.web.events.SyntheticInputEvent
10 | import org.w3c.dom.HTMLDivElement
11 | import org.w3c.dom.HTMLTextAreaElement
12 |
13 | @Composable
14 | fun OutlinedTextField(
15 | id: String,
16 | label: String,
17 | value: String?,
18 | attrs: (AttrsScope.() -> Unit)? = null,
19 | disabled: Boolean = false,
20 | onChange: (SyntheticInputEvent) -> Unit,
21 | trailing: (@Composable () -> Unit)? = null,
22 | ) = Div(attrs) {
23 | OutlinedTextArea(
24 | label,
25 | value,
26 | id,
27 | { style { width(100.percent) } },
28 | disabled,
29 | onChange,
30 | trailing = trailing,
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/core/data/src/androidUnitTest/kotlin/net/subroh0508/colormaster/data/spec/DefaultAuthRepositorySpec.android.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.spec
2 |
3 | import io.kotest.core.spec.style.FunSpec
4 | import io.kotest.matchers.collections.containExactly
5 | import io.kotest.matchers.collections.haveSize
6 | import io.kotest.matchers.should
7 | import net.subroh0508.colormaster.data.module.buildAuthRepository
8 | import net.subroh0508.colormaster.test.extension.flowToList
9 | import net.subroh0508.colormaster.test.model.GoogleUser
10 |
11 | class AndroidDefaultAuthRepositorySpec : FunSpec({
12 | test("#getCurrentUserStream: it should return current user") {
13 | val repository = buildAuthRepository()
14 |
15 | val (instances, _) = flowToList(repository.getCurrentUserStream())
16 |
17 | repository.signInWithGoogle("idToken")
18 | repository.signOut()
19 |
20 | instances.let {
21 | it should haveSize(3)
22 | it should containExactly(
23 | null,
24 | GoogleUser,
25 | null,
26 | )
27 | }
28 | }
29 | })
30 |
--------------------------------------------------------------------------------
/core/model/src/commonMain/kotlin/net/subroh0508/colormaster/model/IdolColorsRepository.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.model
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | interface IdolColorsRepository {
6 | fun getIdolColorsStream(limit: Int, lang: String): Flow>
7 |
8 | fun getInChargeOfIdolIdsStream(): Flow>
9 |
10 | fun getFavoriteIdolIdsStream(): Flow>
11 |
12 | suspend fun refresh()
13 |
14 | suspend fun rand(limit: Int, lang: String): List
15 |
16 | suspend fun search(name: IdolName?, brands: Brands?, types: Set, lang: String): List
17 |
18 | suspend fun search(liveName: LiveName, lang: String): List
19 |
20 | suspend fun search(ids: List, lang: String): List
21 |
22 | suspend fun getInChargeOfIdolIds(): List
23 |
24 | suspend fun getFavoriteIdolIds(): List
25 |
26 | suspend fun registerInChargeOf(id: String)
27 |
28 | suspend fun unregisterInChargeOf(id: String)
29 |
30 | suspend fun favorite(id: String)
31 |
32 | suspend fun unfavorite(id: String)
33 | }
34 |
--------------------------------------------------------------------------------
/core/features/home/src/jsMain/kotlin/net/subroh0508/colormaster/features/home/SubscribeCurrentUserUseCase.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.home
2 |
3 | import androidx.compose.runtime.*
4 | import kotlinx.coroutines.flow.launchIn
5 | import kotlinx.coroutines.flow.onEach
6 | import net.subroh0508.colormaster.common.CurrentLocalKoinApp
7 | import net.subroh0508.colormaster.model.auth.AuthRepository
8 | import net.subroh0508.colormaster.model.auth.CurrentUser
9 | import org.koin.core.KoinApplication
10 |
11 | @Composable
12 | fun rememberSubscribeCurrentUserUseCase(
13 | koinApp: KoinApplication = CurrentLocalKoinApp(),
14 | ): State {
15 | val scope = rememberCoroutineScope()
16 | val repository: AuthRepository by remember(koinApp) { mutableStateOf(koinApp.koin.get()) }
17 |
18 | return produceState(
19 | initialValue = null,
20 | ) {
21 | repository.getCurrentUserStream().onEach {
22 | value = it
23 | }.launchIn(scope)
24 | }
25 | }
26 |
27 | val CurrentUser?.isSignedOut get() = this?.isAnonymous != false
28 | val CurrentUser?.isSignedIn get() = !isSignedOut
29 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/query/SearchByIdQuery.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.query
2 |
3 | import net.subroh0508.colormaster.network.imasparql.internal.ESCAPED_ENDPOINT_RDFS_DETAIL
4 |
5 | class SearchByIdQuery(
6 | lang: String,
7 | ids: List,
8 | ) : ImasparqlQuery() {
9 | override val rawQuery = """
10 | SELECT ?id ?name ?color WHERE {
11 | ?s imas:Color ?color;
12 | imas:Brand ?brand.
13 | OPTIONAL { ?s schema:name ?realName. FILTER(lang(?realName) = '$lang') }
14 | OPTIONAL { ?s schema:alternateName ?altName. FILTER(lang(?altName) = '$lang') }
15 | OPTIONAL { ?s schema:givenName ?givenName. FILTER(lang(?givenName) = '$lang') }
16 | BIND (COALESCE(?altName, ?realName, ?givenName) as ?name)
17 | FILTER (str(?brand) != '1stVision').
18 | BIND (REPLACE(str(?s), '${ESCAPED_ENDPOINT_RDFS_DETAIL}', '') as ?id).
19 | ${ids.takeIf(List::isNotEmpty)?.let { "FILTER (regex(?id, '(${it.joinToString("|")})', 'i'))." }}
20 | }
21 | """.trimIndentAndBr()
22 | }
23 |
--------------------------------------------------------------------------------
/core/test/src/androidMain/kotlin/net/subroh0508/colormaster/test/fake/FakeAuthClient.android.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.test.fake
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.MutableStateFlow
5 | import net.subroh0508.colormaster.network.auth.AuthClient
6 | import net.subroh0508.colormaster.network.auth.model.FirebaseUser
7 | import net.subroh0508.colormaster.test.data.anonymous
8 | import net.subroh0508.colormaster.test.data.fromGoogle
9 |
10 | actual class FakeAuthClient : AuthClient {
11 | private val currentUserStateFlow = MutableStateFlow(null)
12 |
13 | actual override val currentUser: FirebaseUser? get() = currentUserStateFlow.value
14 |
15 | actual override suspend fun signInAnonymously() {
16 | currentUserStateFlow.value = anonymous
17 | }
18 |
19 | actual override suspend fun signOut() {
20 | currentUserStateFlow.value = null
21 | }
22 |
23 | actual override fun subscribeAuthState(): Flow = currentUserStateFlow
24 |
25 | override suspend fun signInWithGoogle(idToken: String) {
26 | currentUserStateFlow.value = fromGoogle
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/backend/cli/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm")
3 | alias(libs.plugins.kotlinx.serialization)
4 | }
5 |
6 | dependencies {
7 | implementation(project(":backend:server"))
8 |
9 | // Add Ktor client dependencies
10 | implementation(libs.ktor.client.core)
11 | implementation(libs.ktor.client.okhttp) // For JVM platform
12 | implementation(libs.ktor.client.json)
13 | implementation(libs.ktor.serialization.core)
14 | implementation(libs.ktor.serialization.kotlinx.json)
15 | implementation(libs.kotlinx.coroutines.core)
16 |
17 | // Logging
18 | implementation(libs.okhttp3.logging.interceptor)
19 |
20 | // SQLDelight
21 | implementation(libs.sqldelight.jvm.driver)
22 | implementation(libs.sqldelight.coroutines.extensions)
23 | implementation(libs.sqldelight.primitive.adapters)
24 | }
25 |
26 | val fetchIdolColorsFromImasparql by tasks.registering(JavaExec::class) {
27 | group = "colormaster"
28 | description = "Fetch idol member colors from im@sparql"
29 | mainClass.set("net.subroh0508.colormaster.backend.cli.commands.FetchIdolColorsFromImasparqlCommand")
30 | classpath(sourceSets["main"].runtimeClasspath)
31 | }
32 |
--------------------------------------------------------------------------------
/backend/server/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm")
3 | application
4 | alias(libs.plugins.ktor)
5 | alias(libs.plugins.sqldelight)
6 | alias(libs.plugins.kotlinx.serialization)
7 | }
8 |
9 | dependencies {
10 |
11 | // Ktor サーバーフレームワーク
12 | implementation(libs.ktor.server.core)
13 | implementation(libs.ktor.server.netty)
14 | implementation(libs.ktor.server.content.negotiation)
15 | implementation(libs.ktor.serialization.kotlinx.json)
16 |
17 | // SQLDelight
18 | implementation(libs.sqldelight.jvm.driver)
19 | implementation(libs.sqldelight.coroutines.extensions)
20 | implementation(libs.sqldelight.primitive.adapters)
21 |
22 | // ロギング
23 | implementation(libs.logback.classic)
24 |
25 | // テスト
26 | testImplementation(libs.ktor.server.test.host)
27 | testImplementation(kotlin("test"))
28 | }
29 |
30 | application {
31 | mainClass.set("net.subroh0508.colormaster.backend.ApplicationKt")
32 | }
33 |
34 | sqldelight {
35 | databases {
36 | create("ColorMasterDatabase") {
37 | packageName.set("net.subroh0508.colormaster.backend.database")
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/backend/server/src/main/sqldelight/net/subroh0508/colormaster/backend/database/Idol.sq:
--------------------------------------------------------------------------------
1 | CREATE TABLE idol (
2 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
3 | name_ja TEXT NOT NULL,
4 | name_kana_ja TEXT NOT NULL,
5 | name_en TEXT NOT NULL,
6 | color TEXT NOT NULL,
7 | content_category TEXT NOT NULL,
8 | content_title TEXT NOT NULL
9 | );
10 |
11 | insertIdol:
12 | INSERT INTO idol(name_ja, name_kana_ja, name_en, color, content_category, content_title)
13 | VALUES (?, ?, ?, ?, ?, ?);
14 |
15 | selectAll:
16 | SELECT *
17 | FROM idol;
18 |
19 | selectById:
20 | SELECT *
21 | FROM idol
22 | WHERE id = ?;
23 |
24 | selectByContentCategory:
25 | SELECT *
26 | FROM idol
27 | WHERE content_category = ?;
28 |
29 | selectByContentTitle:
30 | SELECT *
31 | FROM idol
32 | WHERE content_title = ?;
33 |
34 | selectByNameLike:
35 | SELECT *
36 | FROM idol
37 | WHERE name_ja LIKE '%' || ? || '%' OR name_kana_ja LIKE '%' || ? || '%' OR name_en LIKE '%' || ? || '%';
38 |
39 | selectByNameEnAndContentCategoryAndTitle:
40 | SELECT *
41 | FROM idol
42 | WHERE name_en = ? AND content_category = ? AND content_title = ?;
43 |
44 | deleteById:
45 | DELETE FROM idol
46 | WHERE id = ?;
47 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/subroh0508/colormaster/androidapp/themes/Type.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.androidapp.themes
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | val captionTextStyle = TextStyle(
10 | fontFamily = FontFamily.Default,
11 | fontWeight = FontWeight.W400,
12 | fontSize = 16.sp
13 | )
14 |
15 | // Set of Material typography styles to start with
16 | val typography = Typography(
17 | body1 = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.Normal,
20 | fontSize = 16.sp
21 | )
22 | /* Other default text styles to override
23 | button = TextStyle(
24 | fontFamily = FontFamily.Default,
25 | fontWeight = FontWeight.W500,
26 | fontSize = 14.sp
27 | ),
28 | caption = TextStyle(
29 | fontFamily = FontFamily.Default,
30 | fontWeight = FontWeight.Normal,
31 | fontSize = 12.sp
32 | )
33 | */
34 | )
35 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
15 |
23 |
31 |
32 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/components/molecules/icon/ActionIcons.kt:
--------------------------------------------------------------------------------
1 | package components.molecules.icon
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.web.events.SyntheticMouseEvent
5 | import material.components.Icon
6 | import org.jetbrains.compose.web.attributes.AttrsScope
7 | import org.jetbrains.compose.web.css.*
8 | import org.jetbrains.compose.web.dom.Div
9 | import org.jetbrains.compose.web.dom.Span
10 | import org.w3c.dom.HTMLDivElement
11 |
12 | @Composable
13 | fun ActionIcons(
14 | attrsScope: (AttrsScope.() -> Unit)?,
15 | leading: @Composable () -> Unit,
16 | trailing: @Composable () -> Unit,
17 | ) {
18 | Style(ActionIconsStyle)
19 |
20 | Div({
21 | classes(ActionIconsStyle.content)
22 | attrsScope?.invoke(this)
23 | }) {
24 | leading()
25 | Span({ style { flexGrow(1) } })
26 | trailing()
27 | }
28 | }
29 |
30 | private object ActionIconsStyle : StyleSheet() {
31 | val content by style {
32 | display(DisplayStyle.Flex)
33 | justifyContent(JustifyContent.FlexEnd)
34 |
35 | (className("material-icons") + not(lastChild)) style {
36 | marginRight(4.px)
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/core/data/src/jsTest/kotlin/net/subroh0508/colormaster/data/spec/DefaultAuthRepositorySpec.js.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.spec
2 |
3 | import io.kotest.core.spec.style.FunSpec
4 | import io.kotest.matchers.collections.containExactly
5 | import io.kotest.matchers.collections.haveSize
6 | import io.kotest.matchers.should
7 | import net.subroh0508.colormaster.data.module.buildAuthRepository
8 | import net.subroh0508.colormaster.test.model.GoogleUser
9 | import net.subroh0508.colormaster.test.extension.flowToList
10 |
11 | class JsDefaultAuthRepositorySpec : FunSpec({
12 | test("#getCurrentUserStream: it should return current user") {
13 | val repository = buildAuthRepository()
14 |
15 | val (instances, _) = flowToList(repository.getCurrentUserStream())
16 |
17 | repository.signInWithGoogle()
18 | repository.signOut()
19 | repository.signInWithGoogleForMobile()
20 | repository.signOut()
21 |
22 | instances.let {
23 | it should haveSize(5)
24 | it should containExactly(
25 | null,
26 | GoogleUser,
27 | null,
28 | GoogleUser,
29 | null,
30 | )
31 | }
32 | }
33 | })
34 |
--------------------------------------------------------------------------------
/android/app/google-services.json:
--------------------------------------------------------------------------------
1 | {
2 | "project_info": {
3 | "project_number": "1084041594481",
4 | "firebase_url": "https://imas-colormaster.firebaseio.com",
5 | "project_id": "imas-colormaster",
6 | "storage_bucket": "imas-colormaster.appspot.com"
7 | },
8 | "client": [
9 | {
10 | "client_info": {
11 | "mobilesdk_app_id": "1:1084041594481:android:1626c3a293d503dba1bdd0",
12 | "android_client_info": {
13 | "package_name": "net.subroh0508.colormaster"
14 | }
15 | },
16 | "oauth_client": [
17 | {
18 | "client_id": "1084041594481-r82aj4pegif8o5hon2dodenpq7jqpurf.apps.googleusercontent.com",
19 | "client_type": 3
20 | }
21 | ],
22 | "api_key": [
23 | {
24 | "current_key": "AIzaSyAU6clPkJtbLpQKlj83yFMnzLiHTSDzwJs"
25 | }
26 | ],
27 | "services": {
28 | "appinvite_service": {
29 | "other_platform_oauth_client": [
30 | {
31 | "client_id": "1084041594481-r82aj4pegif8o5hon2dodenpq7jqpurf.apps.googleusercontent.com",
32 | "client_type": 3
33 | }
34 | ]
35 | }
36 | }
37 | }
38 | ],
39 | "configuration_version": "1"
40 | }
--------------------------------------------------------------------------------
/core/test/src/jsMain/kotlin/net/subroh0508/colormaster/test/fake/FakeAuthClient.js.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.test.fake
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.MutableStateFlow
5 | import net.subroh0508.colormaster.network.auth.AuthClient
6 | import net.subroh0508.colormaster.network.auth.model.FirebaseUser
7 | import net.subroh0508.colormaster.test.data.anonymous
8 | import net.subroh0508.colormaster.test.data.fromGoogle
9 |
10 | actual class FakeAuthClient : AuthClient {
11 | private val currentUserStateFlow = MutableStateFlow(null)
12 |
13 | actual override val currentUser: FirebaseUser? get() = currentUserStateFlow.value
14 |
15 | actual override suspend fun signInAnonymously() {
16 | currentUserStateFlow.value = anonymous
17 | }
18 |
19 | actual override suspend fun signOut() {
20 | currentUserStateFlow.value = null
21 | }
22 |
23 | actual override fun subscribeAuthState(): Flow = currentUserStateFlow
24 |
25 | override suspend fun signInWithGoogle() {
26 | currentUserStateFlow.value = fromGoogle
27 | }
28 | override suspend fun signInWithGoogleForMobile() {
29 | currentUserStateFlow.value = fromGoogle
30 | }
31 | }
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/components/templates/Routing.kt:
--------------------------------------------------------------------------------
1 | package components.templates
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.getValue
5 | import page.MyIdolsPage
6 | import page.PenlightPage
7 | import page.PreviewPage
8 | import page.about.HowToUsePage
9 | import page.SearchIdolPage
10 | import page.about.DevelopmentPage
11 | import page.about.TermsPage
12 | import routes.*
13 | import utilities.subscribeAsState
14 |
15 | @Composable
16 | fun Routing(
17 | topAppBarVariant: String,
18 | isSignedIn: Boolean,
19 | ) {
20 | val router = CurrentLocalRouter() ?: return
21 | val routerState by router.stack.subscribeAsState()
22 |
23 | routerState.active.instance.let {
24 | when (it) {
25 | is Search -> SearchIdolPage(topAppBarVariant, isSignedIn, it.initParams)
26 | is MyIdols -> MyIdolsPage(topAppBarVariant, isSignedIn)
27 | is Preview -> PreviewPage(topAppBarVariant, it.ids)
28 | is Penlight -> PenlightPage(topAppBarVariant, it.ids)
29 | is HowToUse -> HowToUsePage(topAppBarVariant)
30 | is Development -> DevelopmentPage(topAppBarVariant)
31 | is Terms -> TermsPage(topAppBarVariant)
32 | else -> Unit
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/subroh0508/colormaster/androidapp/components/atoms/DrawerHeader.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.androidapp.components.atoms
2 |
3 | import androidx.compose.material.Text
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.material.Divider
6 | import androidx.compose.material.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.unit.dp
11 |
12 | @Composable
13 | fun DrawerHeader(title: String, subtext: String) {
14 | Spacer(Modifier.height(24.dp))
15 | Column(
16 | modifier = Modifier.fillMaxWidth()
17 | .padding(start = 16.dp, bottom = 18.dp),
18 | ) {
19 | Text(
20 | text = title,
21 | style = MaterialTheme.typography.h6,
22 | modifier = Modifier.height(36.dp)
23 | .wrapContentHeight(Alignment.Bottom),
24 | )
25 | Text(
26 | text = subtext,
27 | style = MaterialTheme.typography.caption,
28 | modifier = Modifier.height(20.dp)
29 | .wrapContentHeight(Alignment.Bottom),
30 | )
31 | }
32 | Divider(color = MaterialTheme.colors.onSurface.copy(alpha = .12F))
33 | }
34 |
--------------------------------------------------------------------------------
/core/features/preview/src/commonMain/kotlin/net/subroh0508/colormaster/features/preview/viewmodel/PenlightViewModel.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.preview.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import kotlinx.coroutines.flow.SharingStarted
6 | import kotlinx.coroutines.flow.StateFlow
7 | import kotlinx.coroutines.flow.map
8 | import kotlinx.coroutines.flow.stateIn
9 | import net.subroh0508.colormaster.model.PreviewRepository
10 |
11 | class PenlightViewModel(
12 | private val withDescription: Boolean,
13 | private val repository: PreviewRepository,
14 | ) : ViewModel() {
15 | val uiState: StateFlow = repository.getPreviewColorsStream()
16 | .map { idols ->
17 | when (idols.isEmpty()) {
18 | true -> PenlightUiState.Loading
19 | false -> PenlightUiState.Loaded(idols, withDescription)
20 | }
21 | }.stateIn(
22 | scope = viewModelScope,
23 | started = SharingStarted.WhileSubscribed(5_000),
24 | initialValue = PenlightUiState.Loading,
25 | )
26 |
27 | suspend fun show(ids: List, lang: String) {
28 | repository.clear()
29 | repository.show(ids, lang)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/components/atoms/list/ListGroupSubHeader.kt:
--------------------------------------------------------------------------------
1 | package components.atoms.list
2 |
3 | import androidx.compose.runtime.Composable
4 | import material.components.Icon
5 | import org.jetbrains.compose.web.css.*
6 | import org.jetbrains.compose.web.dom.Span
7 | import org.jetbrains.compose.web.dom.Text
8 | import material.components.ListGroupSubHeader as MaterialListGroupSubHeader
9 |
10 | @Composable
11 | fun ListGroupSubHeader(label: String) = MaterialListGroupSubHeader(label)
12 |
13 | @Composable
14 | fun ListGroupSubHeader(
15 | id: String,
16 | label: String,
17 | helpContent: @Composable (String) -> Unit,
18 | ) = MaterialListGroupSubHeader(applyAttrs = {
19 | style {
20 | display(DisplayStyle.Flex)
21 | }
22 | }) {
23 | Span({
24 | style {
25 | flexShrink(0)
26 | height(16.px)
27 | marginTop(12.px)
28 | property("vertical-align", "bottom")
29 | }
30 | }) { Text(label) }
31 | Icon("help_outline_icon", applyAttrs = {
32 | attr("aria-describedby", id)
33 |
34 | style {
35 | height(16.px)
36 | width(16.px)
37 | margin(14.px, 2.px, 0.px)
38 | fontSize(16.px)
39 | }
40 | })
41 |
42 | helpContent(id)
43 | }
44 |
--------------------------------------------------------------------------------
/core/features/myidols/src/commonMain/kotlin/net/subroh0508/colormaster/features/myidols/viewmodel/MyIdolsViewModel.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.myidols.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import kotlinx.coroutines.flow.SharingStarted
6 | import kotlinx.coroutines.flow.StateFlow
7 | import kotlinx.coroutines.flow.catch
8 | import kotlinx.coroutines.flow.combine
9 | import kotlinx.coroutines.flow.stateIn
10 | import net.subroh0508.colormaster.model.IdolColor
11 | import net.subroh0508.colormaster.model.MyIdolsRepository
12 |
13 | class MyIdolsViewModel(
14 | private val repository: MyIdolsRepository,
15 | ) : ViewModel() {
16 | val uiState: StateFlow = combine<
17 | List,
18 | List,
19 | MyIdolsUiState,
20 | >(
21 | repository.getInChargeOfIdolsStream("ja"),
22 | repository.getFavoriteIdolsStream("ja"),
23 | ) { inCharges, favorites ->
24 | MyIdolsUiState.Loaded(inCharges, favorites)
25 | }.catch { e ->
26 | emit(MyIdolsUiState.Error(e))
27 | }.stateIn(
28 | scope = viewModelScope,
29 | started = SharingStarted.WhileSubscribed(5_000),
30 | initialValue = MyIdolsUiState.Loading,
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/subroh0508/colormaster/androidapp/components/molecules/ScrollableTabs.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.androidapp.components.molecules
2 |
3 | import androidx.compose.material.MaterialTheme
4 | import androidx.compose.material.ScrollableTabRow
5 | import androidx.compose.runtime.*
6 | import androidx.compose.ui.Modifier
7 | import net.subroh0508.colormaster.androidapp.components.atoms.Tab
8 |
9 | @Composable
10 | fun ScrollableTabs(
11 | titles: Array,
12 | modifier: Modifier = Modifier,
13 | selectedIndex: Int = 0,
14 | onClick: (String) -> Unit = {},
15 | ) {
16 | var tabSelected by remember { mutableStateOf(selectedIndex) }
17 |
18 | ScrollableTabRow(
19 | selectedTabIndex = tabSelected,
20 | backgroundColor = MaterialTheme.colors.background,
21 | contentColor = MaterialTheme.colors.onSurface,
22 | indicator = {},
23 | divider = {},
24 | modifier = modifier
25 | ) {
26 | titles.forEachIndexed { index, title ->
27 | Tab(
28 | title,
29 | index == tabSelected,
30 | onClick = {
31 | tabSelected = index
32 | onClick(title)
33 | },
34 | )
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/core/network/firestore/src/commonMain/kotlin/net/subroh0508/colormaster/network/firestore/internal/FirestoreClientImpl.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.firestore.internal
2 |
3 | import dev.gitlive.firebase.firestore.FirebaseFirestore
4 | import net.subroh0508.colormaster.network.firestore.COLLECTION_USERS
5 | import net.subroh0508.colormaster.network.firestore.FirestoreClient
6 | import net.subroh0508.colormaster.network.firestore.document.UserDocument
7 |
8 | internal class FirestoreClientImpl(
9 | private val firestore: FirebaseFirestore,
10 | ) : FirestoreClient {
11 | private fun getUsersCollection() = firestore.collection(COLLECTION_USERS)
12 |
13 | override suspend fun setUserDocument(
14 | uid: String,
15 | userDocument: UserDocument,
16 | ) {
17 | getUsersCollection().document(uid).set(
18 | UserDocument.serializer(),
19 | userDocument,
20 | merge = true,
21 | )
22 | }
23 |
24 | override suspend fun getUserDocument(uid: String?): UserDocument {
25 | uid ?: return UserDocument()
26 |
27 | return getUsersCollection().document(uid)
28 | .get()
29 | .takeIf { it.exists }
30 | ?.data(UserDocument.serializer())
31 | ?: UserDocument()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/js/app/src/jsMain/kotlin/page/SearchIdolPage.kt:
--------------------------------------------------------------------------------
1 | package page
2 |
3 | import androidx.compose.runtime.*
4 | import components.atoms.backdrop.Backdrop
5 | import components.atoms.backdrop.BackdropValues
6 | import components.atoms.backdrop.rememberBackdropState
7 | import components.templates.search.BackLayer
8 | import components.templates.search.frontlayer.FrontLayer
9 | import net.subroh0508.colormaster.features.search.model.SearchByTab
10 | import net.subroh0508.colormaster.features.search.model.SearchParams
11 |
12 | @Composable
13 | fun SearchIdolPage(
14 | topAppBarVariant: String,
15 | isSignedIn: Boolean,
16 | initParams: SearchParams,
17 | ) {
18 | val backdropState = rememberBackdropState(BackdropValues.Concealed)
19 | val (params, setParams) = remember(initParams) { mutableStateOf(initParams) }
20 |
21 | val tab = when (initParams) {
22 | is SearchParams.ByName -> SearchByTab.BY_NAME
23 | is SearchParams.ByLive -> SearchByTab.BY_LIVE
24 | else -> SearchByTab.BY_NAME
25 | }
26 |
27 | Backdrop(
28 | backdropState,
29 | backLayerContent = {
30 | BackLayer(topAppBarVariant, tab, setParams)
31 | },
32 | frontLayerContent = {
33 | FrontLayer(backdropState, isSignedIn, params)
34 | },
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/plugins/src/main/kotlin/net/subroh0508/colormaster/convention/ApiModulePlugin.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.convention
2 |
3 | import net.subroh0508.colormaster.koinCore
4 | import net.subroh0508.colormaster.kotlinxCoroutinesCore
5 | import net.subroh0508.colormaster.kotlinxSerialization
6 | import net.subroh0508.colormaster.libs
7 | import net.subroh0508.colormaster.primitive.kmp.applyKmpPlugins
8 | import net.subroh0508.colormaster.primitive.kmp.kotlin
9 | import org.gradle.api.Plugin
10 | import org.gradle.api.Project
11 |
12 | @Suppress("unused")
13 | class ApiModulePlugin : Plugin {
14 | override fun apply(target: Project) {
15 | with (target) {
16 | with (plugins) {
17 | applyKmpPlugins()
18 | apply("org.jetbrains.kotlin.plugin.serialization")
19 | }
20 |
21 | kotlin {
22 | with (sourceSets) {
23 | commonMain {
24 | dependencies {
25 | implementation(libs.kotlinxCoroutinesCore)
26 | implementation(libs.kotlinxSerialization)
27 | implementation(libs.koinCore)
28 | }
29 | }
30 | }
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/subroh0508/colormaster/androidapp/components/atoms/Tab.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.androidapp.components.atoms
2 |
3 | import androidx.compose.foundation.BorderStroke
4 | import androidx.compose.material.Text
5 | import androidx.compose.foundation.border
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.shape.RoundedCornerShape
8 | import androidx.compose.material.MaterialTheme
9 | import androidx.compose.material.Tab as MaterialTab
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.unit.dp
13 |
14 | @Composable
15 | fun Tab(
16 | title: String,
17 | selected: Boolean,
18 | onClick: () -> Unit,
19 | ) {
20 | var textModifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp)
21 | if (selected) {
22 | textModifier =
23 | Modifier.border(BorderStroke(2.dp, MaterialTheme.colors.onSurface), RoundedCornerShape(16.dp))
24 | .then(textModifier)
25 | }
26 |
27 | MaterialTab(
28 | selected = selected,
29 | onClick = onClick,
30 | ) {
31 | Text(
32 | title,
33 | style = MaterialTheme.typography.button,
34 | modifier = textModifier,
35 | )
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/components/Ripple.kt:
--------------------------------------------------------------------------------
1 | package material.components
2 |
3 | import androidx.compose.runtime.*
4 | import material.externals.attachRippleTo
5 | import org.jetbrains.compose.web.attributes.AttrsScope
6 | import org.jetbrains.compose.web.dom.TagElement
7 | import org.w3c.dom.HTMLElement
8 | import material.utilities.TagElementBuilder
9 |
10 |
11 | @Composable
12 | fun Ripple(
13 | applyAttrs: (AttrsScope.() -> Unit)? = null,
14 | tag: String = "div",
15 | unbounded: Boolean = false,
16 | content: @Composable () -> Unit,
17 | ) {
18 | val element = rememberRippleElement(unbounded)
19 |
20 | TagElement(
21 | TagElementBuilder(tag),
22 | {
23 | applyAttrs?.invoke(this)
24 |
25 | classes("mdc-ripple-surface")
26 | ref {
27 | element.value = it
28 | onDispose { element.value = null }
29 | }
30 | },
31 | ) { content() }
32 | }
33 |
34 | @Composable
35 | fun rememberRippleElement(unbounded: Boolean = false): MutableState {
36 | val element = remember { mutableStateOf(null) }
37 |
38 | SideEffect {
39 | element.value?.let {
40 | attachRippleTo(it) { isUnbounded = unbounded }
41 | }
42 | }
43 |
44 | return element
45 | }
46 |
--------------------------------------------------------------------------------
/core/features/home/src/jsMain/kotlin/net/subroh0508/colormaster/features/home/JsSignInUseCase.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.home
2 |
3 | import androidx.compose.runtime.*
4 | import kotlinx.browser.window
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.launch
7 | import net.subroh0508.colormaster.model.auth.AuthRepository
8 | import org.koin.core.KoinApplication
9 |
10 | actual class SignInUseCase(
11 | private val isMobile: Boolean,
12 | private val repository: AuthRepository,
13 | private val scope: CoroutineScope,
14 | ) {
15 | operator fun invoke() {
16 | scope.launch {
17 | runCatching {
18 | if (isMobile)
19 | repository.signInWithGoogleForMobile()
20 | else
21 | repository.signInWithGoogle()
22 | }
23 | }
24 | }
25 | }
26 |
27 | @Composable
28 | actual fun rememberSignInUseCase(
29 | koinApp: KoinApplication,
30 | ): SignInUseCase {
31 | val scope = rememberCoroutineScope()
32 | val repository: AuthRepository by remember(koinApp) { mutableStateOf(koinApp.koin.get()) }
33 |
34 | return remember(koinApp) { SignInUseCase(isMobile, repository, scope) }
35 | }
36 |
37 | private val isMobile: Boolean get() = """(iPhone|iPad|Android)""".toRegex().matches(window.navigator.userAgent)
38 |
--------------------------------------------------------------------------------
/core/data/src/commonTest/kotlin/net/subroh0508/colormaster/data/module/MyIdolsRepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.module
2 |
3 | import io.ktor.client.HttpClient
4 | import net.subroh0508.colormaster.data.di.MyIdolsRepositories
5 | import net.subroh0508.colormaster.model.MyIdolsRepository
6 | import net.subroh0508.colormaster.network.auth.AuthClient
7 | import net.subroh0508.colormaster.network.firestore.FirestoreClient
8 | import net.subroh0508.colormaster.network.imasparql.di.Api
9 | import net.subroh0508.colormaster.test.fake.FakeAuthClient
10 | import net.subroh0508.colormaster.test.fake.FakeFirestoreClient
11 | import org.koin.dsl.koinApplication
12 | import org.koin.dsl.module
13 |
14 | internal fun buildMyIdolsRepository(
15 | block: () -> HttpClient,
16 | ): Triple {
17 | val authClient: AuthClient = FakeAuthClient()
18 | val firestoreClient: FirestoreClient = FakeFirestoreClient()
19 |
20 | val repository: MyIdolsRepository = koinApplication {
21 | modules(
22 | Api.Module(block()) + module {
23 | single { authClient }
24 | single { firestoreClient }
25 | } + MyIdolsRepositories.Module
26 | )
27 | }.koin.get(MyIdolsRepository::class)
28 |
29 | return Triple(repository, authClient, firestoreClient)
30 | }
31 |
--------------------------------------------------------------------------------
/js/app/webpack.config.d/03.constants.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 |
3 | const prod = config.mode === 'production';
4 |
5 | const API_KEY = prod ? 'AIzaSyAa9XK8rRymTOsfNpb2Fejn6aAo7A0c13o' : 'AIzaSyAEg1EDJyytx_9M-6r63osIdOvmHW1qr_I';
6 | const AUTH_DOMAIN = prod ? 'imas-colormaster.firebaseapp.com' : 'color-master-2875c.firebaseapp.com';
7 | const DATABASE_URL = prod ? 'https://imas-colormaster.firebaseio.com' : 'https://color-master-2875c.firebaseio.com';
8 | const PROJECT_ID = prod ? 'imas-colormaster' : 'color-master-2875c';
9 | const STORAGE_BUCKET = prod ? 'imas-colormaster.appspot.com' : 'color-master-2875c.appspot.com';
10 | const MESSAGING_SENDER_ID = prod ? '1084041594481' : '1067956718797';
11 | const APP_ID = prod ? '1:1084041594481:web:d21eb204c9ebae39a1bdd0' : '1:1067956718797:web:9c04b4cef92b722d4b2d84';
12 | const MEASUREMENT_ID = prod ? 'G-XV1LEWMH6K' : 'G-JPJRS76T16';
13 |
14 | config.plugins.push(
15 | new webpack.DefinePlugin({
16 | APP_NAME: '"COLOR M@STER"',
17 | APP_VERSION: '"v2025.05.06"',
18 | API_KEY: `"${API_KEY}"`,
19 | AUTH_DOMAIN: `"${AUTH_DOMAIN}"`,
20 | DATABASE_URL: `"${DATABASE_URL}"`,
21 | PROJECT_ID: `"${PROJECT_ID}"`,
22 | STORAGE_BUCKET: `"${STORAGE_BUCKET}"`,
23 | MESSAGING_SENDER_ID: `"${MESSAGING_SENDER_ID}"`,
24 | APP_ID: `"${APP_ID}"`,
25 | MEASUREMENT_ID: `"${MEASUREMENT_ID}"`,
26 | }),
27 | );
28 |
--------------------------------------------------------------------------------
/plugins/src/main/kotlin/net/subroh0508/colormaster/convention/DataModulePlugin.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.convention
2 |
3 | import net.subroh0508.colormaster.koinCore
4 | import net.subroh0508.colormaster.kotlinxCoroutinesCore
5 | import net.subroh0508.colormaster.libs
6 | import net.subroh0508.colormaster.primitive.kmp.applyKmpPlugins
7 | import net.subroh0508.colormaster.primitive.kmp.kotlin
8 | import org.gradle.api.Plugin
9 | import org.gradle.api.Project
10 |
11 | @Suppress("unused")
12 | class DataModulePlugin : Plugin {
13 | override fun apply(target: Project) {
14 | with (target) {
15 | with (plugins) {
16 | applyKmpPlugins()
17 | apply("io.kotest.multiplatform")
18 | }
19 |
20 | kotlin {
21 | with (sourceSets) {
22 | commonMain {
23 | dependencies {
24 | implementation(libs.kotlinxCoroutinesCore)
25 | implementation(libs.koinCore)
26 | }
27 | }
28 | commonTest {
29 | dependencies {
30 | implementation(project(":core:test"))
31 | }
32 | }
33 | }
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/core/data/src/commonTest/kotlin/net/subroh0508/colormaster/data/module/IdolColorsRepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data.module
2 |
3 | import io.ktor.client.HttpClient
4 | import net.subroh0508.colormaster.data.di.IdolColorsRepositories
5 | import net.subroh0508.colormaster.model.IdolColorsRepository
6 | import net.subroh0508.colormaster.network.auth.AuthClient
7 | import net.subroh0508.colormaster.network.firestore.FirestoreClient
8 | import net.subroh0508.colormaster.network.imasparql.di.Api
9 | import net.subroh0508.colormaster.test.fake.FakeAuthClient
10 | import net.subroh0508.colormaster.test.fake.FakeFirestoreClient
11 | import org.koin.dsl.koinApplication
12 | import org.koin.dsl.module
13 |
14 | internal fun buildIdolColorsRepository(
15 | block: () -> HttpClient,
16 | ): Triple {
17 | val authClient: AuthClient = FakeAuthClient()
18 | val firestoreClient: FirestoreClient = FakeFirestoreClient()
19 |
20 | val repository: IdolColorsRepository = koinApplication {
21 | modules(
22 | Api.Module(block()) + module {
23 | single { authClient }
24 | single { firestoreClient }
25 | } + IdolColorsRepositories.Module
26 | )
27 | }.koin.get(IdolColorsRepository::class)
28 |
29 | return Triple(repository, authClient, firestoreClient)
30 | }
31 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/components/Tooltip.kt:
--------------------------------------------------------------------------------
1 | package material.components
2 |
3 | import androidx.compose.runtime.*
4 | import material.externals.MDCTooltip
5 | import org.jetbrains.compose.web.dom.Div
6 | import org.jetbrains.compose.web.dom.Text
7 | import org.w3c.dom.HTMLElement
8 |
9 | @Composable
10 | fun Tooltip(
11 | id: String,
12 | hideDelayMs: Long = 600L,
13 | showDelayMs: Long = 500L,
14 | content: @Composable () -> Unit,
15 | ) {
16 | var tooltip by remember { mutableStateOf(null) }
17 |
18 | SideEffect {
19 | tooltip?.let {
20 | MDCTooltip(it).apply {
21 | setHideDelay(hideDelayMs)
22 | setShowDelay(showDelayMs)
23 | }
24 | }
25 | }
26 |
27 | Div({
28 | id(id)
29 | classes("mdc-tooltip")
30 | attr("role", "tooltip")
31 | attr("aria-hidden", "true")
32 |
33 | ref {
34 | tooltip = it
35 | onDispose { tooltip = null }
36 | }
37 | }) {
38 | Div({ classes("mdc-tooltip__surface", "mdc-tooltip__surface-animation") }) {
39 | content()
40 | }
41 | }
42 | }
43 |
44 | @Composable
45 | fun Tooltip(
46 | id: String,
47 | text: String,
48 | hideDelayMs: Long = 600L,
49 | showDelayMs: Long = 500L,
50 | ) = Tooltip(id, hideDelayMs, showDelayMs) { Text(text) }
51 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/query/SearchByLiveQuery.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.query
2 |
3 | import net.subroh0508.colormaster.network.imasparql.internal.ESCAPED_ENDPOINT_RDFS_DETAIL
4 |
5 | class SearchByLiveQuery(
6 | lang: String,
7 | liveName: String?,
8 | ) : ImasparqlQuery() {
9 | override val rawQuery = """
10 | SELECT ?id ?name ?color WHERE {
11 | ?live rdf:type imas:Live;
12 | schema:name ?liveName;
13 | schema:actor ?actor;
14 | schema:eventStatus ?eventStatus.
15 | FILTER(?eventStatus != schema:EventCancelled)
16 | ${liveName?.let { "FILTER (str(?liveName) = '$it')." } ?: ""}
17 | ?s imas:Color ?color;
18 | imas:Brand ?brand;
19 | imas:cv ?actor.
20 | OPTIONAL { ?s schema:name ?realName. FILTER(lang(?realName) = '$lang') }
21 | OPTIONAL { ?s schema:alternateName ?altName. FILTER(lang(?altName) = '$lang') }
22 | OPTIONAL { ?s schema:givenName ?givenName. FILTER(lang(?givenName) = '$lang') }
23 | BIND (COALESCE(?altName, ?realName, ?givenName) as ?name)
24 | FILTER (str(?brand) != '1stVision').
25 | BIND (REPLACE(str(?s), '${ESCAPED_ENDPOINT_RDFS_DETAIL}', '') as ?id).
26 | }
27 | ORDER BY ?name
28 | """.trimIndentAndBr()
29 | }
30 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/subroh0508/colormaster/androidapp/components/atoms/DrawerButton.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.androidapp.components.atoms
2 |
3 | import androidx.compose.foundation.layout.Spacer
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.height
6 | import androidx.compose.foundation.layout.width
7 | import androidx.compose.material.*
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.vector.ImageVector
11 | import androidx.compose.ui.text.style.TextAlign
12 | import androidx.compose.ui.unit.dp
13 |
14 | @Composable
15 | fun DrawerButton(
16 | asset: ImageVector,
17 | label: String,
18 | onClick: () -> Unit,
19 | ) {
20 | TextButton(
21 | onClick = onClick,
22 | colors = ButtonDefaults.textButtonColors(
23 | contentColor = MaterialTheme.colors.onSurface,
24 | ),
25 | modifier = Modifier.fillMaxWidth()
26 | .height(48.dp),
27 | ) {
28 | Spacer(Modifier.width(16.dp))
29 | Icon(asset, contentDescription = null)
30 | Spacer(Modifier.width(16.dp))
31 | Text(
32 | text = label,
33 | style = MaterialTheme.typography.body2,
34 | textAlign = TextAlign.Justify,
35 | modifier = Modifier.fillMaxWidth(),
36 | )
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/subroh0508/colormaster/androidapp/pages/activity/HomeActivity.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.androidapp.pages.activity
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.ExperimentalFoundationApi
7 | import androidx.compose.foundation.layout.ExperimentalLayoutApi
8 | import androidx.compose.material.*
9 | import androidx.compose.runtime.CompositionLocalProvider
10 | import net.subroh0508.colormaster.androidapp.ScreenType
11 | import net.subroh0508.colormaster.androidapp.intentToPreview
12 | import net.subroh0508.colormaster.androidapp.pages.Home
13 | import net.subroh0508.colormaster.common.LocalKoinApp
14 | import net.subroh0508.colormaster.common.koinApp
15 |
16 | class HomeActivity : ComponentActivity() {
17 | @ExperimentalFoundationApi
18 | @ExperimentalMaterialApi
19 | @ExperimentalLayoutApi
20 | override fun onCreate(savedInstanceState: Bundle?) {
21 | super.onCreate(savedInstanceState)
22 |
23 | setContent {
24 | CompositionLocalProvider(
25 | LocalKoinApp provides koinApp,
26 | ) { Home(::launchPreviewActivity) }
27 | }
28 | }
29 |
30 | private fun launchPreviewActivity(
31 | type: ScreenType,
32 | ids: List,
33 | ) = startActivity(intentToPreview(type, ids))
34 | }
35 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/query/ImasparqlQuery.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.query
2 |
3 | import net.subroh0508.colormaster.network.imasparql.internal.URLEncoder
4 |
5 | abstract class ImasparqlQuery {
6 | companion object {
7 | private const val ENDPOINT_MAIN = "/spql/imas/query"
8 |
9 | private const val PREFIX_SCHEMA = "PREFIX schema: "
10 | private const val PREFIX_IMAS = "PREFIX imas: "
11 | private const val PREFIX_RDF = "PREFIX rdf: "
12 | private const val PREFIX_RDFS = "PREFIX rdfs: "
13 | private const val PREFIX_XSD = "PREFIX xsd: "
14 | }
15 |
16 | protected abstract val rawQuery: String
17 |
18 | val plainQuery get() = buildString {
19 | append(PREFIX_SCHEMA)
20 | append(PREFIX_IMAS)
21 | append(PREFIX_RDF)
22 | append(PREFIX_RDFS)
23 | append(PREFIX_XSD)
24 |
25 | append(rawQuery)
26 | }
27 |
28 | fun build() = buildString {
29 | append(ENDPOINT_MAIN)
30 | append("?output=json")
31 | append("&query=")
32 |
33 | append(URLEncoder.encode(plainQuery))
34 | }
35 |
36 | protected fun String.trimIndentAndBr() = trimIndent().replace("[\n\r]", "")
37 | }
38 |
--------------------------------------------------------------------------------
/backend/cli/src/main/kotlin/net/subroh0508/colormaster/backend/cli/imasparql/query/ImasparqlQuery.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.backend.cli.imasparql.query
2 |
3 | import net.subroh0508.colormaster.backend.cli.imasparql.URLEncoder
4 |
5 | abstract class ImasparqlQuery {
6 | companion object {
7 | private const val ENDPOINT_MAIN = "https://sparql.crssnky.xyz/spql/imas/query"
8 |
9 | private const val PREFIX_SCHEMA = "PREFIX schema: "
10 | private const val PREFIX_IMAS = "PREFIX imas: "
11 | private const val PREFIX_RDF = "PREFIX rdf: "
12 | private const val PREFIX_RDFS = "PREFIX rdfs: "
13 | private const val PREFIX_XSD = "PREFIX xsd: "
14 | }
15 |
16 | protected abstract val rawQuery: String
17 |
18 | val plainQuery get() = buildString {
19 | append(PREFIX_SCHEMA)
20 | append(PREFIX_IMAS)
21 | append(PREFIX_RDF)
22 | append(PREFIX_RDFS)
23 | append(PREFIX_XSD)
24 |
25 | append(rawQuery)
26 | }
27 |
28 | fun build() = buildString {
29 | append(ENDPOINT_MAIN)
30 | append("?output=json")
31 | append("&query=")
32 |
33 | append(URLEncoder.encode(plainQuery))
34 | }
35 |
36 | protected fun String.trimIndentAndBr() = trimIndent().replace("[\n\r]", "")
37 | }
38 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/utilities/MediaQuery.kt:
--------------------------------------------------------------------------------
1 | package material.utilities
2 |
3 | import androidx.compose.runtime.*
4 | import kotlinx.browser.window
5 | import org.w3c.dom.MediaQueryListEvent
6 | import org.w3c.dom.events.Event
7 |
8 | // ref. https://material.io/design/layout/responsive-layout-grid.html#breakpoints
9 | private const val DP_PHONE_MIN = 0
10 | private const val DP_TABLET_SMALL_MIN = 600
11 | private const val DP_TABLET_LARGE_MIN = 905
12 | private const val DP_LAPTOP_MIN = 1240
13 | private const val DP_DESKTOP_MIN = 1440
14 |
15 | const val MEDIA_QUERY_PHONE = "(min-width: ${DP_PHONE_MIN}px)"
16 | const val MEDIA_QUERY_TABLET_SMALL = "(min-width: ${DP_TABLET_SMALL_MIN}px)"
17 | const val MEDIA_QUERY_TABLET_LARGE = "(min-width: ${DP_TABLET_LARGE_MIN}px)"
18 | const val MEDIA_QUERY_LAPTOP = "(min-width: ${DP_LAPTOP_MIN}px)"
19 | const val MEDIA_QUERY_DESKTOP = "(min-width: ${DP_DESKTOP_MIN}px)"
20 |
21 | @Composable
22 | fun rememberMediaQuery(query: String): MutableState {
23 | val matches = remember(query) { mutableStateOf(false) }
24 | val listener = remember(query) {
25 | { e: Event -> matches.value = (e as? MediaQueryListEvent)?.matches == true }
26 | }
27 |
28 | DisposableEffect(query) {
29 | val media = window.matchMedia(query).apply { addEventListener("change", listener) }
30 | matches.value = media.matches
31 |
32 | onDispose { media.removeEventListener("change", listener) }
33 | }
34 |
35 | return matches
36 | }
37 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/androidMain/kotlin/net/subroh0508/colormaster/network/imasparql/di/AndroidHttpClient.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.di
2 |
3 | import io.ktor.client.HttpClient
4 | import io.ktor.client.engine.okhttp.OkHttp
5 | import io.ktor.client.plugins.*
6 | import io.ktor.client.request.accept
7 | import io.ktor.http.URLProtocol
8 | import io.ktor.http.userAgent
9 | import kotlinx.serialization.json.Json
10 | import net.subroh0508.colormaster.network.imasparql.BuildConfig
11 | import net.subroh0508.colormaster.network.imasparql.HOSTNAME
12 | import net.subroh0508.colormaster.network.imasparql.internal.ContentType
13 | import net.subroh0508.colormaster.network.imasparql.internal.UserAgent
14 | import okhttp3.logging.HttpLoggingInterceptor
15 |
16 | internal actual fun httpClient(json: Json) = HttpClient(OkHttp) {
17 | engine {
18 | if (BuildConfig.DEBUG) {
19 | val loggingInterceptor = HttpLoggingInterceptor()
20 | loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
21 | addInterceptor(loggingInterceptor)
22 | }
23 |
24 | // @see https://github.com/ktorio/ktor/issues/1708
25 | config {
26 | retryOnConnectionFailure(true)
27 | }
28 | }
29 | defaultRequest {
30 | url {
31 | protocol = URLProtocol.HTTPS
32 | host = HOSTNAME
33 | }
34 | accept(ContentType.Application.SparqlJson)
35 | userAgent(UserAgent)
36 | }
37 | Json(json) {}
38 | }
39 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/components/Icon.kt:
--------------------------------------------------------------------------------
1 | package material.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.web.events.SyntheticMouseEvent
5 | import org.jetbrains.compose.web.attributes.AttrsScope
6 | import org.jetbrains.compose.web.dom.Button
7 | import org.jetbrains.compose.web.dom.I
8 | import org.jetbrains.compose.web.dom.Span
9 | import org.jetbrains.compose.web.dom.Text
10 | import org.w3c.dom.HTMLButtonElement
11 | import org.w3c.dom.HTMLElement
12 |
13 | @Composable
14 | fun Icon(
15 | icon: String,
16 | applyAttrs: (AttrsScope.() -> Unit)? = null,
17 | ) = ComposableIcon(icon, applyAttrs)
18 |
19 | @Composable
20 | fun IconButton(
21 | icon: String,
22 | applyAttrs: (AttrsScope.() -> Unit)? = null,
23 | ) {
24 | val element = rememberRippleElement(unbounded = true)
25 |
26 | Button({
27 | applyAttrs?.invoke(this)
28 | classes("mdc-icon-button")
29 | ref {
30 | element.value = it
31 | onDispose { element.value = null }
32 | }
33 | }) {
34 | Span({ classes("mdc-icon-button__ripple") })
35 | Span({ classes("mdc-icon-button__focus-ring") })
36 | ComposableIcon(icon)
37 | }
38 | }
39 |
40 | @Composable
41 | private fun ComposableIcon(icon: String, applyAttrs: (AttrsScope.() -> Unit)? = null) {
42 | I({
43 | classes("material-icons")
44 | applyAttrs?.invoke(this)
45 | }) {
46 | Text(icon)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/core/features/preview/src/commonMain/kotlin/net/subroh0508/colormaster/features/preview/FetchIdolsUseCase.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.preview
2 |
3 | import androidx.compose.runtime.*
4 | import kotlinx.coroutines.launch
5 | import net.subroh0508.colormaster.common.CurrentLocalKoinApp
6 | import net.subroh0508.colormaster.common.model.LoadState
7 | import net.subroh0508.colormaster.common.ui.CurrentLocalLanguage
8 | import net.subroh0508.colormaster.common.ui.Languages
9 | import net.subroh0508.colormaster.model.IdolColorsRepository
10 | import org.koin.core.KoinApplication
11 |
12 | @Composable
13 | fun rememberFetchIdolsUseCase(
14 | ids: List,
15 | language: Languages = CurrentLocalLanguage(),
16 | koinApp: KoinApplication = CurrentLocalKoinApp(),
17 | ): State {
18 | val scope = rememberCoroutineScope()
19 | val repository: IdolColorsRepository by remember(koinApp) { mutableStateOf(koinApp.koin.get()) }
20 |
21 | return produceState(
22 | initialValue = LoadState.Initialize,
23 | ids,
24 | ) {
25 | if (ids.isEmpty()) {
26 | value = LoadState.Error(EmptyIdsRequestException())
27 | return@produceState
28 | }
29 |
30 | val job = scope.launch {
31 | runCatching { repository.search(ids, language.code) }
32 | .onSuccess { value = LoadState.Loaded(it) }
33 | .onFailure { value = LoadState.Error(it) }
34 | }
35 |
36 | value = LoadState.Loading
37 | job.start()
38 | }
39 | }
40 |
41 | class EmptyIdsRequestException : Throwable()
42 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/components/Menu.kt:
--------------------------------------------------------------------------------
1 | package material.components
2 |
3 | import androidx.compose.runtime.*
4 | import androidx.compose.web.events.SyntheticMouseEvent
5 | import material.externals.MDCMenu
6 | import org.jetbrains.compose.web.dom.Div
7 | import org.jetbrains.compose.web.dom.Text
8 |
9 | @Composable
10 | fun Menu(
11 | anchor: @Composable (MDCMenu?) -> Unit,
12 | content: @Composable () -> Unit,
13 | ) {
14 | var menu by remember { mutableStateOf(null) }
15 |
16 | Div({ classes("mdc-menu-surface--anchor") }) {
17 | anchor(menu)
18 |
19 | Div({
20 | classes("mdc-menu", "mdc-menu-surface")
21 |
22 | ref {
23 | menu = MDCMenu(it)
24 | onDispose { menu = null }
25 | }
26 | }) {
27 | List({
28 | attr("role", "menu")
29 | attr("aria-hidden", "true")
30 | attr("aria-orientation", "vertical")
31 | attr("tabindex", "-1")
32 | }) { content() }
33 | }
34 | }
35 | }
36 |
37 | @Composable
38 | fun MenuItem(
39 | onClick: (SyntheticMouseEvent) -> Unit = {},
40 | activated: Boolean = false,
41 | content: @Composable () -> Unit,
42 | ) = ListItem(
43 | {
44 | attr("role", "menuitem")
45 |
46 | onClick(onClick)
47 | },
48 | activated,
49 | ) { content() }
50 |
51 | @Composable
52 | fun MenuItem(
53 | text: String,
54 | activated: Boolean = false,
55 | onClick: (SyntheticMouseEvent) -> Unit = {},
56 | ) = MenuItem(onClick, activated) { Text(text) }
57 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/subroh0508/colormaster/androidapp/components/organisms/StaticColorLists.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.androidapp.components.organisms
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 | import net.subroh0508.colormaster.androidapp.ScreenType
9 | import net.subroh0508.colormaster.androidapp.components.atoms.ColorItemContent
10 | import net.subroh0508.colormaster.common.extensions.toColor
11 | import net.subroh0508.colormaster.model.IdolColor
12 | import net.subroh0508.colormaster.model.IntColor
13 |
14 | @Composable
15 | fun StaticColorLists(
16 | type: ScreenType,
17 | items: List,
18 | ) {
19 | Column(Modifier.fillMaxSize()) {
20 | items.forEach { (_, name, intColor) ->
21 | StaticColorListItem(type, name, intColor)
22 | }
23 | }
24 | }
25 |
26 | @Composable
27 | private fun ColumnScope.StaticColorListItem(
28 | type: ScreenType,
29 | label: String,
30 | intColor: IntColor,
31 | ) {
32 | val boxModifier = Modifier.fillMaxWidth()
33 | .weight(1.0F, true)
34 | .background(color = intColor.toColor())
35 |
36 | if (type == ScreenType.Penlight) {
37 | Box(modifier = boxModifier)
38 | return
39 | }
40 |
41 | Box(modifier = boxModifier) {
42 | ColorItemContent(
43 | label, intColor,
44 | modifier = Modifier.fillMaxWidth()
45 | .align(Alignment.Center),
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/core/common/src/commonTest/kotlin/net/subroh0508/colormaster/common/DateNumSpec.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.common
2 |
3 | import io.kotest.core.spec.style.FunSpec
4 | import io.kotest.matchers.be
5 | import io.kotest.matchers.should
6 | import net.subroh0508.colormaster.common.model.DateNum
7 |
8 | data class Row(val num: Int, val expectRange: Pair?) {
9 | val expectValidate get() = expectRange != null
10 | }
11 |
12 | class DateNumSpec : FunSpec({
13 | listOf(
14 | Row(-1, null),
15 | Row(0, null),
16 | Row(100, null),
17 | Row(2000, null),
18 | Row(2005, "2005-01-01" to "2005-12-31"),
19 | Row(2020, "2020-01-01" to "2020-12-31"),
20 | Row(20151, null),
21 | Row(201500, null),
22 | Row(201501, "2015-01-01" to "2015-01-31"),
23 | Row(201504, "2015-04-01" to "2015-04-30"),
24 | Row(201502, "2015-02-01" to "2015-02-28"),
25 | Row(201202, "2012-02-01" to "2012-02-29"),
26 | Row(201213, null),
27 | Row(2017051, null),
28 | Row(20170500, null),
29 | Row(20170501, "2017-05-01" to "2017-05-01"),
30 | Row(20170630, "2017-06-30" to "2017-06-30"),
31 | Row(20170931, null),
32 | Row(20170229, null),
33 | ).forEach { r ->
34 | test("#validate: when number = ${r.num} it should return ${r.expectValidate}") {
35 | DateNum(r.num).validate() should be(r.expectValidate)
36 | }
37 |
38 | test("#range: when number = ${r.num} it should return ${r.expectRange} ") {
39 | DateNum(r.num).range() should be(r.expectRange)
40 | }
41 | }
42 | })
43 |
--------------------------------------------------------------------------------
/js/material/src/jsMain/kotlin/material/components/Card.kt:
--------------------------------------------------------------------------------
1 | package material.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import org.jetbrains.compose.web.attributes.AttrsScope
5 | import org.jetbrains.compose.web.css.padding
6 | import org.jetbrains.compose.web.css.px
7 | import org.jetbrains.compose.web.dom.Div
8 | import org.w3c.dom.HTMLDivElement
9 |
10 | @Composable
11 | fun Card(
12 | attrsScope: ((AttrsScope).() -> Unit)? = null,
13 | content: @Composable () -> Unit,
14 | ) {
15 | Div({
16 | classes("mdc-card")
17 | attrsScope?.invoke(this)
18 | }) {
19 | content()
20 | }
21 | }
22 |
23 | @Composable
24 | fun OutlinedCard(
25 | attrsScope: ((AttrsScope).() -> Unit)? = null,
26 | content: @Composable () -> Unit,
27 | ) {
28 | Div({
29 | classes("mdc-card", "mdc-card--outlined")
30 | attrsScope?.invoke(this)
31 | }) {
32 | content()
33 | }
34 | }
35 |
36 | @Composable
37 | fun CardHeader(
38 | attrsScope: ((AttrsScope).() -> Unit)? = null,
39 | content: @Composable () -> Unit,
40 | ) {
41 | Div({
42 | classes("mdc-card__header")
43 | style { padding(16.px) }
44 | attrsScope?.invoke(this)
45 | }) {
46 | content()
47 | }
48 | }
49 | @Composable
50 | fun CardContent(
51 | attrsScope: ((AttrsScope).() -> Unit)? = null,
52 | content: @Composable () -> Unit,
53 | ) {
54 | Div({
55 | classes("mdc-card__content")
56 | style { padding(16.px) }
57 | attrsScope?.invoke(this)
58 | }) {
59 | content()
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/plugins/src/main/kotlin/net/subroh0508/colormaster/convention/AndroidAppModulePlugin.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.convention
2 |
3 | import net.subroh0508.colormaster.primitive.android.Android
4 | import net.subroh0508.colormaster.primitive.android.androidApplication
5 | import net.subroh0508.colormaster.primitive.android.setupAndroid
6 | import org.gradle.api.Plugin
7 | import org.gradle.api.Project
8 |
9 | @Suppress("unused")
10 | class AndroidAppModulePlugin : Plugin {
11 | override fun apply(target: Project) {
12 | with (target) {
13 | with (plugins) {
14 | apply("com.android.application")
15 | apply("org.jetbrains.kotlin.android")
16 | apply("org.jetbrains.compose")
17 | apply("org.jetbrains.kotlin.plugin.compose")
18 | apply("com.google.gms.google-services")
19 | }
20 |
21 | setupAndroid(isMinifyEnabled = true)
22 | androidApplication {
23 | defaultConfig {
24 | minSdk = Android.Versions.minSdk
25 | targetSdk = Android.Versions.targetSdk
26 |
27 | applicationId = Android.applicationId
28 | versionCode = Android.versionCode
29 | versionName = Android.versionName
30 | testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
31 | }
32 |
33 | packaging {
34 | resources {
35 | excludes.add("META-INF/*")
36 | }
37 | }
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/subroh0508/colormaster/androidapp/components/atoms/ColorItemContent.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.androidapp.components.atoms
2 |
3 | import androidx.compose.material.Text
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.material.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.text.font.FontWeight
12 | import androidx.compose.ui.unit.dp
13 | import net.subroh0508.colormaster.model.IntColor
14 | import net.subroh0508.colormaster.model.isBrighter
15 | import net.subroh0508.colormaster.model.toHex
16 |
17 | @Composable
18 | fun ColorItemContent(
19 | label: String,
20 | intColor: IntColor,
21 | modifier: Modifier = Modifier,
22 | ) {
23 | val textColor = if (intColor.isBrighter) Color.Black else Color.White
24 |
25 | Column(modifier) {
26 | Text(
27 | label,
28 | color = textColor,
29 | style = MaterialTheme.typography.body1.copy(fontWeight = FontWeight.Medium),
30 | modifier = Modifier.align(Alignment.CenterHorizontally)
31 | .padding(top = 8.dp, start = 24.dp, end = 24.dp),
32 | )
33 | Text(
34 | intColor.toHex(),
35 | color = textColor,
36 | style = MaterialTheme.typography.body1.copy(fontWeight = FontWeight.Medium),
37 | modifier = Modifier.align(Alignment.CenterHorizontally)
38 | .padding(bottom = 8.dp, start = 24.dp, end = 24.dp),
39 | )
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/core/network/imasparql/src/commonMain/kotlin/net/subroh0508/colormaster/network/imasparql/query/SearchByNameQuery.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.imasparql.query
2 |
3 | import net.subroh0508.colormaster.network.imasparql.internal.ESCAPED_ENDPOINT_RDFS_DETAIL
4 |
5 | class SearchByNameQuery(
6 | lang: String,
7 | idolName: String?, brandsQueryStr: String?, typesQueryStr: List,
8 | ) : ImasparqlQuery() {
9 | override val rawQuery = """
10 | SELECT ?id ?name ?color WHERE {
11 | ?s imas:Color ?color;
12 | imas:Brand ?brand.
13 | OPTIONAL { ?s schema:name ?realName. FILTER(lang(?realName) = '$lang') }
14 | OPTIONAL { ?s schema:alternateName ?altName. FILTER(lang(?altName) = '$lang') }
15 | OPTIONAL { ?s schema:givenName ?givenName. FILTER(lang(?givenName) = '$lang') }
16 | BIND (COALESCE(?altName, ?realName, ?givenName) as ?name)
17 | OPTIONAL { ?s imas:Division ?division }
18 | OPTIONAL { ?s imas:Type ?type }
19 | OPTIONAL { ?s imas:Category ?category }
20 | BIND (COALESCE(?category, ?division, ?type) as ?attribute)
21 | ${idolName?.let {"FILTER (regex(?name, '.*$it.*', 'i') && str(?brand) != '1stVision')." } ?: ""}
22 | ${brandsQueryStr?.let { "FILTER (str(?brand) = '$it')." } ?: ""}
23 | ${typesQueryStr.regex?.let { "FILTER regex(?attribute, '$it', 'i')." } ?: "" }
24 | BIND (REPLACE(str(?s), '${ESCAPED_ENDPOINT_RDFS_DETAIL}', '') as ?id).
25 | }
26 | ORDER BY ?name
27 | """.trimIndentAndBr()
28 |
29 | private val List.regex get() =
30 | takeIf(List::isNotEmpty)?.joinToString("|")?.let { "($it)" }
31 | }
32 |
--------------------------------------------------------------------------------
/plugins/src/main/kotlin/net/subroh0508/colormaster/primitive/android/AndroidDsl.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.primitive.android
2 |
3 | import com.android.build.gradle.LibraryExtension
4 | import com.android.build.gradle.TestedExtension
5 | import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
6 | import org.gradle.api.JavaVersion
7 | import org.gradle.api.Project
8 | import org.gradle.api.tasks.testing.Test
9 | import org.gradle.api.tasks.testing.logging.TestLogEvent
10 | import org.gradle.kotlin.dsl.configure
11 | import org.gradle.kotlin.dsl.withType
12 |
13 | fun Project.androidApplication(action: BaseAppModuleExtension.() -> Unit) = extensions.configure(action)
14 |
15 | fun Project.androidLibrary(action: LibraryExtension.() -> Unit) = extensions.configure(action)
16 |
17 | fun Project.android(action: TestedExtension.() -> Unit) = extensions.configure(action)
18 |
19 | fun Project.setupAndroid(isMinifyEnabled: Boolean = false) {
20 | android {
21 | compileSdkVersion(Android.Versions.compileSdk)
22 |
23 | buildTypes {
24 | getByName("release") {
25 | this.isMinifyEnabled = isMinifyEnabled
26 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
27 | }
28 | }
29 |
30 | compileOptions {
31 | sourceCompatibility = JavaVersion.VERSION_17
32 | targetCompatibility = JavaVersion.VERSION_17
33 | }
34 |
35 | tasks.withType {
36 | useJUnitPlatform()
37 | testLogging {
38 | events = setOf(TestLogEvent.PASSED, TestLogEvent.FAILED, TestLogEvent.SKIPPED)
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/plugins/src/main/kotlin/net/subroh0508/colormaster/convention/CommonModulePlugin.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.convention
2 |
3 | import net.subroh0508.colormaster.koinCore
4 | import net.subroh0508.colormaster.library
5 | import net.subroh0508.colormaster.libs
6 | import net.subroh0508.colormaster.primitive.compose.compose
7 | import net.subroh0508.colormaster.primitive.kmp.applyKmpPlugins
8 | import net.subroh0508.colormaster.primitive.kmp.kotlin
9 | import org.gradle.api.Plugin
10 | import org.gradle.api.Project
11 |
12 | @Suppress("unused")
13 | class CommonModulePlugin : Plugin {
14 | override fun apply(target: Project) {
15 | with (target) {
16 | with (plugins) {
17 | applyKmpPlugins()
18 | apply("org.jetbrains.compose")
19 | apply("org.jetbrains.kotlin.plugin.compose")
20 | apply("io.kotest.multiplatform")
21 | }
22 |
23 | kotlin {
24 | with (sourceSets) {
25 | commonMain {
26 | dependencies {
27 | implementation(compose.dependencies.runtime)
28 | implementation(compose.dependencies.ui)
29 | implementation(libs.koinCore)
30 | }
31 | }
32 | jsMain {
33 | dependencies {
34 | implementation(dependencies.platform(libs.library("kotlin-wrappers-bom")))
35 | implementation(libs.library("kotlin-wrappers-js"))
36 | }
37 | }
38 | }
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/core/data/src/commonMain/kotlin/net/subroh0508/colormaster/data/DefaultLiveRepository.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data
2 |
3 | import kotlinx.coroutines.flow.MutableStateFlow
4 | import kotlinx.coroutines.flow.onStart
5 | import net.subroh0508.colormaster.network.imasparql.ImasparqlClient
6 | import net.subroh0508.colormaster.network.imasparql.json.LiveNameJson
7 | import net.subroh0508.colormaster.network.imasparql.query.SuggestLiveQuery
8 | import net.subroh0508.colormaster.model.LiveName
9 | import net.subroh0508.colormaster.model.LiveRepository
10 | import net.subroh0508.colormaster.network.imasparql.serializer.Response
11 |
12 | internal class DefaultLiveRepository(
13 | private val imasparqlClient: ImasparqlClient,
14 | ) : LiveRepository {
15 | private val names: MutableStateFlow> = MutableStateFlow(listOf())
16 |
17 | override fun getLiveNamesStream() = names.onStart {
18 | refresh()
19 | }
20 |
21 | override suspend fun refresh() {
22 | names.value = listOf()
23 | }
24 |
25 | override suspend fun suggest(
26 | dateRange: Pair,
27 | ) = imasparqlClient.search(
28 | SuggestLiveQuery(dateRange = dateRange).build(),
29 | LiveNameJson.serializer(),
30 | ).toLiveNames().also { names.value = it }
31 |
32 | override suspend fun suggest(
33 | name: String?,
34 | ) = imasparqlClient.search(
35 | SuggestLiveQuery(name = name).build(),
36 | LiveNameJson.serializer(),
37 | ).toLiveNames().also { names.value = it }
38 |
39 | private fun Response.toLiveNames() = results
40 | .bindings
41 | .mapNotNull { (liveNameMap) -> liveNameMap["value"]?.let { LiveName(it) } }
42 | }
43 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/net/subroh0508/colormaster/androidapp/themes/Theme.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.androidapp.themes
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.graphics.Color
9 | import net.subroh0508.colormaster.androidapp.ColorMasterApplication
10 |
11 | private val DarkColorPalette = darkColors(
12 | primary = orange200,
13 | primaryVariant = deepOrange200,
14 | secondary = teal200,
15 | surface = gray900,
16 | background = darkBackground,
17 | onPrimary = Color.Black,
18 | )
19 |
20 | private val LightColorPalette = lightColors(
21 | primary = orange900,
22 | primaryVariant = deepOrange900,
23 | secondary = teal200,
24 | surface = Color.White,
25 | background = lightBackground,
26 | onPrimary = Color.White,
27 |
28 | /* Other default colors to override
29 | background = Color.White,
30 | surface = Color.White,
31 | onPrimary = Color.White,
32 | onSecondary = Color.Black,
33 | onBackground = Color.Black,
34 | onSurface = Color.Black,
35 | */
36 | )
37 |
38 | @Composable
39 | fun ColorMasterTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
40 | val colors = if (darkTheme) {
41 | DarkColorPalette
42 | } else {
43 | LightColorPalette
44 | }
45 |
46 | MaterialTheme(
47 | colors = colors,
48 | typography = typography,
49 | shapes = shapes,
50 | content = content
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/core/features/search/src/commonMain/kotlin/net/subroh0508/colormaster/features/search/viewmodel/SuggestLiveNameViewModel.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.features.search.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import kotlinx.coroutines.flow.SharingStarted
6 | import kotlinx.coroutines.flow.StateFlow
7 | import kotlinx.coroutines.flow.catch
8 | import kotlinx.coroutines.flow.emitAll
9 | import kotlinx.coroutines.flow.flow
10 | import kotlinx.coroutines.flow.map
11 | import kotlinx.coroutines.flow.stateIn
12 | import net.subroh0508.colormaster.features.search.model.LiveNameQuery
13 | import net.subroh0508.colormaster.model.LiveRepository
14 |
15 | class SuggestLiveNameViewModel(
16 | private val repository: LiveRepository,
17 | ) : ViewModel() {
18 | val uiState: StateFlow = repository.getLiveNamesStream()
19 | .map { liveNames ->
20 | SuggestLiveNameUiState.Loaded(liveNames)
21 | }.catch { e ->
22 | SuggestLiveNameUiState.Error(e)
23 | }.stateIn(
24 | scope = viewModelScope,
25 | started = SharingStarted.WhileSubscribed(5_000),
26 | initialValue = SuggestLiveNameUiState.Loaded(listOf()),
27 | )
28 |
29 | suspend fun suggest(liveNameQuery: LiveNameQuery) {
30 | val (query, isSettled) = liveNameQuery
31 |
32 | if (query == null || isSettled) {
33 | repository.refresh()
34 | return
35 | }
36 |
37 | if (liveNameQuery.isNumber()) {
38 | val dateRange = liveNameQuery.toDateNum()?.range() ?: return
39 | repository.suggest(dateRange)
40 | }
41 |
42 | repository.suggest(query)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/core/data/src/commonMain/kotlin/net/subroh0508/colormaster/data/DefaultMyIdolsRepository.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.data
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.MutableStateFlow
5 | import kotlinx.coroutines.flow.onStart
6 | import net.subroh0508.colormaster.data.extension.search
7 | import net.subroh0508.colormaster.model.IdolColor
8 | import net.subroh0508.colormaster.model.MyIdolsRepository
9 | import net.subroh0508.colormaster.network.auth.AuthClient
10 | import net.subroh0508.colormaster.network.firestore.FirestoreClient
11 | import net.subroh0508.colormaster.network.imasparql.ImasparqlClient
12 |
13 | internal class DefaultMyIdolsRepository(
14 | private val imasparqlClient: ImasparqlClient,
15 | private val firestoreClient: FirestoreClient,
16 | private val authClient: AuthClient,
17 | ) : MyIdolsRepository {
18 | private val inChargeOfIdolsStateFlow = MutableStateFlow>(listOf())
19 | private val favoriteIdolsStateFlow = MutableStateFlow>(listOf())
20 |
21 | override fun getInChargeOfIdolsStream(lang: String): Flow> {
22 | return inChargeOfIdolsStateFlow.onStart {
23 | inChargeOfIdolsStateFlow.value = imasparqlClient.search(getUserDocument().inCharges, lang)
24 | }
25 | }
26 |
27 | override fun getFavoriteIdolsStream(lang: String): Flow> {
28 | return favoriteIdolsStateFlow.onStart {
29 | favoriteIdolsStateFlow.value = imasparqlClient.search(getUserDocument().favorites, lang)
30 | }
31 | }
32 |
33 | private val currentUser get() = authClient.currentUser
34 |
35 | private suspend fun getUserDocument() = firestoreClient.getUserDocument(currentUser?.uid)
36 | }
37 |
--------------------------------------------------------------------------------
/core/network/auth/src/androidMain/kotlin/net/subroh0508/colormaster/network/auth/internal/AuthClientImpl.android.kt:
--------------------------------------------------------------------------------
1 | package net.subroh0508.colormaster.network.auth.internal
2 |
3 | import dev.gitlive.firebase.auth.FirebaseAuth
4 | import dev.gitlive.firebase.auth.GoogleAuthProvider
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.map
7 | import net.subroh0508.colormaster.network.auth.AuthClient
8 | import net.subroh0508.colormaster.network.auth.model.FirebaseUser
9 | import net.subroh0508.colormaster.network.auth.model.Provider
10 | import dev.gitlive.firebase.auth.FirebaseUser as RawFirebaseUser
11 |
12 | internal class AuthClientImpl(private val auth: FirebaseAuth) : AuthClient {
13 | override val currentUser get() = auth.currentUser?.toDataClass()
14 |
15 | override suspend fun signInAnonymously() {
16 | auth.signInAnonymously()
17 | }
18 |
19 | override suspend fun signOut() = auth.signOut()
20 |
21 | override fun subscribeAuthState(): Flow = auth.authStateChanged.map { it?.toDataClass() }
22 |
23 | override suspend fun signInWithGoogle(idToken: String) {
24 | val credential = GoogleAuthProvider.credential(idToken, null)
25 |
26 | auth.signInWithCredential(credential)
27 | }
28 |
29 | private fun getProviderData(): List {
30 | val rawUser = auth.currentUser ?: return listOf()
31 | if (rawUser.isAnonymous) {
32 | return listOf(Provider(Provider.PROVIDER_ANONYMOUS, null, null))
33 | }
34 |
35 | return rawUser.providerData.map { Provider(it.providerId, it.email, it.displayName) }
36 | }
37 |
38 | private fun RawFirebaseUser.toDataClass() = FirebaseUser(
39 | uid,
40 | this@AuthClientImpl.getProviderData(),
41 | )
42 | }
43 |
--------------------------------------------------------------------------------