├── composeApp ├── libs │ └── common │ │ └── data.txt ├── src │ ├── androidMain │ │ ├── res │ │ │ ├── values │ │ │ │ └── strings.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── logo_pro.webp │ │ │ │ ├── logo_pro_round.webp │ │ │ │ └── logo_pro_foreground.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── logo_pro.webp │ │ │ │ ├── logo_pro_round.webp │ │ │ │ └── logo_pro_foreground.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── logo_pro.webp │ │ │ │ ├── logo_pro_round.webp │ │ │ │ └── logo_pro_foreground.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── logo_pro.webp │ │ │ │ ├── logo_pro_round.webp │ │ │ │ └── logo_pro_foreground.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── logo_pro.webp │ │ │ │ ├── logo_pro_round.webp │ │ │ │ └── logo_pro_foreground.webp │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── logo_pro.xml │ │ │ │ └── logo_pro_round.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── logo_pro_background.xml │ │ ├── logo_pro-playstore.png │ │ ├── kotlin │ │ │ ├── di │ │ │ │ └── CommonModule.android.kt │ │ │ ├── ui │ │ │ │ └── components │ │ │ │ │ ├── MediaPlayer.android.kt │ │ │ │ │ └── Notification.android.kt │ │ │ ├── data │ │ │ │ └── store │ │ │ │ │ └── SettingsWrapper.android.kt │ │ │ ├── biz │ │ │ │ ├── MainUtils.android.kt │ │ │ │ ├── StatusBar.android.kt │ │ │ │ ├── FMessagingService.android.kt │ │ │ │ └── ChatSocketService.android.kt │ │ │ └── com │ │ │ │ └── aster │ │ │ │ └── yuno │ │ │ │ └── tomoyo │ │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml │ ├── desktopMain │ │ ├── resources │ │ │ ├── empty.png │ │ │ ├── logo_pro.ico │ │ │ ├── logo_pro_round.png │ │ │ └── logo_pro_round_32.png │ │ └── kotlin │ │ │ ├── biz │ │ │ ├── StatusBar.desktop.kt │ │ │ ├── MainUtils.desktop.kt │ │ │ └── AudioPlayer.desktop.kt │ │ │ ├── ui │ │ │ └── components │ │ │ │ ├── MediaPlayer.desktop.kt │ │ │ │ ├── Notification.desktop.kt │ │ │ │ └── StartLoading.kt │ │ │ ├── data │ │ │ └── store │ │ │ │ └── SettingsWrapper.desktop.kt │ │ │ ├── di │ │ │ └── CommonModule.desktop.kt │ │ │ └── main.kt │ ├── commonMain │ │ ├── composeResources │ │ │ ├── drawable │ │ │ │ ├── bg1.jpg │ │ │ │ ├── bg3.jpg │ │ │ │ ├── logo_pro_round_512.png │ │ │ │ ├── logo_pro_trans_128.png │ │ │ │ ├── user_solid.xml │ │ │ │ ├── input_exclamation_circle.xml │ │ │ │ ├── input_check_circle.xml │ │ │ │ ├── media_circle_pause.xml │ │ │ │ ├── id_badge_solid.xml │ │ │ │ ├── envelope_alt.xml │ │ │ │ ├── input_microphone.xml │ │ │ │ ├── input_map_pin.xml │ │ │ │ ├── skull_solid.xml │ │ │ │ ├── media_repeat_all.xml │ │ │ │ ├── input_folder.xml │ │ │ │ ├── media_circle_play.xml │ │ │ │ ├── media_play.xml │ │ │ │ ├── media_circle_next.xml │ │ │ │ ├── input_plus_square.xml │ │ │ │ ├── input_map_marker.xml │ │ │ │ ├── input_send.xml │ │ │ │ ├── input_question_circle.xml │ │ │ │ ├── media_circle_previous.xml │ │ │ │ ├── media_next.xml │ │ │ │ ├── input_tag.xml │ │ │ │ ├── input_sliders_h.xml │ │ │ │ ├── media_previous.xml │ │ │ │ ├── stackoverflow.xml │ │ │ │ ├── drumstick_bite_solid.xml │ │ │ │ ├── input_upload.xml │ │ │ │ ├── media_audio.xml │ │ │ │ ├── input_compass.xml │ │ │ │ ├── input_film.xml │ │ │ │ ├── twitter.xml │ │ │ │ ├── input_heart.xml │ │ │ │ ├── media_pause.xml │ │ │ │ ├── media_circle_stop.xml │ │ │ │ ├── input_user.xml │ │ │ │ ├── user_graduate_solid.xml │ │ │ │ ├── input_link.xml │ │ │ │ ├── input_image.xml │ │ │ │ ├── media_repeat_one.xml │ │ │ │ ├── input_camera.xml │ │ │ │ ├── envelope_question.xml │ │ │ │ ├── envelope_heart.xml │ │ │ │ ├── media_shuffle.xml │ │ │ │ ├── github.xml │ │ │ │ ├── input_trash.xml │ │ │ │ ├── input_package.xml │ │ │ │ ├── input_star.xml │ │ │ │ ├── envelope_exclamation.xml │ │ │ │ ├── input_map.xml │ │ │ │ ├── google.xml │ │ │ │ ├── input_store.xml │ │ │ │ ├── user_secret_solid.xml │ │ │ │ ├── input_calendar.xml │ │ │ │ ├── envelope_star.xml │ │ │ │ ├── input_phone.xml │ │ │ │ ├── input_users_three.xml │ │ │ │ ├── input_globe.xml │ │ │ │ ├── reddit.xml │ │ │ │ ├── amazon.xml │ │ │ │ ├── input_settings.xml │ │ │ │ └── compose-multiplatform.xml │ │ │ └── font │ │ │ │ ├── static │ │ │ │ ├── RobotoSlab-Black.ttf │ │ │ │ ├── RobotoSlab-Bold.ttf │ │ │ │ ├── RobotoSlab-Light.ttf │ │ │ │ ├── RobotoSlab-Thin.ttf │ │ │ │ ├── RobotoSlab-Medium.ttf │ │ │ │ ├── RobotoSlab-Regular.ttf │ │ │ │ ├── RobotoSlab-SemiBold.ttf │ │ │ │ ├── RobotoSlab-ExtraBold.ttf │ │ │ │ └── RobotoSlab-ExtraLight.ttf │ │ │ │ └── RobotoSlab-VariableFont_wght.ttf │ │ └── kotlin │ │ │ ├── data │ │ │ ├── PlatformInitData.kt │ │ │ ├── ResultObj.kt │ │ │ ├── store │ │ │ │ ├── SettingsWrapper.kt │ │ │ │ └── DataStorageManager.kt │ │ │ ├── ArticleModel.kt │ │ │ ├── MusicModel.kt │ │ │ ├── model │ │ │ │ ├── ContactScreenModel.kt │ │ │ │ ├── GlobalDataModel.kt │ │ │ │ ├── ArticleScreenModel.kt │ │ │ │ └── MusicScreenModel.kt │ │ │ ├── ChatModel.kt │ │ │ └── UserModel.kt │ │ │ ├── ui │ │ │ ├── components │ │ │ │ ├── MediaPlayer.kt │ │ │ │ ├── CardComponents.kt │ │ │ │ └── Notification.kt │ │ │ └── pages │ │ │ │ └── MainVideos.kt │ │ │ ├── biz │ │ │ ├── StatusBar.kt │ │ │ ├── AudioPlayer.kt │ │ │ ├── CustomTransition.kt │ │ │ └── MainUtils.kt │ │ │ ├── di │ │ │ ├── KoinInit.kt │ │ │ └── CommonModule.kt │ │ │ ├── theme │ │ │ └── Type.kt │ │ │ └── constant │ │ │ ├── BaseConstants.kt │ │ │ └── ChineseDateData.kt │ └── iosMain │ │ └── kotlin │ │ ├── biz │ │ ├── StatusBar.ios.kt │ │ └── MainUtils.ios.kt │ │ ├── ui.components │ │ ├── Notification.ios.kt │ │ └── MediaPlayer.ios.kt │ │ ├── di │ │ └── CommonModule.ios.kt │ │ ├── data.store │ │ └── SettingsWrapper.ios.kt │ │ └── MainViewController.kt ├── proguard-rules.pro ├── compose-desktop.pro ├── google-services.json └── config │ └── compose_compiler_config.conf ├── image ├── ios_1.png ├── ios_2.png ├── ios_3.png ├── ios_4.png ├── ios_5.png ├── ios_6.png ├── ios_7.png ├── ios_8.png ├── ios_9.png ├── android_1.jpg ├── android_2.jpg ├── android_3.jpg ├── android_4.jpg ├── android_5.jpg ├── android_6.jpg ├── android_7.jpg ├── android_8.jpg ├── android_9.jpg ├── desktop_1.jpg ├── desktop_2.jpg ├── desktop_3.jpg ├── desktop_4.jpg ├── desktop_5.jpg ├── desktop_6.jpg ├── desktop_7.jpg ├── desktop_8.jpg └── desktop_9.jpg ├── iosApp ├── Configuration │ └── Config.xcconfig ├── iosApp │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── app-icon-512.png │ │ │ └── Contents.json │ │ └── AccentColor.colorset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── ContentView.swift │ ├── iOSApp.swift │ ├── GoogleService-Info.plist │ └── Info.plist └── iosApp.xcodeproj │ └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── swiftpm │ └── Package.resolved ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── gradle.properties ├── .gitignore ├── .fleet └── receipt.json ├── LICENSE ├── settings.gradle.kts ├── gradlew.bat └── README_zh.md /composeApp/libs/common/data.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /image/ios_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/ios_1.png -------------------------------------------------------------------------------- /image/ios_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/ios_2.png -------------------------------------------------------------------------------- /image/ios_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/ios_3.png -------------------------------------------------------------------------------- /image/ios_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/ios_4.png -------------------------------------------------------------------------------- /image/ios_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/ios_5.png -------------------------------------------------------------------------------- /image/ios_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/ios_6.png -------------------------------------------------------------------------------- /image/ios_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/ios_7.png -------------------------------------------------------------------------------- /image/ios_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/ios_8.png -------------------------------------------------------------------------------- /image/ios_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/ios_9.png -------------------------------------------------------------------------------- /image/android_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/android_1.jpg -------------------------------------------------------------------------------- /image/android_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/android_2.jpg -------------------------------------------------------------------------------- /image/android_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/android_3.jpg -------------------------------------------------------------------------------- /image/android_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/android_4.jpg -------------------------------------------------------------------------------- /image/android_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/android_5.jpg -------------------------------------------------------------------------------- /image/android_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/android_6.jpg -------------------------------------------------------------------------------- /image/android_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/android_7.jpg -------------------------------------------------------------------------------- /image/android_8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/android_8.jpg -------------------------------------------------------------------------------- /image/android_9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/android_9.jpg -------------------------------------------------------------------------------- /image/desktop_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/desktop_1.jpg -------------------------------------------------------------------------------- /image/desktop_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/desktop_2.jpg -------------------------------------------------------------------------------- /image/desktop_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/desktop_3.jpg -------------------------------------------------------------------------------- /image/desktop_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/desktop_4.jpg -------------------------------------------------------------------------------- /image/desktop_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/desktop_5.jpg -------------------------------------------------------------------------------- /image/desktop_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/desktop_6.jpg -------------------------------------------------------------------------------- /image/desktop_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/desktop_7.jpg -------------------------------------------------------------------------------- /image/desktop_8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/desktop_8.jpg -------------------------------------------------------------------------------- /image/desktop_9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/image/desktop_9.jpg -------------------------------------------------------------------------------- /iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=com.aster.yuno.tomoyo.Tomoyo 3 | APP_NAME=Tomoyo -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Tomoyo 3 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /composeApp/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -dontwarn java.net.** 2 | -dontwarn org.slf4j.** 3 | -dontwarn androidx.test.platform.app.InstrumentationRegistry -------------------------------------------------------------------------------- /composeApp/src/desktopMain/resources/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/desktopMain/resources/empty.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/logo_pro-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/logo_pro-playstore.png -------------------------------------------------------------------------------- /composeApp/src/desktopMain/resources/logo_pro.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/desktopMain/resources/logo_pro.ico -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/resources/logo_pro_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/desktopMain/resources/logo_pro_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/logo_pro.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/res/mipmap-hdpi/logo_pro.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/logo_pro.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/res/mipmap-mdpi/logo_pro.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/logo_pro.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/res/mipmap-xhdpi/logo_pro.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/logo_pro.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/res/mipmap-xxhdpi/logo_pro.webp -------------------------------------------------------------------------------- /composeApp/src/desktopMain/resources/logo_pro_round_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/desktopMain/resources/logo_pro_round_32.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/logo_pro.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/res/mipmap-xxxhdpi/logo_pro.webp -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/bg1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/commonMain/composeResources/drawable/bg1.jpg -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/bg3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/commonMain/composeResources/drawable/bg3.jpg -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/logo_pro_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/res/mipmap-hdpi/logo_pro_round.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/logo_pro_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/res/mipmap-mdpi/logo_pro_round.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/logo_pro_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/res/mipmap-xhdpi/logo_pro_round.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/logo_pro_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/res/mipmap-xxhdpi/logo_pro_round.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/logo_pro_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/res/mipmap-xxxhdpi/logo_pro_round.webp -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-512.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/logo_pro_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/res/mipmap-hdpi/logo_pro_foreground.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/logo_pro_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/res/mipmap-mdpi/logo_pro_foreground.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/logo_pro_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/res/mipmap-xhdpi/logo_pro_foreground.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/logo_pro_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/res/mipmap-xxhdpi/logo_pro_foreground.webp -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/logo_pro_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/androidMain/res/mipmap-xxxhdpi/logo_pro_foreground.webp -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/logo_pro_round_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/commonMain/composeResources/drawable/logo_pro_round_512.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/logo_pro_trans_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/commonMain/composeResources/drawable/logo_pro_trans_128.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/static/RobotoSlab-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/commonMain/composeResources/font/static/RobotoSlab-Black.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/static/RobotoSlab-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/commonMain/composeResources/font/static/RobotoSlab-Bold.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/static/RobotoSlab-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/commonMain/composeResources/font/static/RobotoSlab-Light.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/static/RobotoSlab-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/commonMain/composeResources/font/static/RobotoSlab-Thin.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/static/RobotoSlab-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/commonMain/composeResources/font/static/RobotoSlab-Medium.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/static/RobotoSlab-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/commonMain/composeResources/font/static/RobotoSlab-Regular.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/static/RobotoSlab-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/commonMain/composeResources/font/static/RobotoSlab-SemiBold.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/RobotoSlab-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/commonMain/composeResources/font/RobotoSlab-VariableFont_wght.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/static/RobotoSlab-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/commonMain/composeResources/font/static/RobotoSlab-ExtraBold.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/static/RobotoSlab-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AsterCass/Tomoyo/HEAD/composeApp/src/commonMain/composeResources/font/static/RobotoSlab-ExtraLight.ttf -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/data/PlatformInitData.kt: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import constant.enums.MainNavigationEnum 4 | 5 | data class PlatformInitData( 6 | val extraNavigationList: List = emptyList(), 7 | ) 8 | 9 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/components/MediaPlayer.kt: -------------------------------------------------------------------------------- 1 | package ui.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | 6 | 7 | @Composable 8 | expect fun MediaPlayer(modifier: Modifier, url: String) -------------------------------------------------------------------------------- /composeApp/compose-desktop.pro: -------------------------------------------------------------------------------- 1 | -dontwarn cn.hutool.core.util.* 2 | -dontwarn okhttp3.internal.platform.* 3 | -dontwarn androidx.datastore.preferences.protobuf.* 4 | -dontwarn okhttp3.internal.platform.android.* 5 | -dontwarn io.ktor.network.sockets.* 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/data/ResultObj.kt: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | class ResultObj( 7 | var status: Int, 8 | var message: String? = null, 9 | var data: T? = null, 10 | ) 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/data/store/SettingsWrapper.kt: -------------------------------------------------------------------------------- 1 | package data.store 2 | 3 | import com.russhwolf.settings.ExperimentalSettingsApi 4 | import com.russhwolf.settings.coroutines.FlowSettings 5 | 6 | expect class SettingsWrapper { 7 | @OptIn(ExperimentalSettingsApi::class) 8 | fun createSettings(): FlowSettings 9 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-anydpi-v26/logo_pro.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-anydpi-v26/logo_pro_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-512.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "512x512" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | 3 | #Gradle 4 | org.gradle.jvmargs=-Xmx4608m -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" 5 | 6 | #Android 7 | android.suppressUnsupportedCompileSdk=35 8 | android.nonTransitiveRClass=true 9 | android.useAndroidX=true 10 | 11 | # ignore 12 | kotlin.native.ignoreDisabledTargets=true -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/biz/StatusBar.kt: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.Color 5 | 6 | expect class StatusBar() { 7 | 8 | @Composable 9 | fun UpdateColor( 10 | statusBarColor: Color, 11 | navigationBarColor: Color, 12 | isLight: Boolean, 13 | ) 14 | 15 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | #distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.7-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/biz/StatusBar.ios.kt: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.Color 5 | 6 | actual class StatusBar actual constructor() { 7 | @Composable 8 | actual fun UpdateColor( 9 | statusBarColor: Color, 10 | navigationBarColor: Color, 11 | isLight: Boolean, 12 | ) { 13 | } 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .kotlin 3 | .gradle 4 | **/build/ 5 | xcuserdata 6 | !src/**/build/ 7 | local.properties 8 | .idea 9 | .DS_Store 10 | captures 11 | .externalNativeBuild 12 | .cxx 13 | *.xcodeproj/* 14 | !*.xcodeproj/project.pbxproj 15 | !*.xcodeproj/xcshareddata/ 16 | !*.xcodeproj/project.xcworkspace/ 17 | !*.xcworkspace/contents.xcworkspacedata 18 | **/xcshareddata/WorkspaceSettings.xcsettings 19 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/biz/StatusBar.desktop.kt: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.Color 5 | 6 | actual class StatusBar actual constructor() { 7 | @Composable 8 | actual fun UpdateColor( 9 | statusBarColor: Color, 10 | navigationBarColor: Color, 11 | isLight: Boolean, 12 | ) { 13 | } 14 | } -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/ui.components/Notification.ios.kt: -------------------------------------------------------------------------------- 1 | package ui.components 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | 6 | actual fun sendAppNotification(title: String, content: String) { 7 | 8 | } 9 | 10 | @Composable 11 | actual fun CheckAppNotificationPermission( 12 | requestPermission: @Composable (() -> Unit) -> Unit 13 | ) { 14 | } 15 | 16 | actual fun clearAppNotification() { 17 | 18 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/biz/AudioPlayer.kt: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import data.AudioSimpleModel 4 | import data.MusicPlayerState 5 | 6 | expect class AudioPlayer(musicPlayerState: MusicPlayerState) { 7 | fun start(id: String) 8 | fun play() 9 | fun pause() 10 | fun next() 11 | fun prev() 12 | fun seekTo(time: Double) 13 | fun cleanUp() 14 | 15 | fun clearSongs() 16 | fun addSongList(songs: Map) 17 | } -------------------------------------------------------------------------------- /.fleet/receipt.json: -------------------------------------------------------------------------------- 1 | // Project generated by Kotlin Multiplatform Wizard 2 | { 3 | "spec": { 4 | "template_id": "kmt", 5 | "targets": { 6 | "android": { 7 | "ui": [ 8 | "compose" 9 | ] 10 | }, 11 | "desktop": { 12 | "ui": [ 13 | "compose" 14 | ] 15 | } 16 | } 17 | }, 18 | "timestamp": "2024-07-02T05:38:41.891342060Z" 19 | } -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/ui.components/MediaPlayer.ios.kt: -------------------------------------------------------------------------------- 1 | package ui.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | 8 | @Composable 9 | actual fun MediaPlayer(modifier: Modifier, url: String) { 10 | 11 | Column { 12 | Text("目前版本对于JavaFx以及vlcj、包括Swing的支持都很差,windows端暂时不支持视频播放") 13 | Text("") 14 | } 15 | 16 | } 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/ui/components/MediaPlayer.desktop.kt: -------------------------------------------------------------------------------- 1 | package ui.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | 8 | @Composable 9 | actual fun MediaPlayer(modifier: Modifier, url: String) { 10 | 11 | Column { 12 | Text("目前版本对于JavaFx以及vlcj、包括Swing的支持都很差,windows端暂时不支持视频播放") 13 | Text("") 14 | } 15 | 16 | } 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/di/CommonModule.ios.kt: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | 4 | import com.russhwolf.settings.ExperimentalSettingsApi 5 | import data.store.SettingsWrapper 6 | import org.koin.core.module.Module 7 | import org.koin.core.qualifier.named 8 | import org.koin.dsl.module 9 | 10 | 11 | @OptIn(ExperimentalSettingsApi::class) 12 | actual fun platformModule(): Module = module { 13 | 14 | single { SettingsWrapper().createSettings() } 15 | 16 | single(qualifier = named("isMobile")) { 17 | true 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/user_solid.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/di/CommonModule.android.kt: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | 4 | import com.russhwolf.settings.ExperimentalSettingsApi 5 | import data.store.SettingsWrapper 6 | import org.koin.core.module.Module 7 | import org.koin.core.qualifier.named 8 | import org.koin.dsl.module 9 | 10 | @OptIn(ExperimentalSettingsApi::class) 11 | actual fun platformModule(): Module = module { 12 | single { 13 | SettingsWrapper().createSettings() 14 | } 15 | 16 | single(qualifier = named("isMobile")) { 17 | true 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/di/KoinInit.kt: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import org.koin.core.Koin 4 | import org.koin.core.context.startKoin 5 | 6 | import org.koin.dsl.KoinAppDeclaration 7 | 8 | class KoinInit { 9 | fun init(appDeclaration: KoinAppDeclaration = {}): Koin { 10 | return startKoin { 11 | modules( 12 | listOf( 13 | commonModule(), 14 | platformModule(), 15 | ), 16 | ) 17 | appDeclaration() 18 | }.koin 19 | } 20 | 21 | 22 | } -------------------------------------------------------------------------------- /iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import ComposeApp 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | MainViewControllerKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_exclamation_circle.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_check_circle.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import FirebaseCore 3 | 4 | 5 | class AppDelegate: NSObject, UIApplicationDelegate { 6 | func application(_ application: UIApplication, 7 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 8 | // FirebaseApp.configure() 9 | 10 | return true 11 | } 12 | } 13 | 14 | @main 15 | struct iOSApp: App { 16 | 17 | @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate 18 | 19 | var body: some Scene { 20 | WindowGroup { 21 | ContentView() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/data/store/SettingsWrapper.desktop.kt: -------------------------------------------------------------------------------- 1 | package data.store 2 | 3 | import com.russhwolf.settings.ExperimentalSettingsApi 4 | import com.russhwolf.settings.PreferencesSettings 5 | import com.russhwolf.settings.coroutines.FlowSettings 6 | import com.russhwolf.settings.coroutines.toFlowSettings 7 | import java.util.prefs.Preferences 8 | 9 | actual class SettingsWrapper { 10 | @OptIn(ExperimentalSettingsApi::class) 11 | actual fun createSettings(): FlowSettings { 12 | val delegate: Preferences = Preferences.userRoot() 13 | return PreferencesSettings(delegate).toFlowSettings() 14 | } 15 | } -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/data.store/SettingsWrapper.ios.kt: -------------------------------------------------------------------------------- 1 | package data.store 2 | 3 | import com.russhwolf.settings.ExperimentalSettingsApi 4 | import com.russhwolf.settings.NSUserDefaultsSettings 5 | import com.russhwolf.settings.coroutines.FlowSettings 6 | import com.russhwolf.settings.coroutines.toFlowSettings 7 | import platform.Foundation.NSUserDefaults 8 | 9 | actual class SettingsWrapper { 10 | @OptIn(ExperimentalSettingsApi::class) 11 | actual fun createSettings(): FlowSettings { 12 | val delegate: NSUserDefaults = NSUserDefaults.standardUserDefaults 13 | return NSUserDefaultsSettings(delegate).toFlowSettings() 14 | } 15 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/data/ArticleModel.kt: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ArticleSimpleModel( 8 | @SerialName("id") 9 | val id: String? = null, 10 | val authorId: String? = null, 11 | val authorName: String? = null, 12 | val createTime: String? = null, 13 | val authorAvatar: String? = null, 14 | val articleTitle: String? = null, 15 | val articleTagList: List? = null, 16 | val articleBrief: String? = null, 17 | val readNum: Int? = 0, 18 | val likeNum: Int? = 0, 19 | val commentNum: Int? = 0, 20 | ) -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/MainViewController.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.ComposeUIViewController 2 | import constant.enums.MainNavigationEnum 3 | import data.PlatformInitData 4 | import di.KoinInit 5 | 6 | fun MainViewController() = ComposeUIViewController( 7 | configure = { 8 | KoinInit().init { 9 | modules() 10 | } 11 | } 12 | ) { MainApp( 13 | platformData = PlatformInitData( 14 | extraNavigationList = listOf( 15 | MainNavigationEnum.ARTICLES, 16 | MainNavigationEnum.Contacts, 17 | MainNavigationEnum.MUSICS, 18 | MainNavigationEnum.SETTING, 19 | ) 20 | ) 21 | ) } -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/media_circle_pause.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "", 4 | "project_id": "", 5 | "storage_bucket": "" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:123456:android:123456", 11 | "android_client_info": { 12 | "package_name": "com.aster.yuno.tomoyo" 13 | } 14 | }, 15 | "oauth_client": [], 16 | "api_key": [ 17 | { 18 | "current_key": "" 19 | } 20 | ], 21 | "services": { 22 | "appinvite_service": { 23 | "other_platform_oauth_client": [] 24 | } 25 | } 26 | } 27 | ], 28 | "configuration_version": "" 29 | } 30 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/ui/components/MediaPlayer.android.kt: -------------------------------------------------------------------------------- 1 | package ui.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | 6 | @Composable 7 | actual fun MediaPlayer(modifier: Modifier, url: String) { 8 | // AndroidView( 9 | // modifier = modifier, 10 | // factory = { context -> 11 | // VideoView(context).apply { 12 | // setVideoPath(url) 13 | // val mediaController = MediaController(context) 14 | // mediaController.setAnchorView(this) 15 | // setMediaController(mediaController) 16 | // start() 17 | // } 18 | // }, 19 | // update = {}) 20 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/id_badge_solid.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/envelope_alt.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_microphone.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_map_pin.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/skull_solid.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/media_repeat_all.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_folder.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/media_circle_play.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/media_play.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/media_circle_next.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_plus_square.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_map_marker.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_send.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_question_circle.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/media_circle_previous.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/media_next.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_tag.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_sliders_h.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/media_previous.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/stackoverflow.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/biz/MainUtils.desktop.kt: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import java.awt.KeyboardFocusManager 4 | import java.awt.Toolkit 5 | import java.awt.datatransfer.StringSelection 6 | 7 | actual fun copyToClipboard(text: String) { 8 | val clipboard = Toolkit.getDefaultToolkit().systemClipboard 9 | val stringSelection = StringSelection(text) 10 | clipboard.setContents(stringSelection, null) 11 | } 12 | 13 | actual fun logInfo(text: String) { 14 | } 15 | 16 | actual fun isAppInForeground(): Boolean { 17 | return KeyboardFocusManager.getCurrentKeyboardFocusManager().activeWindow != null; 18 | } 19 | 20 | actual fun afterLogin() { 21 | } 22 | 23 | actual fun beforeLogout() { 24 | } 25 | 26 | actual fun getPlatform(): String { 27 | return "desktop" 28 | } 29 | 30 | actual fun setUpdateGoogleFirebaseToken(operation: (String) -> Unit) { 31 | } -------------------------------------------------------------------------------- /iosApp/iosApp/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API_KEY 6 | 7 | GCM_SENDER_ID 8 | 9 | PLIST_VERSION 10 | 1 11 | BUNDLE_ID 12 | 13 | PROJECT_ID 14 | 15 | STORAGE_BUCKET 16 | 17 | IS_ADS_ENABLED 18 | 19 | IS_ANALYTICS_ENABLED 20 | 21 | IS_APPINVITE_ENABLED 22 | 23 | IS_GCM_ENABLED 24 | 25 | IS_SIGNIN_ENABLED 26 | 27 | GOOGLE_APP_ID 28 | 29 | 30 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/drumstick_bite_solid.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_upload.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/media_audio.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_compass.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_film.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/twitter.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_heart.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/media_pause.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/media_circle_stop.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_user.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/user_graduate_solid.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_link.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/data/store/SettingsWrapper.android.kt: -------------------------------------------------------------------------------- 1 | package data.store 2 | 3 | import android.content.Context 4 | import androidx.datastore.preferences.preferencesDataStore 5 | import com.russhwolf.settings.ExperimentalSettingsApi 6 | import com.russhwolf.settings.ExperimentalSettingsImplementation 7 | import com.russhwolf.settings.coroutines.FlowSettings 8 | import com.russhwolf.settings.datastore.DataStoreSettings 9 | import org.koin.core.component.KoinComponent 10 | import org.koin.core.component.inject 11 | 12 | actual class SettingsWrapper : KoinComponent { 13 | private val context: Context by inject() 14 | 15 | companion object { 16 | private val Context.dataStore by preferencesDataStore("userSettings") 17 | } 18 | 19 | 20 | @OptIn(ExperimentalSettingsImplementation::class, ExperimentalSettingsApi::class) 21 | actual fun createSettings(): FlowSettings { 22 | return DataStoreSettings(context.dataStore) 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /composeApp/config/compose_compiler_config.conf: -------------------------------------------------------------------------------- 1 | com.github.panpf.sketch.Sketch 2 | com.github.panpf.sketch.PlatformContext 3 | com.github.panpf.sketch.drawable.DrawableEqualizer 4 | com.github.panpf.sketch.request.Image 5 | com.github.panpf.sketch.request.ImageOptions 6 | com.github.panpf.sketch.request.ImageRequest 7 | com.github.panpf.sketch.request.ImageResult 8 | com.github.panpf.sketch.state.ColorDrawableStateImage 9 | com.github.panpf.sketch.state.CurrentStateImage 10 | com.github.panpf.sketch.state.DrawableStateImage 11 | com.github.panpf.sketch.state.ErrorStateImage 12 | com.github.panpf.sketch.state.MemoryCacheStateImage 13 | com.github.panpf.sketch.state.IconAnimatableStateImage 14 | com.github.panpf.sketch.state.IconStateImage 15 | com.github.panpf.sketch.state.StateImage 16 | com.github.panpf.sketch.state.ThumbnailMemoryCacheStateImage 17 | com.github.panpf.sketch.util.ColorFetcher 18 | com.github.panpf.sketch.util.Equalizer 19 | com.github.panpf.sketch.util.IntColor 20 | com.github.panpf.sketch.util.Size -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/biz/MainUtils.ios.kt: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import platform.UIKit.UIApplication 4 | import platform.UIKit.UIApplicationState 5 | import platform.UIKit.UIPasteboard 6 | 7 | actual fun copyToClipboard(text: String) { 8 | UIPasteboard.generalPasteboard.string = text 9 | } 10 | 11 | actual fun logInfo(text: String) { 12 | } 13 | 14 | actual fun isAppInForeground(): Boolean { 15 | val application = UIApplication.sharedApplication 16 | 17 | return when (application.applicationState) { 18 | UIApplicationState.UIApplicationStateActive -> true 19 | UIApplicationState.UIApplicationStateInactive -> true 20 | UIApplicationState.UIApplicationStateBackground -> false 21 | else -> false 22 | } 23 | } 24 | 25 | actual fun afterLogin() { 26 | } 27 | 28 | actual fun beforeLogout() { 29 | } 30 | 31 | actual fun getPlatform(): String { 32 | return "mobile_ios" 33 | } 34 | 35 | actual fun setUpdateGoogleFirebaseToken(operation: (String) -> Unit) { 36 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_image.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/media_repeat_one.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_camera.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/envelope_question.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/envelope_heart.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/media_shuffle.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/github.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_trash.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 AsterCasc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_package.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_star.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/envelope_exclamation.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_map.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/ui/components/Notification.desktop.kt: -------------------------------------------------------------------------------- 1 | package ui.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import koin 5 | import org.koin.core.qualifier.named 6 | import tray 7 | import java.awt.event.ActionListener 8 | import java.awt.image.BufferedImage 9 | import javax.swing.Timer 10 | 11 | val timer = Timer(500, object : ActionListener { 12 | var toggle = true 13 | val showIcon: BufferedImage = koin.get(named("superLowDpiIcon")) 14 | val hideIcon: BufferedImage = koin.get(named("hideIcon")) 15 | 16 | 17 | override fun actionPerformed(event: java.awt.event.ActionEvent?) { 18 | tray.trayIcons[0].image = if (toggle) showIcon else hideIcon 19 | toggle = !toggle 20 | 21 | } 22 | }) 23 | 24 | actual fun sendAppNotification(title: String, content: String) { 25 | timer.start() 26 | } 27 | 28 | @Composable 29 | actual fun CheckAppNotificationPermission( 30 | requestPermission: @Composable (() -> Unit) -> Unit 31 | ) { 32 | } 33 | 34 | actual fun clearAppNotification() { 35 | timer.stop() 36 | val showIcon: BufferedImage = koin.get(named("superLowDpiIcon")) 37 | 38 | tray.trayIcons[0].image = showIcon 39 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/google.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_store.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/user_secret_solid.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_calendar.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/envelope_star.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/di/CommonModule.desktop.kt: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import com.russhwolf.settings.ExperimentalSettingsApi 4 | import data.store.SettingsWrapper 5 | import org.koin.core.module.Module 6 | import org.koin.core.qualifier.named 7 | import org.koin.dsl.module 8 | import java.awt.image.BufferedImage 9 | import javax.imageio.ImageIO 10 | 11 | @OptIn(ExperimentalSettingsApi::class) 12 | actual fun platformModule(): Module = module { 13 | single { SettingsWrapper().createSettings() } 14 | 15 | single(qualifier = named("isMobile")) { 16 | false 17 | } 18 | 19 | single(qualifier = named("superLowDpiIcon")) { 20 | ImageIO.read( 21 | Thread.currentThread().contextClassLoader 22 | .getResource("logo_pro_round_32.png") 23 | ) 24 | } 25 | 26 | single(qualifier = named("showIcon")) { 27 | ImageIO.read( 28 | Thread.currentThread().contextClassLoader 29 | .getResource("logo_pro_round.png") 30 | ) 31 | } 32 | 33 | single(qualifier = named("hideIcon")) { 34 | ImageIO.read( 35 | Thread.currentThread().contextClassLoader 36 | .getResource("empty.png") 37 | ) 38 | 39 | 40 | } 41 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_phone.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/di/CommonModule.kt: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | 4 | import com.russhwolf.settings.ExperimentalSettingsApi 5 | import data.model.ArticleScreenModel 6 | import data.model.ChatScreenModel 7 | import data.model.ContactScreenModel 8 | import data.model.GlobalDataModel 9 | import data.model.MainScreenModel 10 | import data.model.MusicScreenModel 11 | import data.store.DataStorageManager 12 | import org.koin.core.module.Module 13 | import org.koin.dsl.module 14 | 15 | @OptIn(ExperimentalSettingsApi::class) 16 | fun commonModule() = module { 17 | 18 | //global 19 | single { 20 | GlobalDataModel() 21 | } 22 | 23 | //common 24 | single { 25 | ArticleScreenModel() 26 | } 27 | 28 | single { 29 | ChatScreenModel(dataStorageManager = get()) 30 | } 31 | 32 | single { 33 | ContactScreenModel() 34 | } 35 | 36 | single { 37 | MusicScreenModel(dataStorageManager = get()) 38 | } 39 | 40 | //main 41 | single { 42 | MainScreenModel() 43 | } 44 | 45 | //db 46 | single { 47 | DataStorageManager(settings = get()) 48 | } 49 | 50 | } 51 | 52 | expect fun platformModule(): Module -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/data/store/DataStorageManager.kt: -------------------------------------------------------------------------------- 1 | package data.store 2 | 3 | import com.russhwolf.settings.ExperimentalSettingsApi 4 | import com.russhwolf.settings.ObservableSettings 5 | import com.russhwolf.settings.coroutines.FlowSettings 6 | import com.russhwolf.settings.coroutines.toBlockingObservableSettings 7 | import com.russhwolf.settings.set 8 | 9 | class DataStorageManager @OptIn(ExperimentalSettingsApi::class) constructor(private val settings: FlowSettings) { 10 | 11 | @OptIn(ExperimentalSettingsApi::class) 12 | private val observableSettings: ObservableSettings by lazy { settings.toBlockingObservableSettings() } 13 | 14 | 15 | fun setString(key: String, value: String) { 16 | observableSettings.set(key = key, value = value) 17 | } 18 | 19 | fun getNonFlowString(key: String) = observableSettings.getString( 20 | key = key, 21 | defaultValue = "", 22 | ) 23 | 24 | 25 | fun clearPreferences() { 26 | observableSettings.clear() 27 | } 28 | 29 | companion object { 30 | const val USER_DATA = "user_data" 31 | const val FAV_AUDIO_ID_LIST = "fav_audio_id_list" 32 | const val RECENT_EMOJI_LIST = "recent_emoji_list" 33 | const val RECENT_KAOMOJI_LIST = "recent_kaomoji_list" 34 | const val RECENT_EMOJI_PRO_LIST = "recent_emoji_pro_list" 35 | const val USER_THEME = "user_theme" 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_users_three.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_globe.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/reddit.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/pages/MainVideos.kt: -------------------------------------------------------------------------------- 1 | package ui.pages 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | import cafe.adriel.voyager.core.screen.Screen 14 | import cafe.adriel.voyager.core.screen.ScreenKey 15 | import cafe.adriel.voyager.core.screen.uniqueScreenKey 16 | import constant.enums.ViewEnum 17 | import ui.components.MediaPlayer 18 | 19 | 20 | object MainVideosScreen : Screen { 21 | 22 | override val key: ScreenKey = "${ViewEnum.TAB_MAIN_VIDEOS.code}$uniqueScreenKey" 23 | 24 | @Composable 25 | override fun Content() { 26 | MainVideosScreen() 27 | } 28 | 29 | } 30 | 31 | 32 | @Composable 33 | fun MainVideosScreen() { 34 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 35 | Column( 36 | horizontalAlignment = Alignment.CenterHorizontally, 37 | verticalArrangement = Arrangement.SpaceAround 38 | ) { 39 | MediaPlayer( 40 | modifier = Modifier.fillMaxWidth().height(400.dp), 41 | url = 42 | "" 43 | ) 44 | 45 | 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/data/MusicModel.kt: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.runtime.setValue 9 | import constant.enums.MusicPlayModel 10 | import kotlinx.serialization.SerialName 11 | import kotlinx.serialization.Serializable 12 | 13 | 14 | @Serializable 15 | data class AudioSimpleModel( 16 | @SerialName("id") 17 | val id: String = "", 18 | val audioCollectionId: String? = null, 19 | val audioOrder: Int = 1, 20 | val audioName: String = "", 21 | val audioImg: String? = null, 22 | val audioBrief: String? = null, 23 | val audioAuthor: String = "", 24 | val audioUrl: String = "", 25 | ) 26 | 27 | @Stable 28 | class MusicPlayerState { 29 | var isPlaying by mutableStateOf(false) 30 | internal set 31 | var isBuffering by mutableStateOf(false) 32 | var currentTime by mutableStateOf(0.0) 33 | var totalDuration by mutableStateOf(0.0) 34 | 35 | var currentPlayId by mutableStateOf("") 36 | var currentCollectionId by mutableStateOf("") 37 | var playModel by mutableStateOf(MusicPlayModel.ORDER.ordinal) 38 | 39 | fun toBack() { 40 | isPlaying = false 41 | isBuffering = false 42 | currentTime = 0.0 43 | totalDuration = 0.0 44 | } 45 | } 46 | 47 | @Composable 48 | fun rememberMusicPlayerState(): MusicPlayerState { 49 | return remember { 50 | MusicPlayerState() 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "tomoyo" 2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 3 | 4 | 5 | pluginManagement { 6 | repositories { 7 | // maven(url = "https://mirrors.cloud.tencent.com/gradle/") 8 | 9 | maven(url = "https://maven.aliyun.com/repository/public/") 10 | maven(url = "https://maven.aliyun.com/repository/jcenter/") 11 | maven(url = "https://maven.aliyun.com/repository/google/") 12 | maven(url = "https://maven.aliyun.com/repository/gradle-plugin/") 13 | 14 | 15 | 16 | 17 | 18 | google { 19 | mavenContent { 20 | includeGroupAndSubgroups("androidx") 21 | includeGroupAndSubgroups("com.android") 22 | includeGroupAndSubgroups("com.google") 23 | } 24 | } 25 | mavenCentral() 26 | gradlePluginPortal() 27 | } 28 | } 29 | 30 | dependencyResolutionManagement { 31 | repositories { 32 | maven("https://jogamp.org/deployment/maven") 33 | 34 | // maven(url = "https://mirrors.cloud.tencent.com/gradle/") 35 | 36 | // maven(url = "https://maven.aliyun.com/repository/public/") 37 | // maven(url = "https://maven.aliyun.com/repository/jcenter/") 38 | // maven(url = "https://maven.aliyun.com/repository/google/") 39 | // maven(url = "https://maven.aliyun.com/repository/gradle-plugin/") 40 | 41 | 42 | google { 43 | mavenContent { 44 | includeGroupAndSubgroups("androidx") 45 | includeGroupAndSubgroups("com.android") 46 | includeGroupAndSubgroups("com.google") 47 | } 48 | } 49 | mavenCentral() 50 | } 51 | } 52 | 53 | include(":composeApp") -------------------------------------------------------------------------------- /iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | CADisableMinimumFrameDurationOnPhone 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/biz/MainUtils.android.kt: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.ActivityManager 5 | import android.content.ClipData 6 | import android.content.ClipboardManager 7 | import android.content.Context 8 | import android.util.Log 9 | import com.google.firebase.messaging.FirebaseMessaging 10 | import org.koin.java.KoinJavaComponent.inject 11 | 12 | actual fun copyToClipboard(text: String) { 13 | val context: Context by inject(Context::class.java) 14 | val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 15 | val clipData = ClipData.newPlainText("Tomoyo Copy", text) 16 | clipboardManager.setPrimaryClip(clipData) 17 | } 18 | 19 | actual fun logInfo(text: String) { 20 | Log.i("Tomoyo", text) 21 | } 22 | 23 | @SuppressLint("ServiceCast") 24 | actual fun isAppInForeground(): Boolean { 25 | val context: Context by inject(Context::class.java) 26 | val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 27 | val appProcesses = activityManager.runningAppProcesses ?: return false 28 | 29 | val packageName = context.packageName 30 | for (appProcess in appProcesses) { 31 | if (appProcess.processName == packageName) { 32 | return appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND 33 | } 34 | } 35 | return false 36 | } 37 | 38 | actual fun afterLogin() { 39 | reloadGoogleMessageToken() 40 | } 41 | 42 | actual fun beforeLogout() { 43 | FirebaseMessaging.getInstance().deleteToken() 44 | } 45 | 46 | actual fun getPlatform(): String { 47 | return "mobile_android" 48 | } 49 | 50 | actual fun setUpdateGoogleFirebaseToken(operation: (String) -> Unit) { 51 | updateGoogleFirebaseTokenFun = operation 52 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/data/model/ContactScreenModel.kt: -------------------------------------------------------------------------------- 1 | package data.model 2 | 3 | import api.BaseApi 4 | import cafe.adriel.voyager.core.model.ScreenModel 5 | import constant.enums.UserDetailTabScreenTabModel 6 | import data.PublicUserSimpleModel 7 | import data.UserDetailModel 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | import kotlinx.coroutines.flow.asStateFlow 10 | 11 | class ContactScreenModel : ScreenModel { 12 | 13 | private val _loadAllPublicUser = MutableStateFlow(false) 14 | val loadAllPublicUser = _loadAllPublicUser.asStateFlow() 15 | 16 | private val _publicUserDataList = MutableStateFlow(emptyList()) 17 | val publicUserDataList = _publicUserDataList.asStateFlow() 18 | suspend fun loadPublicUser() { 19 | if (_publicUserDataList.value.isNotEmpty()) { 20 | return 21 | } 22 | _publicUserDataList.value = BaseApi().getPublicUser() 23 | _loadAllPublicUser.value = true 24 | } 25 | 26 | private val _userDetail = MutableStateFlow(UserDetailModel()) 27 | val userDetail = _userDetail.asStateFlow() 28 | suspend fun updateUserDetail(userId: String) { 29 | if (_userDetail.value.id == userId) { 30 | return 31 | } 32 | val articleNum = BaseApi().getArticleCount(userId, 1) 33 | val thoughtNum = BaseApi().getArticleCount(userId, 2) 34 | val baseData = BaseApi().getUserDetail(userId) 35 | baseData.articleNum = articleNum 36 | baseData.thoughtNum = thoughtNum 37 | _userDetail.value = baseData 38 | } 39 | 40 | //tab 41 | private val _userDetailTab = MutableStateFlow(UserDetailTabScreenTabModel.FRIENDS) 42 | val userDetailTab = _userDetailTab.asStateFlow() 43 | fun updateUserDetailTab(tab: UserDetailTabScreenTabModel) { 44 | _userDetailTab.value = tab 45 | } 46 | 47 | 48 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/biz/StatusBar.android.kt: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import android.app.Activity 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.SideEffect 6 | import androidx.compose.ui.graphics.Color 7 | import androidx.compose.ui.graphics.toArgb 8 | import androidx.compose.ui.platform.LocalView 9 | import androidx.core.view.WindowCompat 10 | 11 | actual class StatusBar actual constructor() { 12 | @Composable 13 | actual fun UpdateColor( 14 | statusBarColor: Color, 15 | navigationBarColor: Color, 16 | isLight: Boolean, 17 | ) { 18 | val view = LocalView.current 19 | 20 | if (!view.isInEditMode) { 21 | SideEffect { 22 | val window = (view.context as Activity).window 23 | 24 | val insetsController = WindowCompat.getInsetsController(window, view) 25 | window.statusBarColor = statusBarColor.toArgb() 26 | window.navigationBarColor = navigationBarColor.toArgb() 27 | insetsController.isAppearanceLightStatusBars = isLight 28 | insetsController.isAppearanceLightNavigationBars = isLight 29 | 30 | // same color to main 31 | // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 32 | // window.isNavigationBarContrastEnforced = false 33 | // } 34 | } 35 | } 36 | 37 | 38 | 39 | 40 | 41 | //use enableEdgeToEdge() instead 42 | 43 | // val view = LocalView.current 44 | // if (!view.isInEditMode) { 45 | // SideEffect { 46 | // val window = (view.context as Activity).window 47 | // window.statusBarColor = bgColor.toArgb() 48 | // WindowCompat.getInsetsController(window, view) 49 | // .isAppearanceLightStatusBars = textColorIsDark 50 | // } 51 | // } 52 | } 53 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/ui/components/StartLoading.kt: -------------------------------------------------------------------------------- 1 | //package ui.components 2 | // 3 | //import javafx.application.Application 4 | //import javafx.scene.Scene 5 | //import javafx.scene.layout.StackPane 6 | //import javafx.scene.media.Media 7 | //import javafx.scene.media.MediaException 8 | //import javafx.scene.media.MediaPlayer 9 | //import javafx.scene.media.MediaView 10 | //import javafx.stage.Stage 11 | // 12 | // 13 | //class StartLoading : Application() { 14 | // override fun start(primaryStage: Stage) { 15 | // val videoUrl = "https://api.astercasc.com/ushio/video/play/VC1648909883875288/output.mp4" 16 | // 17 | // 18 | // // Create a Media object for the video 19 | // val media = Media(videoUrl) 20 | // 21 | // 22 | // // Create a MediaPlayer to control the playback of the media 23 | // val mediaPlayer = MediaPlayer(media) 24 | // 25 | // 26 | // // Handle media errors 27 | // mediaPlayer.onError = Runnable { 28 | // val error: MediaException = mediaPlayer.error 29 | // println("Media error occurred: " + error.message) 30 | // } 31 | // 32 | // // Handle media view errors 33 | // media.onError = Runnable { 34 | // val error: MediaException = media.error 35 | // println("Media error event: " + error.message) 36 | // } 37 | // 38 | // 39 | // // Create a MediaView to display the video 40 | // val mediaView = MediaView(mediaPlayer) 41 | // 42 | // 43 | // // Add the MediaView to a layout pane 44 | // val root = StackPane() 45 | // root.children.add(mediaView) 46 | // 47 | // 48 | // // Create and set up the scene 49 | // val scene = Scene(root, 800.0, 600.0) 50 | // primaryStage.setScene(scene) 51 | // primaryStage.setTitle("Web Video Player") 52 | // primaryStage.show() 53 | // 54 | // // Start playing the video 55 | // mediaPlayer.play() 56 | // } 57 | // 58 | // 59 | //} 60 | // 61 | // 62 | // 63 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/biz/FMessagingService.android.kt: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import android.util.Log 4 | import com.google.firebase.messaging.FirebaseMessaging 5 | import com.google.firebase.messaging.FirebaseMessagingService 6 | import com.google.firebase.messaging.RemoteMessage 7 | import ui.components.sendAppNotification 8 | 9 | var globalGoogleFirebaseToken = "" 10 | 11 | var updateGoogleFirebaseTokenFun: (String) -> Unit = {} 12 | 13 | fun reloadGoogleMessageToken() { 14 | FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> 15 | if (task.isSuccessful) { 16 | updaterToken(task.result) 17 | } else { 18 | Log.w("Tomoyo", "Failed to get google message token ", task.exception) 19 | } 20 | } 21 | } 22 | 23 | private fun updaterToken(token: String) { 24 | if (token != globalGoogleFirebaseToken) { 25 | globalGoogleFirebaseToken = token 26 | updateGoogleFirebaseTokenFun(token) 27 | Log.i("Tomoyo", "Current google message token: $globalGoogleFirebaseToken") 28 | } 29 | } 30 | 31 | 32 | class FMessagingService : FirebaseMessagingService() { 33 | 34 | override fun onNewToken(token: String) { 35 | super.onNewToken(token) 36 | updaterToken(token) 37 | } 38 | 39 | override fun onMessageReceived(remoteMessage: RemoteMessage) { 40 | super.onMessageReceived(remoteMessage) 41 | 42 | // Client notification 43 | remoteMessage.notification?.let { 44 | showNotification(it.title, it.body) 45 | } 46 | 47 | // Biz logic 48 | remoteMessage.data.let { 49 | if (it.isNotEmpty()) { 50 | processData(it) 51 | } 52 | } 53 | } 54 | 55 | private fun showNotification(title: String?, body: String?) { 56 | if (title.isNullOrBlank() && body.isNullOrBlank()) { 57 | sendAppNotification(title!!, body!!) 58 | } 59 | } 60 | 61 | 62 | private fun processData(data: Map) { 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/aster/yuno/tomoyo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.aster.yuno.tomoyo 2 | 3 | import MainApp 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.activity.enableEdgeToEdge 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.tooling.preview.Preview 10 | import biz.PlayerHolder 11 | import constant.enums.MainNavigationEnum 12 | import data.PlatformInitData 13 | import di.KoinInit 14 | import org.koin.android.ext.koin.androidContext 15 | import org.koin.android.ext.koin.androidLogger 16 | import org.koin.core.context.GlobalContext 17 | import ui.components.clearAppNotification 18 | import ui.components.createAppNotificationChannel 19 | 20 | 21 | class MainActivity : ComponentActivity() { 22 | 23 | 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | 26 | enableEdgeToEdge() 27 | 28 | super.onCreate(savedInstanceState) 29 | 30 | PlayerHolder.init(applicationContext) 31 | 32 | if (GlobalContext.getOrNull() == null) { 33 | KoinInit().init { 34 | androidLogger() 35 | androidContext(applicationContext) 36 | modules() 37 | } 38 | } 39 | 40 | createAppNotificationChannel(this) 41 | 42 | clearAppNotification() 43 | 44 | setContent { 45 | MainApp( 46 | platformData = PlatformInitData( 47 | extraNavigationList = listOf( 48 | MainNavigationEnum.ARTICLES, 49 | MainNavigationEnum.Contacts, 50 | MainNavigationEnum.MUSICS, 51 | MainNavigationEnum.SETTING, 52 | ) 53 | ) 54 | ) 55 | 56 | } 57 | } 58 | 59 | override fun onResume() { 60 | super.onResume() 61 | clearAppNotification() 62 | } 63 | } 64 | 65 | @Preview 66 | @Composable 67 | fun AppAndroidPreview() { 68 | MainApp() 69 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/data/ChatModel.kt: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import kotlinx.coroutines.flow.MutableStateFlow 4 | import kotlinx.serialization.Serializable 5 | 6 | 7 | @Serializable 8 | data class UserChatStarEmojiSimple( 9 | var addressType: Int = 0, 10 | var id: String = "", 11 | var readAddress: String = "", 12 | ) 13 | 14 | 15 | @Serializable 16 | data class UserChatStarEmojis( 17 | var fileEmojis: MutableList = mutableListOf(), 18 | var total: Int? = 0, 19 | ) 20 | 21 | @Serializable 22 | data class UserChattingSimple( 23 | var chatId: String? = null, 24 | var chatType: Int? = null, 25 | var chatName: String? = null, 26 | var chatAvatar: String? = null, 27 | var chatUserId: String? = null, 28 | var chatUserGender: Int? = null, 29 | var chatUserRoleType: Int? = null, 30 | var lastMessageTime: String? = null, 31 | var lastMessageId: String? = null, 32 | var userChattingData: MutableList = mutableListOf(), 33 | var latestRead: Boolean? = null, 34 | var lastMessageText: String? = null, 35 | 36 | var userChattingDataFlow: MutableStateFlow> = MutableStateFlow(emptyList()), 37 | var clientLoadAllHistoryMessage: MutableStateFlow = MutableStateFlow(false), 38 | var latestReadWeb: MutableStateFlow = MutableStateFlow(true), 39 | ) 40 | 41 | 42 | @Serializable 43 | data class UserChatMsgDto( 44 | var messageId: String? = null, 45 | var sendUserId: String? = null, 46 | var sendUserNickname: String? = null, 47 | var sendUserAvatar: String? = null, 48 | var sendUserGender: Int? = null, 49 | var sendUserRoleType: Int? = null, 50 | var sendDate: String? = null, 51 | var message: String? = null, 52 | //web 53 | var webChatLabel: String? = null, 54 | ) 55 | 56 | @Serializable 57 | data class UserChatParam( 58 | val toUserId: String 59 | ) 60 | 61 | 62 | @Serializable 63 | data class UserUploadRetDto( 64 | val id: String?, 65 | val readAddress: String? 66 | ) 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/data/model/GlobalDataModel.kt: -------------------------------------------------------------------------------- 1 | package data.model 2 | 3 | import api.BaseApi 4 | import biz.logInfo 5 | import data.UserChatStarEmojiSimple 6 | import data.UserDataModel 7 | import data.UserExData 8 | import data.UserState 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.asStateFlow 11 | import kotlinx.coroutines.flow.update 12 | 13 | class GlobalDataModel { 14 | 15 | //user data 16 | private val _userState = MutableStateFlow(UserState()) 17 | val userState = _userState.asStateFlow() 18 | 19 | fun clearLocalUserState() { 20 | logInfo("[op:clearLocalUserState] User data clear") 21 | _userState.value.userData = UserDataModel() 22 | _userState.value.token = "" 23 | clearLocalUserExData() 24 | } 25 | 26 | private val _userExData = UserExData() 27 | fun getUserExData(): UserExData { 28 | return _userExData 29 | } 30 | 31 | fun resetUserEmoji(data: List) { 32 | _userExData.emojiProListFlow.update { currentList -> 33 | currentList.toMutableList().apply { 34 | this.addAll(data) 35 | } 36 | } 37 | } 38 | 39 | fun clearLocalUserExData() { 40 | _userExData.emojiProListFlow.update { currentList -> 41 | currentList.toMutableList().apply { 42 | this.clear() 43 | } 44 | } 45 | } 46 | 47 | //network 48 | private val _netStatus = MutableStateFlow(true) 49 | val netStatus = _netStatus.asStateFlow() 50 | 51 | suspend fun checkNetwork() { 52 | _netStatus.value = BaseApi().checkNetwork() 53 | } 54 | 55 | fun resetNetStatus(status: Boolean) { 56 | _netStatus.value = status 57 | } 58 | 59 | //socket 60 | private val _socketConnected = MutableStateFlow(true) 61 | val socketConnected = _socketConnected.asStateFlow() 62 | fun resetSocketConnected(status: Boolean) { 63 | logInfo("[op:resetSocketConnected] reset") 64 | _socketConnected.value = status 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/data/model/ArticleScreenModel.kt: -------------------------------------------------------------------------------- 1 | package data.model 2 | 3 | import api.BaseApi 4 | import cafe.adriel.voyager.core.model.ScreenModel 5 | import data.ArticleSimpleModel 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.asStateFlow 8 | 9 | class ArticleScreenModel : ScreenModel { 10 | 11 | private val _articleDataKey = MutableStateFlow("") 12 | val articleDataKey = _articleDataKey.asStateFlow() 13 | private val _articleIsLoadAll = MutableStateFlow(false) 14 | val articleIsLoadAll = _articleIsLoadAll.asStateFlow() 15 | 16 | private val _articleDataList = MutableStateFlow(emptyList()) 17 | val articleDataList = _articleDataList.asStateFlow() 18 | suspend fun updateArticleList() { 19 | val newData = BaseApi().getArticleList( 20 | offset = _articleDataList.value.size, 21 | keyword = _articleDataKey.value 22 | ) 23 | _articleIsLoadAll.value = newData.isEmpty() 24 | _articleDataList.value += newData 25 | } 26 | 27 | fun clearResetKeyword(keyword: String = "") { 28 | _articleDataKey.value = keyword 29 | _articleDataList.value = emptyList() 30 | } 31 | 32 | private val _readingArticleId = MutableStateFlow("") 33 | private val _readingArticleMeta = MutableStateFlow(ArticleSimpleModel()) 34 | val readingArticleMeta = _readingArticleMeta.asStateFlow() 35 | private val _readingArticleData = MutableStateFlow("") 36 | val readingArticleData = _readingArticleData.asStateFlow() 37 | fun updateReadingMeta(data: ArticleSimpleModel) { 38 | _readingArticleMeta.value = data 39 | } 40 | suspend fun updateReadingArticleData(articleId: String, token: String) { 41 | if (_readingArticleId.value == articleId) { 42 | return 43 | } 44 | val content = BaseApi().getArticleDetail(articleId, token) 45 | if (content.isNotBlank()) { 46 | _readingArticleData.value = content 47 | _readingArticleId.value = articleId 48 | } else { 49 | _readingArticleId.value = articleId 50 | _readingArticleData.value = "Not Login" 51 | } 52 | 53 | } 54 | 55 | 56 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/amazon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/biz/ChatSocketService.android.kt: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import android.app.Notification 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.app.PendingIntent 7 | import android.app.Service 8 | import android.content.Context 9 | import android.content.Intent 10 | import android.os.IBinder 11 | import androidx.core.app.NotificationCompat 12 | import com.aster.yuno.tomoyo.MainActivity 13 | import com.aster.yuno.tomoyo.R 14 | 15 | class ChatSocketService : Service() { 16 | 17 | override fun onCreate() { 18 | super.onCreate() 19 | 20 | createNotificationChannel() 21 | val notification = buildNotification() 22 | startForeground(NOTIFICATION_ID, notification) 23 | } 24 | 25 | override fun onBind(intent: Intent?): IBinder? { 26 | return null 27 | } 28 | 29 | private fun createNotificationChannel() { 30 | val name = "Chat Service" 31 | val descriptionText = "Service for Chat socket" 32 | val importance = NotificationManager.IMPORTANCE_LOW 33 | val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { 34 | description = descriptionText 35 | } 36 | val notificationManager: NotificationManager = 37 | getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 38 | notificationManager.createNotificationChannel(channel) 39 | } 40 | 41 | private fun buildNotification(): Notification { 42 | val intent = Intent(this, MainActivity::class.java).apply { 43 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 44 | } 45 | val pendingIntent: PendingIntent = 46 | PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE) 47 | 48 | return NotificationCompat.Builder(this, CHANNEL_ID) 49 | .setContentTitle("Tomoyo") 50 | .setContentText("Getting message ...") 51 | .setSmallIcon(R.drawable.logo_pro_background) 52 | .setContentIntent(pendingIntent) 53 | .setOngoing(true) 54 | .build() 55 | } 56 | 57 | companion object { 58 | private const val CHANNEL_ID = "ChatSocketChannel" 59 | private const val NOTIFICATION_ID = 2 60 | } 61 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/input_settings.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.material3.Typography 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.text.font.FontFamily 7 | import tomoyo.composeapp.generated.resources.Res 8 | import tomoyo.composeapp.generated.resources.RobotoSlab_VariableFont_wght 9 | 10 | @Composable 11 | fun MainTypography(): Typography { 12 | 13 | val defaultFontFamily = FontFamily( 14 | org.jetbrains.compose.resources.Font( 15 | Res.font.RobotoSlab_VariableFont_wght 16 | ) 17 | ) 18 | 19 | return Typography( 20 | labelSmall = MaterialTheme.typography.labelSmall.copy( 21 | fontFamily = defaultFontFamily, 22 | ), 23 | labelMedium = MaterialTheme.typography.labelMedium.copy( 24 | fontFamily = defaultFontFamily, 25 | ), 26 | labelLarge = MaterialTheme.typography.labelLarge.copy( 27 | fontFamily = defaultFontFamily, 28 | //fontWeight = FontWeight.Bold, 29 | ), 30 | bodySmall = MaterialTheme.typography.bodySmall.copy( 31 | fontFamily = defaultFontFamily, 32 | ), 33 | bodyMedium = MaterialTheme.typography.bodyMedium.copy( 34 | fontFamily = defaultFontFamily, 35 | ), 36 | bodyLarge = MaterialTheme.typography.bodyLarge.copy( 37 | fontFamily = defaultFontFamily, 38 | //fontWeight = FontWeight.Bold, 39 | ), 40 | titleSmall = MaterialTheme.typography.titleSmall.copy( 41 | fontFamily = defaultFontFamily, 42 | ), 43 | titleMedium = MaterialTheme.typography.titleMedium.copy( 44 | fontFamily = defaultFontFamily, 45 | ), 46 | titleLarge = MaterialTheme.typography.titleLarge.copy( 47 | fontFamily = defaultFontFamily, 48 | ), 49 | headlineSmall = MaterialTheme.typography.headlineSmall.copy( 50 | fontFamily = defaultFontFamily, 51 | ), 52 | headlineMedium = MaterialTheme.typography.headlineMedium.copy( 53 | fontFamily = defaultFontFamily, 54 | ), 55 | headlineLarge = MaterialTheme.typography.headlineLarge.copy( 56 | fontFamily = defaultFontFamily, 57 | ), 58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 40 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/data/UserModel.kt: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.datetime.LocalDate 9 | import kotlinx.serialization.Serializable 10 | 11 | @Serializable 12 | data class UserSocialLinkDto( 13 | var qq: String? = null, 14 | var wechat: String? = null, 15 | var github: String? = null, 16 | ) 17 | 18 | @Serializable 19 | data class UserCommunityModel( 20 | var followNum: Int = 0, 21 | var fansNum: Int = 0, 22 | var friendNum: Int = 0, 23 | ) 24 | 25 | @Serializable 26 | data class UserDetailModel( 27 | var id: String = "", 28 | var nickName: String = "", 29 | var gender: Int = 1, 30 | var roleType: Int = 1, 31 | var avatar: String = "", 32 | var motto: String? = null, 33 | var mail: String = "", 34 | var birth: LocalDate = LocalDate.fromEpochDays(0), 35 | var friendList: List = emptyList(), 36 | var community: UserCommunityModel = UserCommunityModel(), 37 | var socialLink: UserSocialLinkDto = UserSocialLinkDto(), 38 | //insert data 39 | var articleNum: Int? = null, 40 | var thoughtNum: Int? = null, 41 | ) 42 | 43 | @Serializable 44 | data class UserDataModel( 45 | var id: String? = null, 46 | var mail: String? = null, 47 | var account: String? = null, 48 | var avatar: String? = null, 49 | var nickName: String? = null, 50 | var token: String? = null, 51 | ) 52 | 53 | @Serializable 54 | data class PublicUserSimpleModel( 55 | var id: String = "", 56 | var nickName: String = "", 57 | var gender: Int = 1, 58 | var roleType: Int = 1, 59 | var avatar: String = "", 60 | var motto: String? = null, 61 | ) 62 | 63 | @Stable 64 | class UserState { 65 | var token by mutableStateOf("") 66 | var userData by mutableStateOf(UserDataModel()) 67 | } 68 | 69 | @Serializable 70 | class UserExData { 71 | val emojiProListFlow: MutableStateFlow> = 72 | MutableStateFlow(emptyList()) 73 | } 74 | 75 | @Serializable 76 | data class LoginParam( 77 | val accountMail: String, 78 | val passwd: String, 79 | ) 80 | 81 | @Serializable 82 | data class ChatRowModel( 83 | val fromChatId: String = "", 84 | val sendUserId: String = "", 85 | val sendUserRoleType: Int = 0, 86 | val sendUserGender: Int = 0, 87 | val sendMessageId: String = "", 88 | val sendUserAvatar: String = "", 89 | val sendMessage: String = "", 90 | val sendUserNickname: String = "", 91 | val sendDate: String = "", 92 | ) -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/constant/BaseConstants.kt: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | 6 | object BaseResText { 7 | var deleteImageProConfirm: String = "" 8 | var errorTooLargeUpload5000: String = "" 9 | var errorTooLargeUpload20: String = "" 10 | var userNoLogin: String = "" 11 | var underDevelopment: String = "" 12 | var confirmBtn: String = "" 13 | var cancelBtn: String = "" 14 | var copyTip: String = "" 15 | var bgColorList: List = listOf(Color.White, Color.White) 16 | var weekDayList: List = listOf("", "", "", "", "", "", "", "") 17 | } 18 | 19 | const val RECENT_EMOJI_MAX_SIZE = 8; 20 | const val BASE_SERVER_ADDRESS = "https://api.astercasc.com" 21 | const val BASE_SERVER_ADDRESS_STATIC = "https://api.astercasc.com/public/resources/" 22 | const val EMOJI_REPLACE_KEY = "EMOJI_PLACEHOLDER" 23 | const val NETWORK_CHECK_HOST = "https://www.baidu.com" 24 | const val MAX_TIME_SPE_SEC = 600 25 | //val ANN_TEXT_MAP = mapOf( 26 | // EMOJI_REPLACE_KEY to InlineTextContent( 27 | // Placeholder( 28 | // width = 25.sp, 29 | // height = 25.sp, 30 | // placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter 31 | // ) 32 | // ) { emoji -> 33 | // Image( 34 | // painter = painterResource( 35 | // biliEmojiMap[emoji] ?: Res.drawable.bili_00 36 | // ), 37 | // modifier = Modifier.fillMaxSize(), 38 | // contentDescription = null, 39 | // ) 40 | // } 41 | //) 42 | 43 | //私人编码: 44 | //Basic Multilingual Plane : U+E000 至 U+F8FF (57344 - 59647) 45 | //Supplementary Private Use Area-A : U+F0000 至 U+FFFFD 46 | //Supplementary Private Use Area-B: U+100000 (\uD800\uDC00) 至 U+10FFFD (\uDBFF\uDFFD) 47 | 48 | 49 | val emojiList = listOf( 50 | "🤭", "😄", "😁", "😆", "😅", "😂", "🤣", "😊", "😇", 51 | "🙂", "🙃", "😉", "😌", "😍", "🥰", "😘", "😗", "😙", "😚", 52 | "😋", "😛", "😜", "🤪", "😝", "🤑", "🤗", "🤭", "🤫", "🤔", 53 | "🤐", "🤨", "😐", "😑", "😶", "😏", "😒", "🙄", "😬", "😔", 54 | "😪", "😴", "😷", "🤒", "🤕", "🤢", "🤮", "🤧", "😵", "🤯", 55 | "😳", "🥵", "🥶", "😱", "😨", "😰", "😥", "😢", "😭", "😤", 56 | "😠", "😡", "🤬", "🤯", "😳", "🥱", "😤", 57 | ) 58 | 59 | 60 | val kaomojiList = listOf( 61 | "(^▽^)", "(≧▽≦)", "(´▽`)", "(๑˃ᴗ˂)ﻭ", "( ̄▽ ̄)ノ", 62 | "(*^▽^*)", "(^‿^)", "(^ω^)", "(o^∀^o)", "(≧ω≦)", 63 | "(o´▽`o)", "(@^-^)", "(≧∇≦)ノ", "(*´▽`*)", "(^-^)v", 64 | "(ヽ*^ω^*)ノ", "(๑´ڡ`๑)", "( •̀ ω •́ )✧", "(*^3^)/~☆", 65 | "(≧∇≦)/", "(。♥‿♥。)", "(。•ㅅ•。)♡", "(。・ω・。)ノ♡", "(ฅ•ω•ฅ)♡", 66 | "(๑•́ ₃ •̀๑)", "(´・ω・`)", "(。•́︿•̀。)", "(。♥‿♥。)", 67 | "(*≧ω≦)", "(∩_∩)", "( ̄3 ̄)", "(๑°3°๑)", "(,,• •,,)♡", 68 | "(。・//ε//・。)", "(づ ̄ ³ ̄)づ", "(´∀`)♡", "( ˘ ³˘)♥", 69 | "(๑´•.̫ • `๑)", "(づ。◕‿‿◕。)づ", "(´,,•ω•,,`)♡", "(〃ω〃)", 70 | "(⁄ ⁄•⁄ω⁄•⁄ ⁄)", "(。・//ω//・。)", "(*ノωノ)", "(´//ω//`)", 71 | "(*ฅ́˘ฅ̀*)", "(*/ω\*)", "(๑•́‧̫•̀๑)", "(´,,•ω•,,`)", 72 | "(//▽//)", "(。•́︿•̀。)", "(T_T)", "(ಥ_ಥ)", 73 | "(இ﹏இ`。)", "(〒︿〒)", "(╯︵╰,)", "(ಥ﹏ಥ)", "(。•́︿•̀。)", 74 | "(ノ_・。)", "(´;д;`)", "(;′⌒`)", "(つω`。)", "(ᗒᗩᗕ)", 75 | "(。•́︿•̀。)", "(╥﹏╥)", "(ಥ_ಥ)", "。:゚(。ノω\。)゚・。", "(つд⊂)", 76 | "(╥ω╥)", "(#`皿´)", "( ̄へ ̄)", "(≧ヘ≦ )", "(눈_눈)", "(¬_¬)", 77 | "(╬ಠ益ಠ)", "(。•ˇ‸ˇ•。)", "(`⌒´メ)", "(⇀‸↼‶)", "(¬‿¬)", 78 | "(°ロ°) !?", "(・o・)", "(๑•̀ㅁ•́๑)", "(⊙_◎)", "Σ(°ロ°)", 79 | "(°ー°〃)", "Σ( ̄□ ̄||)", "(⊙_⊙')", "(☉。☉)!", "(o_O)", 80 | "( ̄ー ̄)", "(¬_¬)", "(≖_≖ )", "( ´_ゝ`)", "(ಠ_ಠ)", "(; ̄Д ̄)", 81 | ) 82 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/components/CardComponents.kt: -------------------------------------------------------------------------------- 1 | package ui.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.gestures.detectHorizontalDragGestures 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.fillMaxHeight 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.offset 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.setValue 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.graphics.Color 23 | import androidx.compose.ui.input.pointer.pointerInput 24 | import androidx.compose.ui.platform.LocalDensity 25 | import androidx.compose.ui.unit.Dp 26 | import androidx.compose.ui.unit.IntOffset 27 | import kotlin.math.max 28 | import kotlin.math.min 29 | 30 | data class SwipeToRevealCardOption( 31 | val optionText: String, 32 | val optionOperation: () -> Unit, 33 | val width: Dp, 34 | val optColor: Color, 35 | val optBgColor: Color, 36 | ) 37 | 38 | 39 | @Composable 40 | fun SwipeToRevealCard( 41 | modifier: Modifier = Modifier, 42 | optionList: List = emptyList(), 43 | content: @Composable () -> Unit 44 | ) { 45 | 46 | var offsetX by remember { mutableStateOf(0f) } 47 | val maxOffsetDp = optionList.map { it.width }.reduce { acc, dp -> acc + dp } 48 | val maxOffset = with(LocalDensity.current) { maxOffsetDp.toPx() } 49 | 50 | Box( 51 | modifier = modifier 52 | ) { 53 | Row( 54 | modifier = Modifier.fillMaxWidth() 55 | .offset { IntOffset(offsetX.toInt() + maxOffset.toInt(), 0) }, 56 | horizontalArrangement = Arrangement.End, 57 | verticalAlignment = Alignment.CenterVertically, 58 | ) { 59 | 60 | for (opt in optionList) { 61 | Box( 62 | modifier = Modifier.fillMaxHeight() 63 | .width(opt.width).background(opt.optBgColor) 64 | .clickable { 65 | offsetX = 0f 66 | opt.optionOperation() 67 | }, contentAlignment = Alignment.Center 68 | ) { 69 | Text( 70 | modifier = Modifier, text = opt.optionText, color = opt.optColor 71 | ) 72 | } 73 | } 74 | 75 | } 76 | 77 | Box(modifier = Modifier.offset { IntOffset(offsetX.toInt(), 0) }.fillMaxSize() 78 | .pointerInput(Unit) { 79 | detectHorizontalDragGestures(onHorizontalDrag = { _, dragAmount -> 80 | val newOffset = offsetX + dragAmount 81 | offsetX = max(-maxOffset, min(0f, newOffset)) 82 | }, onDragEnd = { 83 | offsetX = if (offsetX < -maxOffset / 2) { 84 | (-maxOffset) 85 | } else { 86 | 0f 87 | } 88 | }) 89 | }) { 90 | content() 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/constant/ChineseDateData.kt: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | val LUNAR_CODE = longArrayOf( 4 | 0x04bd8, 5 | 0x04ae0, 6 | 0x0a570, 7 | 0x054d5, 8 | 0x0d260, 9 | 0x0d950, 10 | 0x16554, 11 | 0x056a0, 12 | 0x09ad0, 13 | 0x055d2, //1900-1909 14 | 0x04ae0, 15 | 0x0a5b6, 16 | 0x0a4d0, 17 | 0x0d250, 18 | 0x1d255, 19 | 0x0b540, 20 | 0x0d6a0, 21 | 0x0ada2, 22 | 0x095b0, 23 | 0x14977, //1910-1919 24 | 0x04970, 25 | 0x0a4b0, 26 | 0x0b4b5, 27 | 0x06a50, 28 | 0x06d40, 29 | 0x1ab54, 30 | 0x02b60, 31 | 0x09570, 32 | 0x052f2, 33 | 0x04970, //1920-1929 34 | 0x06566, 35 | 0x0d4a0, 36 | 0x0ea50, 37 | 0x16a95, 38 | 0x05ad0, 39 | 0x02b60, 40 | 0x186e3, 41 | 0x092e0, 42 | 0x1c8d7, 43 | 0x0c950, //1930-1939 44 | 0x0d4a0, 45 | 0x1d8a6, 46 | 0x0b550, 47 | 0x056a0, 48 | 0x1a5b4, 49 | 0x025d0, 50 | 0x092d0, 51 | 0x0d2b2, 52 | 0x0a950, 53 | 0x0b557, //1940-1949 54 | 0x06ca0, 55 | 0x0b550, 56 | 0x15355, 57 | 0x04da0, 58 | 0x0a5b0, 59 | 0x14573, 60 | 0x052b0, 61 | 0x0a9a8, 62 | 0x0e950, 63 | 0x06aa0, //1950-1959 64 | 0x0aea6, 65 | 0x0ab50, 66 | 0x04b60, 67 | 0x0aae4, 68 | 0x0a570, 69 | 0x05260, 70 | 0x0f263, 71 | 0x0d950, 72 | 0x05b57, 73 | 0x056a0, //1960-1969 74 | 0x096d0, 75 | 0x04dd5, 76 | 0x04ad0, 77 | 0x0a4d0, 78 | 0x0d4d4, 79 | 0x0d250, 80 | 0x0d558, 81 | 0x0b540, 82 | 0x0b6a0, 83 | 0x195a6, //1970-1979 84 | 0x095b0, 85 | 0x049b0, 86 | 0x0a974, 87 | 0x0a4b0, 88 | 0x0b27a, 89 | 0x06a50, 90 | 0x06d40, 91 | 0x0af46, 92 | 0x0ab60, 93 | 0x09570, //1980-1989 94 | 0x04af5, 95 | 0x04970, 96 | 0x064b0, 97 | 0x074a3, 98 | 0x0ea50, 99 | 0x06b58, 100 | 0x05ac0, 101 | 0x0ab60, 102 | 0x096d5, 103 | 0x092e0, //1990-1999 104 | 0x0c960, 105 | 0x0d954, 106 | 0x0d4a0, 107 | 0x0da50, 108 | 0x07552, 109 | 0x056a0, 110 | 0x0abb7, 111 | 0x025d0, 112 | 0x092d0, 113 | 0x0cab5, //2000-2009 114 | 0x0a950, 115 | 0x0b4a0, 116 | 0x0baa4, 117 | 0x0ad50, 118 | 0x055d9, 119 | 0x04ba0, 120 | 0x0a5b0, 121 | 0x15176, 122 | 0x052b0, 123 | 0x0a930, //2010-2019 124 | 0x07954, 125 | 0x06aa0, 126 | 0x0ad50, 127 | 0x05b52, 128 | 0x04b60, 129 | 0x0a6e6, 130 | 0x0a4e0, 131 | 0x0d260, 132 | 0x0ea65, 133 | 0x0d530, //2020-2029 134 | 0x05aa0, 135 | 0x076a3, 136 | 0x096d0, 137 | 0x04afb, 138 | 0x04ad0, 139 | 0x0a4d0, 140 | 0x1d0b6, 141 | 0x0d250, 142 | 0x0d520, 143 | 0x0dd45, //2030-2039 144 | 0x0b5a0, 145 | 0x056d0, 146 | 0x055b2, 147 | 0x049b0, 148 | 0x0a577, 149 | 0x0a4b0, 150 | 0x0aa50, 151 | 0x1b255, 152 | 0x06d20, 153 | 0x0ada0, //2040-2049 154 | 0x14b63, 155 | 0x09370, 156 | 0x049f8, 157 | 0x04970, 158 | 0x064b0, 159 | 0x168a6, 160 | 0x0ea50, 161 | 0x06b20, 162 | 0x1a6c4, 163 | 0x0aae0, //2050-2059 164 | 0x092e0, 165 | 0x0d2e3, 166 | 0x0c960, 167 | 0x0d557, 168 | 0x0d4a0, 169 | 0x0da50, 170 | 0x05d55, 171 | 0x056a0, 172 | 0x0a6d0, 173 | 0x055d4, //2060-2069 174 | 0x052d0, 175 | 0x0a9b8, 176 | 0x0a950, 177 | 0x0b4a0, 178 | 0x0b6a6, 179 | 0x0ad50, 180 | 0x055a0, 181 | 0x0aba4, 182 | 0x0a5b0, 183 | 0x052b0, //2070-2079 184 | 0x0b273, 185 | 0x06930, 186 | 0x07337, 187 | 0x06aa0, 188 | 0x0ad50, 189 | 0x14b55, 190 | 0x04b60, 191 | 0x0a570, 192 | 0x054e4, 193 | 0x0d160, //2080-2089 194 | 0x0e968, 195 | 0x0d520, 196 | 0x0daa0, 197 | 0x16aa6, 198 | 0x056d0, 199 | 0x04ae0, 200 | 0x0a9d4, 201 | 0x0a2d0, 202 | 0x0d150, 203 | 0x0f252, //2090-2099 204 | ) -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/ui/components/Notification.android.kt: -------------------------------------------------------------------------------- 1 | package ui.components 2 | 3 | 4 | import android.Manifest 5 | import android.annotation.SuppressLint 6 | import android.app.NotificationChannel 7 | import android.app.NotificationManager 8 | import android.app.PendingIntent 9 | import android.content.Context 10 | import android.content.Intent 11 | import android.os.Build 12 | import android.provider.Settings 13 | import androidx.annotation.RequiresApi 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.platform.LocalContext 16 | import androidx.core.app.NotificationCompat 17 | import com.aster.yuno.tomoyo.MainActivity 18 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 19 | import com.google.accompanist.permissions.isGranted 20 | import com.google.accompanist.permissions.rememberPermissionState 21 | import org.koin.java.KoinJavaComponent.inject 22 | 23 | @SuppressLint("PermissionLaunchedDuringComposition") 24 | @RequiresApi(Build.VERSION_CODES.TIRAMISU) 25 | @ExperimentalPermissionsApi 26 | @Composable 27 | actual fun CheckAppNotificationPermission( 28 | requestPermission: @Composable (() -> Unit) -> Unit 29 | ) { 30 | 31 | val context = LocalContext.current 32 | 33 | val notificationPermission = rememberPermissionState( 34 | permission = Manifest.permission.POST_NOTIFICATIONS 35 | ) 36 | 37 | if (!notificationPermission.status.isGranted) { 38 | 39 | requestPermission { 40 | //function 1 41 | //notificationPermission.launchPermissionRequest() 42 | 43 | //function 2 44 | val intent = Intent().apply { 45 | action = Settings.ACTION_APP_NOTIFICATION_SETTINGS 46 | putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) 47 | } 48 | 49 | //function 3 50 | // val intent = Intent().apply { 51 | // action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS 52 | // data = Uri.fromParts("package", context.packageName, null) 53 | // } 54 | context.startActivity(intent) 55 | } 56 | 57 | 58 | } 59 | 60 | //Toast.makeText(LocalContext.current, "Permission Check", Toast.LENGTH_SHORT).show() 61 | } 62 | 63 | fun createAppNotificationChannel(context: Context) { 64 | val channel = NotificationChannel( 65 | "TomoyoCommonChannel", 66 | "Tomoyo Common Notification", 67 | NotificationManager.IMPORTANCE_HIGH 68 | ).apply { 69 | this.description = description 70 | enableLights(true) 71 | enableVibration(true) 72 | setShowBadge(true) 73 | setBypassDnd(true) 74 | } 75 | 76 | val notificationManager = context.getSystemService(NotificationManager::class.java) 77 | notificationManager.createNotificationChannel(channel) 78 | } 79 | 80 | 81 | actual fun sendAppNotification(title: String, content: String) { 82 | val context: Context by inject(Context::class.java) 83 | 84 | val intent = Intent(context, MainActivity::class.java).apply { 85 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 86 | } 87 | val pendingIntent = PendingIntent.getActivity( 88 | context, 89 | 0, 90 | intent, 91 | PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 92 | ) 93 | 94 | 95 | val builder = NotificationCompat.Builder(context, "TomoyoCommonChannel") 96 | .setContentTitle(title) 97 | .setContentText(content) 98 | .setSmallIcon(android.R.drawable.ic_dialog_info) 99 | .setPriority(NotificationCompat.PRIORITY_HIGH) 100 | .setAutoCancel(true) 101 | .setContentIntent(pendingIntent) 102 | 103 | val notificationManager = context.getSystemService(NotificationManager::class.java) 104 | 105 | notificationManager.notify(0, builder.build()) 106 | } 107 | 108 | actual fun clearAppNotification() { 109 | val context: Context by inject(Context::class.java) 110 | val notificationManager = 111 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 112 | notificationManager.cancelAll() 113 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 24 | 30 | 36 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/data/model/MusicScreenModel.kt: -------------------------------------------------------------------------------- 1 | package data.model 2 | 3 | import api.BaseApi 4 | import api.baseJsonConf 5 | import biz.AudioPlayer 6 | import cafe.adriel.voyager.core.model.ScreenModel 7 | import constant.enums.MusicPlayModel 8 | import constant.enums.MusicPlayScreenTabModel 9 | import data.AudioSimpleModel 10 | import data.MusicPlayerState 11 | import data.store.DataStorageManager 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.flow.asStateFlow 14 | import kotlinx.serialization.encodeToString 15 | 16 | class MusicScreenModel( 17 | private val dataStorageManager: DataStorageManager 18 | ) : ScreenModel { 19 | 20 | //fav 21 | private val _favList = MutableStateFlow(setOf()) 22 | val favList = _favList.asStateFlow() 23 | fun addFav(id: String) { 24 | val newFav = mutableSetOf() 25 | newFav.addAll(_favList.value) 26 | newFav.add(id) 27 | _favList.value = newFav 28 | dataStorageManager.setString( 29 | DataStorageManager.FAV_AUDIO_ID_LIST, 30 | baseJsonConf.encodeToString(newFav) 31 | ) 32 | favUpdateToPlayer() 33 | } 34 | 35 | fun delFav(id: String) { 36 | val newFav = mutableSetOf() 37 | _favList.value.forEach { if (it != id) newFav.add(it) } 38 | _favList.value = newFav 39 | dataStorageManager.setString( 40 | DataStorageManager.FAV_AUDIO_ID_LIST, 41 | baseJsonConf.encodeToString(newFav) 42 | ) 43 | favUpdateToPlayer() 44 | } 45 | 46 | private fun favUpdateToPlayer() { 47 | if (_playerState.value.currentCollectionId == MusicPlayScreenTabModel.FAV.collectionId) { 48 | _player.value.clearSongs() 49 | _player.value.addSongList(_musicPlayMap.value 50 | .filter { _favList.value.contains(it.key) }) 51 | } 52 | } 53 | 54 | //tab 55 | private val _musicTab = MutableStateFlow(MusicPlayScreenTabModel.COMMON) 56 | val musicTab = _musicTab.asStateFlow() 57 | fun updateMusicTab(tab: MusicPlayScreenTabModel) { 58 | _musicTab.value = tab 59 | } 60 | 61 | //player 62 | private val _playerState = MutableStateFlow(MusicPlayerState()) 63 | val playerState = _playerState.asStateFlow() 64 | 65 | private val _player = MutableStateFlow(AudioPlayer(_playerState.value)) 66 | private val _musicPlayMap = MutableStateFlow>(emptyMap()) 67 | val musicPlayMap = _musicPlayMap.asStateFlow() 68 | suspend fun updateAllAudioList() { 69 | if (_musicPlayMap.value.isNotEmpty()) { 70 | return 71 | } 72 | _musicPlayMap.value = BaseApi().getAllAudio().associateBy { it.id } 73 | } 74 | 75 | fun getCurrentMusicData() = 76 | _musicPlayMap.value.getOrElse(_playerState.value.currentPlayId) { AudioSimpleModel() } 77 | 78 | fun nextPlayModel() { 79 | _playerState.value.playModel = _playerState.value.playModel 80 | .plus(1).rem(MusicPlayModel.entries.size) 81 | } 82 | 83 | fun onStart( 84 | playListId: String, 85 | playCollectionId: String, 86 | musicPlayMap: Map = _musicPlayMap.value 87 | ) { 88 | if (playListId == _playerState.value.currentPlayId) { 89 | return 90 | } 91 | //todo login check 92 | if (musicPlayMap.isNotEmpty() && 93 | _playerState.value.currentCollectionId != playCollectionId 94 | ) { 95 | _playerState.value.currentCollectionId = playCollectionId 96 | _player.value.clearSongs() 97 | _player.value.addSongList(musicPlayMap) 98 | } 99 | _player.value.start(playListId) 100 | } 101 | 102 | fun onPlay() { 103 | _player.value.play() 104 | } 105 | 106 | fun onPause() { 107 | _player.value.pause() 108 | } 109 | 110 | fun onSeek(time: Double) { 111 | _player.value.seekTo(time) 112 | } 113 | 114 | fun onNext() { 115 | _player.value.next() 116 | } 117 | 118 | fun onPrev() { 119 | _player.value.prev() 120 | } 121 | 122 | init { 123 | //fav 124 | val favList = dataStorageManager.getNonFlowString( 125 | DataStorageManager.FAV_AUDIO_ID_LIST 126 | ) 127 | if (favList.isNotBlank()) { 128 | _favList.value = baseJsonConf.decodeFromString(favList) 129 | } 130 | 131 | } 132 | 133 | 134 | } -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "c63c63846d9c539229e96de38d6af51417e28c0ee9a0bc48bd0f0f19d923c329", 3 | "pins" : [ 4 | { 5 | "identity" : "abseil-cpp-binary", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/google/abseil-cpp-binary.git", 8 | "state" : { 9 | "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", 10 | "version" : "1.2024072200.0" 11 | } 12 | }, 13 | { 14 | "identity" : "app-check", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/google/app-check.git", 17 | "state" : { 18 | "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", 19 | "version" : "11.2.0" 20 | } 21 | }, 22 | { 23 | "identity" : "firebase-ios-sdk", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/firebase/firebase-ios-sdk", 26 | "state" : { 27 | "revision" : "fdc352fabaf5916e7faa1f96ad02b1957e93e5a5", 28 | "version" : "11.15.0" 29 | } 30 | }, 31 | { 32 | "identity" : "google-ads-on-device-conversion-ios-sdk", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk", 35 | "state" : { 36 | "revision" : "428d8bb138e00f9a3f4f61cc6cd8863607524f65", 37 | "version" : "2.1.0" 38 | } 39 | }, 40 | { 41 | "identity" : "googleappmeasurement", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/google/GoogleAppMeasurement.git", 44 | "state" : { 45 | "revision" : "45ce435e9406d3c674dd249a042b932bee006f60", 46 | "version" : "11.15.0" 47 | } 48 | }, 49 | { 50 | "identity" : "googledatatransport", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/google/GoogleDataTransport.git", 53 | "state" : { 54 | "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", 55 | "version" : "10.1.0" 56 | } 57 | }, 58 | { 59 | "identity" : "googleutilities", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/google/GoogleUtilities.git", 62 | "state" : { 63 | "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", 64 | "version" : "8.1.0" 65 | } 66 | }, 67 | { 68 | "identity" : "grpc-binary", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/google/grpc-binary.git", 71 | "state" : { 72 | "revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71", 73 | "version" : "1.69.0" 74 | } 75 | }, 76 | { 77 | "identity" : "gtm-session-fetcher", 78 | "kind" : "remoteSourceControl", 79 | "location" : "https://github.com/google/gtm-session-fetcher.git", 80 | "state" : { 81 | "revision" : "c756a29784521063b6a1202907e2cc47f41b667c", 82 | "version" : "4.5.0" 83 | } 84 | }, 85 | { 86 | "identity" : "interop-ios-for-google-sdks", 87 | "kind" : "remoteSourceControl", 88 | "location" : "https://github.com/google/interop-ios-for-google-sdks.git", 89 | "state" : { 90 | "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", 91 | "version" : "101.0.0" 92 | } 93 | }, 94 | { 95 | "identity" : "leveldb", 96 | "kind" : "remoteSourceControl", 97 | "location" : "https://github.com/firebase/leveldb.git", 98 | "state" : { 99 | "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", 100 | "version" : "1.22.5" 101 | } 102 | }, 103 | { 104 | "identity" : "nanopb", 105 | "kind" : "remoteSourceControl", 106 | "location" : "https://github.com/firebase/nanopb.git", 107 | "state" : { 108 | "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", 109 | "version" : "2.30910.0" 110 | } 111 | }, 112 | { 113 | "identity" : "promises", 114 | "kind" : "remoteSourceControl", 115 | "location" : "https://github.com/google/promises.git", 116 | "state" : { 117 | "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", 118 | "version" : "2.4.0" 119 | } 120 | }, 121 | { 122 | "identity" : "swift-protobuf", 123 | "kind" : "remoteSourceControl", 124 | "location" : "https://github.com/apple/swift-protobuf.git", 125 | "state" : { 126 | "revision" : "102a647b573f60f73afdce5613a51d71349fe507", 127 | "version" : "1.30.0" 128 | } 129 | } 130 | ], 131 | "version" : 3 132 | } 133 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.LaunchedEffect 2 | import androidx.compose.runtime.getValue 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.runtime.setValue 6 | import androidx.compose.ui.graphics.toPainter 7 | import androidx.compose.ui.window.Window 8 | import androidx.compose.ui.window.WindowPlacement 9 | import androidx.compose.ui.window.WindowPosition 10 | import androidx.compose.ui.window.application 11 | import androidx.compose.ui.window.rememberWindowState 12 | import constant.enums.MainNavigationEnum 13 | import constant.enums.WindowsSizeEnum 14 | import data.PlatformInitData 15 | import di.KoinInit 16 | import javafx.embed.swing.JFXPanel 17 | import org.koin.core.Koin 18 | import org.koin.core.qualifier.named 19 | import ui.components.clearAppNotification 20 | import java.awt.SystemTray 21 | import java.awt.TrayIcon 22 | import java.awt.event.MouseEvent 23 | import java.awt.event.MouseListener 24 | import java.awt.event.WindowEvent 25 | import java.awt.event.WindowFocusListener 26 | import java.awt.image.BufferedImage 27 | 28 | 29 | lateinit var koin: Koin 30 | val tray: SystemTray = SystemTray.getSystemTray() 31 | 32 | fun main() { 33 | 34 | koin = KoinInit().init() 35 | koin.loadModules( 36 | listOf(), 37 | ) 38 | 39 | //init javafx 40 | JFXPanel() 41 | 42 | val superLowDpiIcon: BufferedImage = koin.get(named("superLowDpiIcon")) 43 | val trayIcon = TrayIcon(superLowDpiIcon, "Tomoyo") 44 | tray.add(trayIcon) 45 | 46 | application { 47 | 48 | var visible by remember { mutableStateOf(true) } 49 | var windowRef: java.awt.Frame? by remember { mutableStateOf(null) } 50 | val winState = rememberWindowState( 51 | placement = WindowPlacement.Floating, 52 | position = WindowPosition.PlatformDefault, 53 | size = WindowsSizeEnum.LOW.data, 54 | ) 55 | trayIcon.apply { 56 | isImageAutoSize = true 57 | addMouseListener( 58 | object : MouseListener { 59 | override fun mouseClicked(e: MouseEvent?) {} 60 | override fun mousePressed(e: MouseEvent?) {} 61 | override fun mouseEntered(e: MouseEvent?) {} 62 | override fun mouseExited(e: MouseEvent?) {} 63 | override fun mouseReleased(e: MouseEvent?) { 64 | val isRight = e?.isPopupTrigger ?: false 65 | if (isRight) { 66 | exitApplication() 67 | } else { 68 | windowRef?.let { win -> 69 | if (!visible) { 70 | visible = true 71 | } 72 | if (win.state == java.awt.Frame.ICONIFIED) { 73 | win.state = java.awt.Frame.NORMAL 74 | } 75 | win.toFront() 76 | win.requestFocus() 77 | win.isAlwaysOnTop = true 78 | win.isAlwaysOnTop = false 79 | clearAppNotification() 80 | } 81 | } 82 | } 83 | } 84 | ) 85 | } 86 | 87 | Window( 88 | onCloseRequest = { visible = false }, 89 | visible = visible, 90 | title = "Tomoyo", 91 | icon = superLowDpiIcon.toPainter(), 92 | state = winState, 93 | ) { 94 | window.iconImage = superLowDpiIcon 95 | 96 | LaunchedEffect(Unit) { 97 | windowRef = window 98 | 99 | windowRef?.addWindowFocusListener(object : WindowFocusListener { 100 | override fun windowGainedFocus(e: WindowEvent?) { 101 | clearAppNotification() 102 | } 103 | 104 | override fun windowLostFocus(e: WindowEvent?) { 105 | } 106 | }) 107 | 108 | } 109 | 110 | MainApp( 111 | platformData = PlatformInitData( 112 | extraNavigationList = listOf( 113 | MainNavigationEnum.ARTICLES, 114 | MainNavigationEnum.Contacts, 115 | MainNavigationEnum.MUSICS, 116 | MainNavigationEnum.SETTING, 117 | ) 118 | ) 119 | ) 120 | } 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # Tomoyo App 2 | 3 | [English](README.md) | 中文 4 | 5 | ## 介绍 6 | 7 | Tomoyo是一个Kotlin Multiplatform 应用程序 8 | 一方面它是一个对于[该网站](https://www.astercasc.com)的功能拷贝,让其中一些功能可以在移动端和桌面端实现。 9 | 另一方面它是一个Kotlin Multiplatform下导航,socket,视频,音频,存储等常用功能的使用用例 10 | 11 | ## 进行中 12 | 13 | > 请注意,此项目仍在开发中,某些功能可能无法按预期工作 14 | 15 | ## 平台支持 16 | 17 | | Android | IOS | Desktop/JVM | Web | 18 | |:-------:|:---:|:-----------:|:---:| 19 | | √ | √ | √ | × | 20 | 21 | ## 文档 22 | 23 | [构建跨平台的客户端界面](https://www.astercasc.com/article/detail?articleId=AT1734101922878869) 24 | 25 | [利用GoogleFirebase为安卓应用提供系统级消息通知](https://www.astercasc.com/article/detail?articleId=AT192764442002353766) 26 | 27 | [Kotlin Compose Multiplatform下音乐播放解决方案](https://www.astercasc.com/article/detail?articleId=AT182402577020566323) 28 | 29 | [Kotlin Compose Multiplatform下实现HTTP请求](https://www.astercasc.com/article/detail?articleId=AT182174036963389030) 30 | 31 | [Kotlin Compose Multiplatform下导航解决方案](https://www.astercasc.com/article/detail?articleId=AT182028575808345292) 32 | 33 | [Kotlin Compose Multiplatform下全局通知组件构建](https://www.astercasc.com/article/detail?articleId=AT183303835787436441) 34 | 35 | [Kotlin Compose Multiplatform下自定义字体](https://www.astercasc.com/article/detail?articleId=AT183482081114038681) 36 | 37 | [Kotlin Compose Multiplatform下全局通知组件构建(续篇)](https://www.astercasc.com/article/detail?articleId=AT183632859813146214) 38 | 39 | [Kotlin Compose Multiplatform下数据持久化解决方案](https://www.astercasc.com/article/detail?articleId=AT183842512953804800) 40 | 41 | [Kotlin Compose Multiplatform下Socket解决方案](https://www.astercasc.com/article/detail?articleId=AT184505260536629248) 42 | 43 | [Kotlin Compose Multiplatform下支持主题切换](https://www.astercasc.com/article/detail?articleId=AT199322086682527744) 44 | 45 | ## 截图 46 | 47 | ### 安卓 48 | 49 | 50 | 51 | 52 | 53 | ### 桌面 54 | 55 | 56 | 57 | 58 | 59 | ### 苹果 60 | 61 | 62 | 63 | 64 | 65 | ## 重要提示 66 | 67 | * 关于系统级消息推送: 68 | * 需要按照[该教程](https://firebase.google.com/docs/cloud-messaging) 69 | 处理,相关客户端代码仓库中已经给出。但出于安全考虑,并没有没有提供对应的`google-services.json` 70 | 文件,如果您有相似的功能需要开发借鉴,可以将其替换,并参考相关消息逻辑,但是无法和默认的服务端进行消息联动,进而收到在 71 | `Tomoyo`中其他用户或者群组中发来的消息 72 | * 目前客户端/服务端的策略是,只要有消息且该用户没有在web端或者桌面端登录,都走FCM推送该用户的移动端。如果对于使用人数较多的场景下,可以通过移动端将活跃状态同步到服务端,只有在客户端不活跃的情况下,才进行消息推送。同时也可以通过消息合并(即对于每个/组用户每隔N秒发送最后一条消息,避免短期消息爆炸)等方式减少FCM的使用 73 | * 目前作者有的三台移动设备测试完成,其他更多移动设备没有条件测试,如果消息推送无法收到,请检测首先是否允许访问外网,其次是否包含谷歌商店应用,这两者不是必须的,但是对于某些手机型号可能需要。如果无论如何也无法利用FCM发送消息,或者这种方式不能被业务接受,可以尝试使用移动设备所在的厂商提供的消息推送平台(鸿蒙推送、小米推送),或者其他第三方平台(JPush、Getui) 74 | * 关于系统级消息推送,不同手机厂商需求不同,一般需要打开通知,以及自启动,以及调整应用程序省电策略 75 | * 苹果的该功能没有开发,因没有开发者账号,但是处理逻辑和安卓一样 76 | 77 | ## 运行项目 78 | 79 | ### 安卓 80 | 81 | 在Android Studio打开直接运行即可 82 | 83 | ### 桌面 84 | 85 | 执行命令`./gradlew :composeApp:run` 86 | 87 | ### 苹果 88 | 89 | [示例](https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-create-first-app.html#run-your-application-on-ios) 90 | 91 | #### 提示 92 | 93 | * 如果遇到 94 | `nw_proxy_resolver_create_parsed_array [C5.1 proxy pac] Evaluation error: NSURLErrorDomain: -1004` 95 | 请关闭苹果手机代理或者模拟器所在电脑的代理 96 | 97 | ## 技术栈 98 | 99 | - [Kotlin Multiplatform](https://kotlinlang.org/lp/multiplatform/) 100 | - [Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/) 101 | - [Kotlin Coroutines](https://github.com/Kotlin/kotlinx.coroutines) 102 | - [Koin](https://insert-koin.io/) 103 | - [Sketch](https://github.com/panpf/sketch/) 104 | - [zoomimage](https://github.com/panpf/zoomimage) 105 | - [FileKit](https://github.com/vinceglb/FileKit) 106 | - [Ktor](https://ktor.io/) 107 | - [Krossbow](https://github.com/joffrey-bion/krossbow) 108 | - [Exoplayer](https://github.com/google/ExoPlayer) 109 | - [Voyager](https://github.com/adrielcafe/voyager) 110 | - [JavaFx](https://openjfx.io/) 111 | - [Multiplatform Setting](https://github.com/russhwolf/multiplatform-settings) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ui/components/Notification.kt: -------------------------------------------------------------------------------- 1 | package ui.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.animation.fadeIn 6 | import androidx.compose.animation.fadeOut 7 | import androidx.compose.animation.slideInHorizontally 8 | import androidx.compose.animation.slideOutHorizontally 9 | import androidx.compose.foundation.background 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.navigationBarsPadding 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.wrapContentWidth 14 | import androidx.compose.foundation.shape.RoundedCornerShape 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.LaunchedEffect 19 | import androidx.compose.runtime.collectAsState 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.unit.dp 23 | import constant.enums.NotificationType 24 | import kotlinx.coroutines.delay 25 | import kotlinx.coroutines.flow.MutableStateFlow 26 | import kotlinx.coroutines.flow.StateFlow 27 | 28 | expect fun sendAppNotification(title: String, content: String) 29 | 30 | expect fun clearAppNotification() 31 | 32 | @Composable 33 | expect fun CheckAppNotificationPermission(requestPermission: @Composable (() -> Unit) -> Unit) 34 | 35 | 36 | data class MainNotification( 37 | val message: String, 38 | val type: NotificationType, 39 | var isExpire: Boolean = false, 40 | ) 41 | 42 | data class MainDialogAlert( 43 | val message: String, 44 | val onDismissed: () -> Unit = {}, 45 | val cancelOperationText: String = "", 46 | val confirmOperationText: String = "", 47 | val confirmOperation: () -> Unit = {} 48 | ) 49 | 50 | object NotificationManager { 51 | 52 | private val _notifications = MutableStateFlow(null) 53 | val notifications: StateFlow = _notifications 54 | private val _dialogAlert = MutableStateFlow(null) 55 | val dialogAlert: StateFlow = _dialogAlert 56 | 57 | 58 | fun showNotification(notification: MainNotification) { 59 | _notifications.value = notification 60 | } 61 | 62 | fun clearNotification() { 63 | _notifications.value = _notifications.value?.copy(isExpire = true) 64 | } 65 | 66 | fun createDialogAlert(dialogAlert: MainDialogAlert) { 67 | _dialogAlert.value = dialogAlert 68 | } 69 | 70 | fun removeDialogAlert() { 71 | _dialogAlert.value = null 72 | } 73 | } 74 | 75 | 76 | @Composable 77 | fun NotificationComponent() { 78 | 79 | val notification by NotificationManager.notifications.collectAsState() 80 | 81 | AnimatedVisibility( 82 | visible = null != notification && false == notification?.isExpire, 83 | enter = fadeIn(animationSpec = tween(durationMillis = 1000)) + 84 | slideInHorizontally( 85 | initialOffsetX = { -it }, 86 | animationSpec = tween(durationMillis = 1000) 87 | ), 88 | exit = fadeOut(animationSpec = tween(durationMillis = 1500)) + 89 | slideOutHorizontally( 90 | targetOffsetX = { it }, 91 | animationSpec = tween(durationMillis = 1500) 92 | ), 93 | ) { 94 | Box( 95 | modifier = Modifier 96 | .navigationBarsPadding() 97 | .wrapContentWidth() 98 | .padding(bottom = 20.dp) 99 | .background( 100 | color = MaterialTheme.colorScheme.surfaceVariant, 101 | shape = RoundedCornerShape(8.dp) 102 | ) 103 | .padding(8.dp) 104 | ) { 105 | Text( 106 | text = notification?.message ?: "", 107 | style = MaterialTheme.typography.bodySmall, 108 | color = MaterialTheme.colorScheme.onSurfaceVariant 109 | ) 110 | } 111 | 112 | LaunchedEffect(notification) { 113 | delay(3000) 114 | NotificationManager.clearNotification() 115 | } 116 | } 117 | 118 | 119 | val dialogAlert by NotificationManager.dialogAlert.collectAsState() 120 | if (null != dialogAlert) { 121 | MainAlertDialog( 122 | message = dialogAlert!!.message, 123 | onDismissed = { 124 | dialogAlert!!.onDismissed 125 | NotificationManager.removeDialogAlert() 126 | }, 127 | cancelOperationText = dialogAlert!!.cancelOperationText, 128 | confirmOperationText = dialogAlert!!.confirmOperationText, 129 | confirmOperation = dialogAlert!!.confirmOperation 130 | ) 131 | } 132 | 133 | 134 | } -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.5.2" 3 | android-compileSdk = "35" 4 | android-minSdk = "24" 5 | android-targetSdk = "35" 6 | androidx-activityCompose = "1.9.3" 7 | androidx-appcompat = "1.7.0" 8 | androidx-constraintlayout = "2.2.0" 9 | androidx-core-ktx = "1.15.0" 10 | androidx-espresso-core = "3.6.1" 11 | androidx-lifecycle = "2.8.4" 12 | androidx-material = "1.12.0" 13 | androidx-test-junit = "1.2.1" 14 | compose-plugin = "1.7.0" 15 | junit = "4.13.2" 16 | kotlin = "2.2.21" 17 | coroutines = "1.10.1" 18 | 19 | 20 | fastjson2 = "2.0.51" 21 | ktor = "3.1.1" 22 | kotlinxSerialization = "1.6.3" 23 | kotlinxDatetime = "0.6.0" 24 | koin = "3.5.6" 25 | koinCompose = "1.1.5" 26 | navigationCompose = "2.7.0-alpha07" 27 | javaFx = "0.1.0" 28 | javaFxLib = "22.0.1" 29 | voyager = "1.1.0-beta02" 30 | 31 | [libraries] 32 | javafx-base = { module = "org.openjfx:javafx-base", version.ref = "javaFxLib" } 33 | javafx-graphics = { module = "org.openjfx:javafx-graphics", version.ref = "javaFxLib" } 34 | javafx-controls = { module = "org.openjfx:javafx-controls", version.ref = "javaFxLib" } 35 | javafx-media = { module = "org.openjfx:javafx-media", version.ref = "javaFxLib" } 36 | 37 | kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } 38 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 39 | kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } 40 | junit = { group = "junit", name = "junit", version.ref = "junit" } 41 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } 42 | androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" } 43 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" } 44 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } 45 | androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } 46 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } 47 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 48 | 49 | 50 | ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } 51 | navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" } 52 | kotlinx-serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "kotlinxSerialization" } 53 | kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" } 54 | 55 | ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } 56 | ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } 57 | ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } 58 | 59 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } 60 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } 61 | kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } 62 | 63 | koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } 64 | koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinCompose" } 65 | koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } 66 | 67 | androidx-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } 68 | androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } 69 | 70 | fastjson2 = { module = "com.alibaba.fastjson2:fastjson2", version.ref = "fastjson2" } 71 | 72 | 73 | # Voyager 74 | voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } 75 | voyager-tabNavigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" } 76 | voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" } 77 | voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" } 78 | 79 | [plugins] 80 | androidApplication = { id = "com.android.application", version.ref = "agp" } 81 | androidLibrary = { id = "com.android.library", version.ref = "agp" } 82 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } 83 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 84 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 85 | javaFx = { id = "org.openjfx.javafxplugin", version.ref = "javaFx" } -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/logo_pro_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/biz/AudioPlayer.desktop.kt: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import constant.enums.MusicPlayModel 4 | import data.AudioSimpleModel 5 | import data.MusicPlayerState 6 | import javafx.scene.media.Media 7 | import javafx.scene.media.MediaPlayer 8 | import javafx.util.Duration 9 | import java.net.URL 10 | import kotlin.random.Random 11 | 12 | actual class AudioPlayer actual constructor(private val musicPlayerState: MusicPlayerState) { 13 | 14 | private var media: Media? = null 15 | 16 | private var mediaPlayer: MediaPlayer? = null 17 | 18 | private var currentItemIndex = -1 19 | 20 | private val maxPlaySize: Int = 50; 21 | 22 | private val playedItems = mutableListOf() 23 | 24 | private val mediaItems = mutableMapOf() 25 | 26 | actual fun start(id: String) { 27 | //check 28 | if (!mediaItems.containsKey(id)) return 29 | currentItemIndex = mediaItems.keys.indexOf(id) 30 | playWithIndex(currentItemIndex) 31 | } 32 | 33 | actual fun play() { 34 | if (musicPlayerState.isPlaying) return 35 | try { 36 | musicPlayerState.isPlaying = true 37 | mediaPlayer?.play() 38 | } catch (ex: Exception) { 39 | ex.printStackTrace() 40 | } 41 | } 42 | 43 | actual fun pause() { 44 | if (!musicPlayerState.isPlaying) return 45 | try { 46 | musicPlayerState.isPlaying = false 47 | mediaPlayer?.pause() 48 | } catch (ex: Exception) { 49 | ex.printStackTrace() 50 | } 51 | } 52 | 53 | actual fun next() { 54 | when (musicPlayerState.playModel) { 55 | MusicPlayModel.ORDER.ordinal -> { 56 | if (mediaItems.isNotEmpty()) { 57 | currentItemIndex = currentItemIndex.plus(1).rem(mediaItems.size) 58 | } 59 | } 60 | 61 | MusicPlayModel.RANDOM.ordinal -> { 62 | if (mediaItems.isNotEmpty()) { 63 | currentItemIndex = Random.nextInt(mediaItems.size) 64 | } 65 | } 66 | 67 | MusicPlayModel.CIRCULATION.ordinal -> { 68 | 69 | } 70 | else -> {} 71 | } 72 | playWithIndex(currentItemIndex) 73 | } 74 | 75 | actual fun prev() { 76 | if (playedItems.isEmpty()) return 77 | val lastItem = playedItems.removeLast() 78 | currentItemIndex = mediaItems.keys.indexOf(lastItem.id) 79 | playWithIndex(currentItemIndex, false) 80 | } 81 | 82 | actual fun seekTo(time: Double) { 83 | musicPlayerState.currentTime = time 84 | mediaPlayer?.seek(Duration.seconds(time)) 85 | } 86 | 87 | actual fun addSongList(songs: Map) { 88 | mediaItems += songs 89 | } 90 | 91 | actual fun clearSongs() { 92 | mediaItems.clear() 93 | } 94 | 95 | actual fun cleanUp() { 96 | musicPlayerState.toBack() 97 | mediaPlayer?.stop() 98 | } 99 | 100 | private fun playWithIndex(index: Int, maintainLast: Boolean = true) { 101 | if (index >= mediaItems.size || index < 0) return 102 | //maintain played map 103 | if (maintainLast) { 104 | val lastItem = mediaItems[musicPlayerState.currentPlayId] 105 | if (null != lastItem) { 106 | playedItems.add(lastItem) 107 | if (playedItems.size > maxPlaySize) { 108 | playedItems.removeFirstOrNull() 109 | } 110 | } 111 | } 112 | //convert 113 | val currentItem = mediaItems.entries.toList()[index] 114 | musicPlayerState.currentPlayId = currentItem.key 115 | val playUrl = currentItem.value.audioUrl 116 | //close 117 | musicPlayerState.toBack() 118 | mediaPlayer?.stop() 119 | //start 120 | val thisUrl = URL(playUrl) 121 | media = Media(thisUrl.toString()) 122 | mediaPlayer = MediaPlayer(media) 123 | mediaPlayer?.statusProperty()?.addListener { _, oldStatus, newStatus -> 124 | if (newStatus === MediaPlayer.Status.READY 125 | && mediaPlayer?.totalDuration?.toSeconds()?.isNaN() != true 126 | ) { 127 | musicPlayerState.totalDuration = 128 | mediaPlayer?.totalDuration?.toSeconds() ?: Short.MAX_VALUE.toDouble() 129 | play() 130 | } 131 | if (newStatus === MediaPlayer.Status.STALLED) { 132 | musicPlayerState.isBuffering = true 133 | } 134 | if (oldStatus === MediaPlayer.Status.STALLED 135 | && newStatus !== MediaPlayer.Status.STALLED 136 | ) { 137 | musicPlayerState.isBuffering = false 138 | } 139 | } 140 | 141 | mediaPlayer?.currentTimeProperty()?.addListener { _, _, newTime -> 142 | if (newTime.toSeconds() > musicPlayerState.currentTime + 1) { 143 | musicPlayerState.currentTime = newTime.toSeconds() 144 | } 145 | } 146 | 147 | mediaPlayer?.onEndOfMedia = Runnable { 148 | next() 149 | } 150 | } 151 | 152 | 153 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/biz/CustomTransition.kt: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import androidx.compose.animation.core.FiniteAnimationSpec 4 | import androidx.compose.animation.core.LinearEasing 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.animation.fadeIn 7 | import androidx.compose.animation.fadeOut 8 | import androidx.compose.animation.slideInHorizontally 9 | import androidx.compose.animation.slideOutHorizontally 10 | import androidx.compose.animation.togetherWith 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.IntOffset 14 | import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi 15 | import cafe.adriel.voyager.core.stack.StackEvent 16 | import cafe.adriel.voyager.navigator.Navigator 17 | import cafe.adriel.voyager.transitions.ScreenTransition 18 | import cafe.adriel.voyager.transitions.ScreenTransitionContent 19 | import constant.enums.MainNavigationEnum 20 | import constant.enums.ViewEnum 21 | 22 | @Composable 23 | fun BaseViewTransition( 24 | secLastScreenKey: String, 25 | navigator: Navigator, 26 | modifier: Modifier = Modifier, 27 | animationSpec: FiniteAnimationSpec = tween( 28 | durationMillis = 200, 29 | easing = LinearEasing, 30 | ), 31 | content: ScreenTransitionContent = { it.Content() } 32 | ) { 33 | BaseViewTransition( 34 | secLastScreenKey = secLastScreenKey, 35 | navigator = navigator, 36 | modifier = modifier, 37 | animationSpec = animationSpec, 38 | disposeScreenAfterTransitionEnd = false, 39 | content = content 40 | ) 41 | 42 | } 43 | 44 | @OptIn(ExperimentalVoyagerApi::class) 45 | @Composable 46 | private fun BaseViewTransition( 47 | secLastScreenKey: String, 48 | navigator: Navigator, 49 | modifier: Modifier = Modifier, 50 | animationSpec: FiniteAnimationSpec = tween( 51 | durationMillis = 200, 52 | easing = LinearEasing, 53 | ), 54 | disposeScreenAfterTransitionEnd: Boolean = false, 55 | content: ScreenTransitionContent = { it.Content() } 56 | ) { 57 | ScreenTransition( 58 | navigator = navigator, 59 | transition = { 60 | 61 | if (navigator.lastItem.key.startsWith(ViewEnum.TAB_PARENT.code) 62 | && secLastScreenKey.startsWith(ViewEnum.PRE_LOAD.code) 63 | ) { 64 | fadeIn( 65 | animationSpec = tween(300) 66 | ) togetherWith fadeOut( 67 | animationSpec = tween(300) 68 | ) 69 | } else { 70 | val (initialOffset, targetOffset) = when (navigator.lastEvent) { 71 | StackEvent.Pop -> ({ size: Int -> -size }) to ({ size: Int -> size }) 72 | else -> ({ size: Int -> size }) to ({ size: Int -> -size }) 73 | } 74 | slideInHorizontally(animationSpec, initialOffset) togetherWith 75 | slideOutHorizontally(animationSpec, targetOffset) 76 | } 77 | }, 78 | modifier = modifier, 79 | disposeScreenAfterTransitionEnd = disposeScreenAfterTransitionEnd, 80 | content = content, 81 | ) 82 | } 83 | 84 | @Composable 85 | fun TabTransition( 86 | secLastScreenKey: String, 87 | navigator: Navigator, 88 | modifier: Modifier = Modifier, 89 | animationSpec: FiniteAnimationSpec = tween( 90 | durationMillis = 200, 91 | easing = LinearEasing, 92 | ), 93 | content: ScreenTransitionContent = { it.Content() } 94 | ) { 95 | SlideTransition( 96 | secLastScreenKey = secLastScreenKey, 97 | navigator = navigator, 98 | modifier = modifier, 99 | animationSpec = animationSpec, 100 | disposeScreenAfterTransitionEnd = false, 101 | content = content 102 | ) 103 | } 104 | 105 | 106 | @OptIn(ExperimentalVoyagerApi::class) 107 | @Composable 108 | private fun SlideTransition( 109 | secLastScreenKey: String, 110 | navigator: Navigator, 111 | modifier: Modifier = Modifier, 112 | animationSpec: FiniteAnimationSpec = tween( 113 | durationMillis = 200, 114 | easing = LinearEasing, 115 | ), 116 | disposeScreenAfterTransitionEnd: Boolean = false, 117 | content: ScreenTransitionContent = { it.Content() } 118 | ) { 119 | ScreenTransition( 120 | navigator = navigator, 121 | transition = { 122 | 123 | if (navigator.lastItem.key.startsWith(ViewEnum.TAB_MAIN_HOME.code) || 124 | secLastScreenKey.startsWith(ViewEnum.TAB_MAIN_HOME.code) 125 | ) { 126 | fadeIn( 127 | animationSpec = tween(300) 128 | ) togetherWith fadeOut( 129 | animationSpec = tween(300) 130 | ) 131 | } else { 132 | val lastTabEnum = MainNavigationEnum.getEnumByCode(navigator.lastItem.key) 133 | val lastSecTabEnum = MainNavigationEnum.getEnumByCode(secLastScreenKey) 134 | 135 | 136 | val isToRight = lastTabEnum > lastSecTabEnum 137 | 138 | val (initialOffset, targetOffset) = if (isToRight) 139 | ({ size: Int -> size }) to ({ size: Int -> -size }) 140 | else ({ size: Int -> -size }) to ({ size: Int -> size }) 141 | 142 | 143 | slideInHorizontally(animationSpec, initialOffset) togetherWith 144 | slideOutHorizontally(animationSpec, targetOffset) 145 | } 146 | }, 147 | modifier = modifier, 148 | disposeScreenAfterTransitionEnd = disposeScreenAfterTransitionEnd, 149 | content = content, 150 | ) 151 | } 152 | 153 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/biz/MainUtils.kt: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import constant.BaseResText 4 | import constant.LUNAR_CODE 5 | import constant.enums.UserChineseZodiac 6 | import constant.enums.UserZodiac 7 | import dev.whyoleg.cryptography.BinarySize.Companion.bits 8 | import dev.whyoleg.cryptography.CryptographyProvider 9 | import dev.whyoleg.cryptography.algorithms.PBKDF2 10 | import dev.whyoleg.cryptography.algorithms.SHA256 11 | import kotlinx.datetime.Clock 12 | import kotlinx.datetime.DateTimeUnit 13 | import kotlinx.datetime.LocalDate 14 | import kotlinx.datetime.LocalDateTime 15 | import kotlinx.datetime.TimeZone 16 | import kotlinx.datetime.format 17 | import kotlinx.datetime.format.FormatStringsInDatetimeFormats 18 | import kotlinx.datetime.format.byUnicodePattern 19 | import kotlinx.datetime.isoDayNumber 20 | import kotlinx.datetime.plus 21 | import kotlinx.datetime.toInstant 22 | import kotlinx.datetime.toLocalDateTime 23 | import kotlinx.io.bytestring.encodeToByteString 24 | import kotlin.io.encoding.Base64 25 | 26 | 27 | expect fun getPlatform(): String 28 | 29 | expect fun copyToClipboard(text: String) 30 | 31 | expect fun logInfo(text: String) 32 | 33 | expect fun isAppInForeground(): Boolean 34 | 35 | expect fun afterLogin() 36 | 37 | expect fun beforeLogout() 38 | 39 | expect fun setUpdateGoogleFirebaseToken(operation: (String) -> Unit) 40 | 41 | fun formatSeconds(seconds: Int): String { 42 | val hours = seconds / 3600 43 | val minutes = (seconds % 3600) / 60 44 | val remainingSeconds = seconds % 60 45 | 46 | return if (0 == hours) "${minutes.toString().padStart(2, '0')}:" + 47 | remainingSeconds.toString().padStart(2, '0') 48 | else "${hours.toString().padStart(2, '0')}:" + 49 | "${minutes.toString().padStart(2, '0')}:" + 50 | remainingSeconds.toString().padStart(2, '0') 51 | } 52 | 53 | fun getChineseZodiac(date: LocalDate): UserChineseZodiac { 54 | val chineseYear = getChineseYear(date) 55 | return UserChineseZodiac.entries[(chineseYear - 1900) % UserChineseZodiac.entries.size] 56 | } 57 | 58 | fun generalOneWayEncryptStr(passwd: String, salt: String): String { 59 | val provider = CryptographyProvider.Default 60 | val pbkdf2 = provider.get(PBKDF2) 61 | val hasher = pbkdf2.secretDerivation( 62 | SHA256, 110_000, 256.bits, salt.encodeToByteString() 63 | ) 64 | val bytes = hasher.deriveSecretBlocking(passwd.encodeToByteString()) 65 | val b64 = Base64.encode(bytes.toByteArray()) 66 | return b64 67 | } 68 | 69 | 70 | fun getChineseYear(localDate: LocalDate): Int { 71 | // 求出和1900年1月31日相差的天数 72 | var offset: Int = localDate.toEpochDays() - 73 | LocalDate(1900, 1, 31).toEpochDays() 74 | // 计算农历年份 75 | // 用offset减去每农历年的天数,计算当天是农历第几天,offset是当年的第几天 76 | var daysOfYear: Int 77 | var iYear = 1900 78 | while (iYear <= 2099) { 79 | daysOfYear = yearDays(iYear) 80 | if (offset < daysOfYear) { 81 | break 82 | } 83 | offset -= daysOfYear 84 | iYear++ 85 | } 86 | return iYear 87 | } 88 | 89 | private fun yearDays(y: Int): Int { 90 | var i: Int 91 | var sum = 348 92 | i = 0x8000 93 | while (i > 0x8) { 94 | if ((getCode(y) and i.toLong()) != 0L) { 95 | sum += 1 96 | } 97 | i = i shr 1 98 | } 99 | return (sum + leapDays(y)) 100 | } 101 | 102 | private fun leapDays(y: Int): Int { 103 | if (leapMonth(y) != 0) { 104 | return if ((getCode(y) and 0x10000L) != 0L) 30 else 29 105 | } 106 | return 0 107 | } 108 | 109 | private fun leapMonth(y: Int): Int { 110 | return (getCode(y) and 0xfL).toInt() 111 | } 112 | 113 | private fun getCode(year: Int): Long { 114 | return LUNAR_CODE[year - 1900] 115 | } 116 | 117 | 118 | fun getZodiac(month: Int, day: Int): UserZodiac { 119 | val actualMonth = month - 1 120 | return if (day < UserZodiac.entries[actualMonth].sepDay) UserZodiac.entries[actualMonth] 121 | else UserZodiac.entries[actualMonth + 1] 122 | } 123 | 124 | @OptIn(FormatStringsInDatetimeFormats::class) 125 | fun getLastTime(time: String?): String { 126 | if (time.isNullOrBlank()) return "" 127 | val dateTime = LocalDateTime.Format { 128 | byUnicodePattern("yyyy-MM-dd HH:mm:ss") 129 | }.parse(time) 130 | 131 | val nowCo = Clock.System.now() 132 | val systemTZ = TimeZone.currentSystemDefault() 133 | val now = nowCo.toLocalDateTime(systemTZ) 134 | if (now.year != dateTime.year) { 135 | return dateTime.format(LocalDateTime.Format 136 | { byUnicodePattern("yyyy/MM/dd") }) 137 | } 138 | if (nowCo.epochSeconds > 139 | dateTime.toInstant(systemTZ) 140 | .plus(3, DateTimeUnit.DAY, systemTZ).epochSeconds 141 | ) { 142 | return dateTime.format(LocalDateTime.Format 143 | { byUnicodePattern("MM/dd") }) 144 | } 145 | if (now.dayOfWeek != dateTime.dayOfWeek) { 146 | return BaseResText.weekDayList[dateTime.dayOfWeek.isoDayNumber] 147 | } 148 | return dateTime.format(LocalDateTime.Format { byUnicodePattern("HH:mm") }) 149 | } 150 | 151 | @OptIn(FormatStringsInDatetimeFormats::class) 152 | fun getLastTimeInChatting(time: String?): String { 153 | if (time.isNullOrBlank()) return "" 154 | val dateTime = LocalDateTime.Format { 155 | byUnicodePattern("yyyy-MM-dd HH:mm:ss") 156 | }.parse(time) 157 | val nowCo = Clock.System.now() 158 | val systemTZ = TimeZone.currentSystemDefault() 159 | val now = nowCo.toLocalDateTime(systemTZ) 160 | 161 | if (now.year != dateTime.year) { 162 | return dateTime.format(LocalDateTime.Format 163 | { byUnicodePattern("yyyy-MM-dd HH:mm") }) 164 | } 165 | if (nowCo.epochSeconds > 166 | dateTime.toInstant(systemTZ) 167 | .plus(3, DateTimeUnit.DAY, systemTZ).epochSeconds 168 | ) { 169 | return dateTime.format(LocalDateTime.Format 170 | { byUnicodePattern("MM-dd HH:mm") }) 171 | } 172 | if (now.dayOfWeek != dateTime.dayOfWeek) { 173 | return BaseResText.weekDayList[dateTime.dayOfWeek.isoDayNumber] + 174 | dateTime.format(LocalDateTime.Format 175 | { byUnicodePattern(" HH:mm") }) 176 | } 177 | return dateTime.format(LocalDateTime.Format { byUnicodePattern("HH:mm") }) 178 | } --------------------------------------------------------------------------------