diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7987daa --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +/release/ diff --git a/BaseMvvm/.gitignore b/BaseMvvm/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/BaseMvvm/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/BaseMvvm/build.gradle.kts b/BaseMvvm/build.gradle.kts new file mode 100644 index 0000000..8ef0697 --- /dev/null +++ b/BaseMvvm/build.gradle.kts @@ -0,0 +1,72 @@ +plugins { + id("com.android.library") + kotlin("android") + kotlin("kapt") +} + +android { + compileSdk = ModuleConfig.compileSdkVersion + + defaultConfig { + minSdk = ModuleConfig.minSdkVersion + targetSdk = ModuleConfig.targetSdkVersion + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + buildFeatures { + viewBinding = true + dataBinding = true + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + // Some libs (such as androidx.core:core-ktx 1.2.0 and newer) require Java 8 + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + // To avoid the compile error: "Cannot inline bytecode built with JVM target 1.8 + // into bytecode that is being built with JVM target 1.6" + kotlinOptions { + val options = this as org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions + options.jvmTarget = "1.8" + } +} + +dependencies { + // Android + implementation(Android.appcompat) + implementation(Android.startup) + implementation(Android.coreKtx) + implementation(Android.constraintLayout) + implementation(Android.material) + + // Navigation + implementation(Navigation.uiKtx) + implementation(Navigation.fragmentKtx) + + // Architecture Components + implementation(Lifecycle.runtimeKtx) + + // ImmersionBar + api(Depends.immersionBar) + api(Depends.immersionBarKtx) + + // Retrofit + implementation(Retrofit.retrofit) + + // Gson + implementation(Depends.gson) + + implementation(Depends.unPeekLiveData) + + implementation(MaterialDialogs.core) + implementation(Depends.lottie) + api(Depends.shimmer) +} \ No newline at end of file diff --git a/BaseMvvm/consumer-rules.pro b/BaseMvvm/consumer-rules.pro new file mode 100644 index 0000000..9c5bda3 --- /dev/null +++ b/BaseMvvm/consumer-rules.pro @@ -0,0 +1,25 @@ +# Android jetpack去混淆 +-keep class com.google.android.material.** {*;} +-keep class androidx.** {*;} +-keep public class * extends androidx.** +-keep interface androidx.** {*;} +-dontwarn com.google.android.material.** +-dontnote com.google.android.material.** +-dontwarn androidx.** + + +# Glide 混淆配置 +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep class * extends com.bumptech.glide.module.AppGlideModule { + (...); +} +-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { + **[] $VALUES; + public *; +} +-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder { + *** rewind(); +} + +# for DexGuard only +-keepresourcexmlelements manifest/application/meta-data@value=GlideModule \ No newline at end of file diff --git a/BaseMvvm/proguard-rules.pro b/BaseMvvm/proguard-rules.pro new file mode 100644 index 0000000..ff59496 --- /dev/null +++ b/BaseMvvm/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/BaseMvvm/src/main/AndroidManifest.xml b/BaseMvvm/src/main/AndroidManifest.xml new file mode 100644 index 0000000..21834b0 --- /dev/null +++ b/BaseMvvm/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/BaseMvvm/src/main/assets/req_loading.json b/BaseMvvm/src/main/assets/req_loading.json new file mode 100644 index 0000000..e0dff7c --- /dev/null +++ b/BaseMvvm/src/main/assets/req_loading.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":23.9759979248047,"ip":0,"op":119.999989613637,"w":400,"h":400,"nm":"boyanimation","ddd":0,"assets":[{"id":"image_0","w":469,"h":284,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAdUAAAEcCAYAAACRXi6LAAAgAElEQVR4Xu2de5RdVX3H9z733pmbeeY1M3kaQiRgIgRDgBoFDKQEIgi2mGJFKVbtQ8uqtbYuHwVb+1q0yLK1q7FUirXWFWkrRSLYlMijUXDkJSG8QoA8JskkM5nM+3HO7rohQBJm5p7Hb7/O75t/s89v/87n99vn+z37nHtGCvwDARAAARAAARAgISBJoiAICIAACIAACICAgKiiCUAABEAABECAiABElQgkwoAACIAACIAARBU9AAIgAAIgAAJEBCCqRCARBgRAAARAAAQgqugBEAABEAABECAiAFElAokwIAACIAACIABRRQ+AAAiAAAiAABEBiCoRSIQBARAAARAAAYgqegAEQAAEQAAEiAhAVIlAIgwIgAAIgAAIQFTRAyAAAiAAAiBARACiSgQSYUAABEAABEAAoooeAAEQAAEQAAEiAhBVIpAIAwIgAAIgAAIQVfQACIAACIAACBARgKgSgUQYEAABEAABEICoogdAAARAAARAgIgARJUIJMKAAAiAAAiAAEQVPQACIAACIAACRAQgqkQgEQYEQAAEQAAEIKroARAAARAAARAgIgBRJQKJMCAAAiAAAiAAUUUPgAAIgAAIgAARAYgqEUiEAQEQAAEQAAGIKnoABEAABEAABIgIQFSJQCIMCIAACIAACEBU0QMgAAIgAAIgQEQAokoEEmFAAARAAARAAKKKHgABEAABEAABIgIQVSKQCAMCIAACIAACEFX0AAiAAAiAAAgQEYCoEoFEGBAAARAAARCAqKIHQAAEQAAEQICIAESVCCTCgAAIgAAIgABEFT0AAiAAAiAAAkQEIKpEIBEGBEAABEAABCCq6AEQAAEQAAEQICIAUSUCiTAgAAIgAAIgAFFFD4AACIAACIAAEQGIKhFIhAEBEAABEAABiCp6AARAAARAAASICEBUiUAiDAiAAAiAAAhAVNEDIAACIAACIEBEAKJKBBJhQAAEQAAEQACiih4AARAAARAAASICEFUikAgDAiAAAiAAAhBV9AAIgAAIgAAIEBGAqBKBRBgQAAEQAAEQgKiiB0AABEAABECAiABElQgkwoAACIAACIAARBU9AAIgAAIgAAJEBCCqRCARBgRAAARAAAQgqugBEAABEAABECAiAFElAokwIAACIAAC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","e":1},{"id":"image_1","w":252,"h":253,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPwAAAD9CAYAAACY9xrCAAAgAElEQVR4Xu2dS6g9y3fX6xrFBMFoFEycBIwg4sDXQONARQcaJzFkpAOfMzNQ0IkTceTAgYIoOBARwUci6MAkSpKBIhJFEXw/8JGoSRRFETSJJnqlzn+vc7+/7/muR1U/du+968Llt3d3dXV3VX3W97tW9znns7b+WyOwRuBlRuCzl7nTdaNrBNYItAX8WgRrBF5oBBbwLzTZ61bXCCzg1xpYI/BCI7CAf6HJXre6RmABv9bAGoEXGoEF/AtN9rrVNQIL+LUG1gi80Ags4F9ostetrhFYwK81sEbghUZgAf9Ck71udY3AAn6tgTUCLzQCC/gXmux1q2sEFvBrDawReKERWMC/0GSvW10jsIB/gjXw+eef7zaPn3322edPMCTrFpwR2G2hrBHedwT2hHjfK/uitxUcjhrZ4/pdwB83tsM9PwLk0U2tADA85acfsIA/ecgfHeqZ4VqBYGbUjjlmAX/MuH7o9RVB50FY4J+02ILTLOAPnIMFuT+4C/4DF94C/pzBXYDPj/MKAPNjN3LkUviR0XLaXhT0ytxe7hHcAn+HBbkU/phBvDPoFaD3uPG7BIUF/h5T97GPsxbNMVd/h15PgPwR5+TQoLDg32+hP+Li2u/uiz0tyIsD9aVmh8G/wB+aB9l4AR+M4UGgHznmW/o+DNQjgsCCfw7+LQtk7owPcNQBoO85znv2VZ2NvYLBXv28XfeCvjp9X7S7x+IZv8oTj9gZ9j3Gd48+jhjBrfBuPX5BPzGrV11ME7cyf8gFIN97Hkb72wW+2wzM9DVzzIcJX4qfMzC6MPIeH6zFjrCPjuVoex7ZrcdXZ2oWxpnjZo755D4W9PG0nrVoqovrtHY7gD46dke3P23s6EQjkB7Vdql9cfZHF2Gx22s3Oxn26hhX2+HgzhxTnZwROK3P6jHVdr3fkbZL7ZPZPXLBVBfWqe1OhL06tnu3O3I8R+CrtK20WdDvOKPVxbbjKe/T1UbQq+OUtdu6/+w8fk8gs76y/ZvA7wev/L61bAHeh86dz7oB9ur4ZO227n9brzsPy2x3GZhb9mfHjqYOy+LTLF9lEc0uvvC4g0HPxs7bHx2X9Xkv8DMQZ/dHx2V9TsP/ykpfWWCHwHh0pwfCno3Z3qBn59ujiFeFq2KrZyCeOWYa+Fe29yOL6WhGd+t/EvbKWMyo8z0DwF5jOgukOm6mr0pAqrR5eYtfWeR7LZrD+7kD6KMwq/ajfdg4njF3M3B6LsDra2R7BnW2/8MafDV7f8aiORz0txU298cYsvvfS9GroM+cbw9LX7HqUZut0O7lBKZt/quAny34U2Dd4yQnAl9V5CrkXiFu5Hg1hJW5rSriWUCPgD/rPuRyW8DvQeEJfUyAnoFQBXoLqHyOUbize8hUvwp6pJjVIMDtqlBvaYf3P3Svzw7+yMI5Ad+xU0zA7kGa5cUzgGbHVCEfCUDeAGIfIwBUoVZ2PwM921/ps2LhR+73rb9nhv4hgb8Q6Bm0o9Bn/WXKXbH2IwBUgc/UOIN7dP9oYbBao3gfv2eF/lWAHy2GVcAbgXmk7UiqkDmWMcv0Resq6JkKRyCPQp4Fld3V/hmhfwXgj4Y9grm6bybAVNR8BPiK8o9AOgs7HleBvNJm5fS3EXgo4Ces/Ej+Owodtp8Be1T1IzXfcx5HAKpCHUHs7RsJLqMWvxLcntLe77lQRtRkuO2dYd8KtB1f7ccrIlaC0vDY0gFVO58FBgVyJUBUg0OWTmQW/yWhfwjgB2EfsfAZQFVAldp7DmDGGYzm9Vlxr7LYZ4D2rLP1lal5tl9BnF1nVrCrjMXTVO8vD/yBsCuIPMBH4R0JAHwd1YDguYCtCl9RzopScz+Z4ldgr7Q5zN4/QxHv0sCfCPuRSj4SLEbz+mz+ov2Zso3k0BnMCGHWdmZ/JUhFSp+NxdPk89mC2UMtpvvYAfjMsmcqr3LvaBv2N6Ly1T4zq75lPivWOAN36/4Z2EcCU5TXvwT0WxbINMiVA3eAPYM5stJVcCOojwQ+cwKVIfbajAAUAboFXs75MytfTTEi4LNc/5PxelR7f0ngB2D3rj9T9lELXwG74gYq/dwjp6+oexW62SDgQV49b1QzOCSvf0TonxH4PWCvgFlVcA4ElcCQOYysYDcyr56VzUDzAIsq8hnUlWAx4xxGrXzJ3i/gtxjJ27EHqPsZal4NEJV2s7CPQK5mKlP5LACMwNrbjrafCTBK2bOUJbP9D23tty6SHRD/oouDYffAr0Dotakem6l8VfUrdQnVJpqnDPQMNNxfVXAFvBcARgKDuhYF8K7Qvw34Z5+VXMGuwEx0dhngB2D3FvSIla8CpkDNjo3grgaIM/P4o4DPAFbBIYM7289we2BX7vkplf4Rga8W6hRcmV3mY0aVubevAF/t17veqtKPaEAGQQW2isJHbSrHVxyFp/ReWqKs/wJ+ZPWMtC2q+1bQq7BHqs59VByAFwRm0oRI+W3IZ4J4ZnG3wh6pOe5TnxneLChEAeFwe391az+zOEZYTtsWYT/LxlcBjsDP+qiqu9fOAzuby74/yzMj8EehzyCf2Z/BP6r+nrJnbsdd1wv4BPki8BV15zZRvmzAevn4qC3H9pXP3vmrwWAW+mg2zoB9BHJP+T3oK7BXLb0XGLOA+Ta+V4Y+U4VUobc22AB8BfAZG1+Bjm16BfJM+dW1Riqv5k4FsOoUeVaZVbBSbPPaZPa97+c2mYXnYyrXe6i1X8A7S25H2DOwqzBVlX0W+D2g5/QGr8VTfg/6inX9f7eDM0tfUe+twPM1ZOdksLm9FxxUQMAxTJX+qtDfTeE3wK4WPE6GZ9Mz6DMYWcUjVR9V/Iqr4OtXKYsCm+c4Wqyerc/sdQZeBHqk6ErxPUuvzmFtR0CPUpssEHwy/leE/i7AF2FnsD318hb+CEQKUDu/UvNM4bP92Lf6jPf+E243ru4zsvVVK88ql1liBo4dQARpZd9IAPCAjmy+ciozSv+QKn9l4CuLOYOgAj3DqZRUqXum+JVjItgj0FUw9OZyT4Vn2BG4LP+u7lfAV1KBGfjV/XgqXkl/PgTZq6n86cAX1X0UdmXXM+X0QK+APNumavXVtTHkFUvvza+nTpmlj4BX8I+otQWECvCVAKDSDO/6FeRPae2fBXhWchUAIsAZHmvrqXQV+KydXWdXc+VGPKjV/XopT8XaR4s7yn8964zwjoLsAZ8FD6Xwe0JfDZJyvK+i9KcCf5C6e7ZdWfPMrqMriGDn4IEBomLlub13D9V740UWBQRsWy1mcd7rfecAgPl9Bn5lf5ZCHA095/qe/b+stT8N+CLsKjeN8lWlgBE8I5aa4Y8gVfsi8FnRMYBkqQiOh+X5W9Q9s7PeY7lIPRW8vR8vj2dHUE0PVH9V6DloMcxqfwS45wDe4b+Cyl8NeHU9vG0UcgVQBH5F2TOFj1ICBXsEOd9vpPpewIxsfdXOK8C9XNpTYoS+Cv9MMFDQ8zUxvFW3M63yb5Nz5x+jvRLws7B71n1v0CuKH1l7A70abLLr91Q9mlPbV4EcF7ay8JG6esD3YxT0VagrQWJU4SPQn07lHwl4pewMu/c9g6yi6ta3B65n6/Hxmqf8Cm5+LKfycnVfrOY8x9Xik3q+jqpo/RjAGWgMtTqO23jfPcuvUonsulRgy+z+tMq/hMIX8/fIurNV9Ra/srsKZk89PWitD4Qd+/Wg5vaVwKPSBVZzbuOpfWTlcZ+n+Ai1ssCojiMFOgOmovaVIOAFAIY5S0Gq8PNY8DiH+fw9oT9c4SdhZ8Az4D2QMlvsAY7HRUEg2pfBnrkKvvb+vfoyzui8olWOFn1WJFNwKhht2/+9kVKButImOpcHu6pNsIJ71r7qlj4E3ntBP7owqorx3m4S+Ejt91R3z2JXIB9R9eg8BrKn7JG1P8rSI9helZ4tOcKW2XUGcy+l92oBfD78ngU4peZRbs/BQjLzysCroIPb1OcK9HtZeQU/Vtq56v5lt5doMgXnflnR1cs4+BhOqb1yRpUg7S1gtukMSrSfC3RKfRHQI6BXYEcuZQZ+D/DQ1r9N1B0q9ldQ+D2Aj3JjtudH2HVl3yPgvefwygkY2Ah7FvCq4DPobG8ZAFb+Sg6fWfF+DoP9COgZcE/tPdVXQUBB7tVB3ID7dMDvYOcrSs+LX1njUbUdzc094CupgWpjNt/A9XJ3vldbXLg9UniVu1cKWwymARBZ+coz+J7TY6EwCxYj+yu2fu98/nIqf6jCF4A/Qt0jtfeCAQLFnz1oUXkjhcd9Xt7Pyu49DfDSFH7jbkbhLTBk1XYGR0Gu+vDUm4PFiMrz23teIMLAULH5M9Zewf06wBdgV4uSA4BnXTnfzSCftfGRctu+irqrZ/fecRwUUO3VvmqNQym9Z3etbaSgVfC5jwjKEWs/EhjYtVTyeE/tPYv/ELn8YQpfAN6DG20pf1b2nWHOvo/Y7Az4Cuz9fKqdchKqIu+pPdv+qqp74CvIeXFbIa5vZ2U3VUcQR5TeILRjzN57QWcU+AjySgDwQK/k7pdR+kOAL8CeqXukWpm6Z2rPwGfQerDOHBfBj+lG75uLdXZfSuVVkGOw+/HewmPLy+qtgIjy+MxqW+CIHETvI4J6Fnh1L1ndgtWe1RzH9dLW/l7AV3P3zK5mcFetPMJVCQj46C3K8XlfP86uCVMCvk4EHkHHXB3vnXN4zz1li1Hlura4laJnwOJ+BWgGbaTy2bFeLcLL5730JoLdAz0b5w8u66yK/aMCr6w9F75QMe2zQWZ2WMGq4FcFOrbk2KfnCvB8URvvOlUAQ7ekinfKwvO2aLEbOGjjsb0HJQNZAR6DisrnPchVOhEFoy0qX7H2C3haYdX8PcrZlbX3oI/y9ixPZ0iVMmMfGeym3srCq7weFZ7vI4JduaPIiiLYSgUVjErBR6HlnF+BmhXzZoFnK+9Ze1b5zOLzOKvvd1H53RW+kL9X7Twu5kjRVf6qwB8Bmx2BffesfAQ8BoiKquO58d7wWHMTVeA9lWdLygs5y8UZbq9whzl7pvQevB70mbW3c3t1Cc/iY3ul6hn0wyr/NpkHv313BeA9tb+XumfwVqHlfqLin6fsfQy4H6wBGMjoAhhuNce8GFndMW9nK8/fPYXm7ZmSZynACPCVIBDl8gr2p1D5s4HfU90zZY9svKfgDBh+R6Xmvtmm9/1YoOufMS+3/QpoGyM+HmsErOwYHLMxZthV/u4VvBSUBge/JdfbogPo+yNHkAFvSl1tV8nhtxbwdlf5h1L4He08q7uXrzP0vPAV9F5RzmuLcGIbzwmowMCpgH1XxUAVZHAbKzxfN6u+Z+fZptriRdjVZ1Ta3geCHkGvgFXwRkEFgwimCQaup+xZjq/s/ojKc46uAoHNg7L673P0SsCjMlXtvAe4CgRZMS1Sd5W7e8Bj2wh+BXt0LCs83w/m9Qg5V+7RvuMiRKWPAFHWXNl3s/4K0sje28/Is4XH/jzAuV9OPyqqz/Ar8DlYesDzdvX9k4D8TMBnVjMDXgUBpW6ZlWdQou9m1VWO7Vl8VG9l2dn+Wz8R7OxK+iJBy2/3zE4oUndPwRh8hoRBVLl1BLnX3s7jAY/Xwf1zAMhyeEwtvCDggZ8FgArkd1N5BWG2SNz9iaXfE/hI2RFOz4LbdoQWFZRt9Yh9R4BV4U71ZfBiMOCAogITq74Bz6ruzRlWsHmBKxU15Tco8V9uz8/n8XsWNCIHgIGhauszS1/N5bPCnWflVd3E5ehIlb8i8CN2PgM/yssZevVd5doqx1ZwqqId23h0ENV+7Z6tf6X2uJhQ/b0iE2734PAgNejw11UZ3Jzfs/JyEMhyelRmU3l1DLsBz6Uoyx85n0zdEWzvM6ZRp0O/G/ATBTtl4U2hvH89wKPtFbAr+bhnz207Q4/A/0R6N55tPB7rfVbnjwJeZW7RvqPaRxY5Umfch7abt2P/CCy7ApUaRNa+4gwyu4/Bz1N+Bl/l9FFef7dcvrIoShb/IOC9vB1zVc5fOd9llffgruTb2Ebl3H0/PoLj72jZ0cZjX+gqPFvv3eNWS8/q7hXjDMwMfgW6Fwgs4GQBgNU6ShUwsLCa271yWqBSG0/1FeiXVvmzgOfzeN8jO5+pmbLvuM0rzkXbVWGObThbcQ4E2F4pu5e3M/ho3fEYOz8GQQvSGAwxcLOys6X38ncGHMFX8HuWXik3BxEEkfv2HIhqpxT9CJVXoF/O1l8BeGXtZ5RdAa+UUCm8t60SDEzF2dobsJ5iq/3YR1XpEWq7X3Y9CPtXtNa+Bn5Uti/KH26t/QD87TdWW2WVOU9n5VYFPTxGuYeK1VcuRF3fCOjY1ite8nZW9wx4ZeO5mPeJmz6ieLcL8AfZec7jPYXn7ZGln4Udj/Meu/U2bM0jRfeAtn68Y03pGW5rj0r/5a21X9Ba+zk3yDvo0X8/egP/X7fW/lFr7T/e3o5T6t23sSrj23S236y0BQR87KbsOAcKL21QKp85AeVcOEXg71WL/xB5/JWA95Rega5y+MjSe3m7suzVF2dQjbnQpuBHJ4B5Plt+LwVg64/Ac5D7JTfQf36pAKMbfVdr7Ttvqs/qbd8RMM/eWxtW935WDBAdNP6uzstFO/wepQUZ7MoRbIEd0yROp6Lvh6r8GcCrc0Q2nnPRSNmzfZ4l57zbgoWXT/N+DhSqUGdFOduHfWN73m/t+HhVG8Dr6p9/aWvt17TWftoG0PuhXen/UGvtf9GvkEZ4K9BjGw9elQpUrb2CWEHv1SRYzbGQp5QfA4D6fHmV3wz8hJ1Hq862PbPxStlxGwOOqq/UfFThlcqqHD5SbVZ3Bb/Xhp2E3V+37N+8A+gWJ/5Ba+3byM6zgiuYvW2o3ugKPIeANQQv1/fs+6zKe4W8qspHOfx08W7vPP6ZgK/AjsquVB6B5iKcV5zzcnfczgU6T+H7dq+tCja9APdrW2tfv1HR+fA/11r7p0LdMb/GnJwBxfw+CxR2rKoHcPHQAodnzzl9wEp/9Bn7M8A9tecAwEqvVH4a+Dc12/Fn5O8BPJ8TbfmMwldz9wh2VZTjgKBsPAcFBSyqvbLvuI33q319jPr2r2qt/ebW2lfvDHvv7g9Q7u7l8JZzK2XHfQaqen6PQUS9eaccwchThCroWfHO9nvAV0FH+G3q1Lb3aX0m4CuFOmXjDXK1z1P6iqWPCnacg3tFO4M0U2rcz5/ZHXCK8LNba7+jtdar8Hv/959ba38S7Lyy3ViA48dzHARU5V7l/7ytQ/Dj8KgQgw4qPVv+SjDgIIDf+3han/Z51Nargt20yj8j8CMqj4rOwG+B3Sw7VrytP1UwQwgN/qhAxxB7ym7b++u4GFRs+5Gw9/H8/tban4VF71nyCOy+T6l/ZO8Nug65gcbOgUH1CnSRK4hg36ryDLoH+ZDKvyLwUTXeoFew47ZM4Tl/9xTcq7Yj0AYrKjdbewYeA46lBlix75+7jf+W1tpP3lvWob9/3Fr7q0LhUalNee1fy9k5rzfwMU9HpUZg+bGdwace3eE+z7JzGsJ5PxfpvKIdB4Esh/egXwp/UzDO21G1WcEje8+5PAcAD3iv8o3bUeHtMwLOljvKxe04hBnb2w/acB8/pbX2O1trP+tA2HvX/6S19u0EvCrQGYj8r9ltDgJcyGMLz8B71Xze7ll6tvYYGNQxth8tPcKubD3CnYHOqp59f4ocfkvBLlJ5fvzWB2uLumcFOrb6npIruPu1dqg9S2/Ao0vobX9Da+2XHwx77/4/tNb+wi13zopyytYb8BwIMGh4Vl2pvBcYEOg9oUcn4EFubZTa4zb7bNMWqbyy+W/HPZKl9wBHVWdFj1Q9U3hP1dUjOFWF94KEUnu26NZG2XkOCGjZ+bMFCQwIX9da++0nwN5P8V8gh0fYlAKrXB1B52O8AKGq/OgI0DWwcivrzgEAXUGUAlRyeBUEPMirOT0Hhg9TvRf0Rz+WqwDPBbvMxkfQzwKPNt3cAtt0287wYu5t6s35OOfipvD9XuwzKj9W+vuxv7e19pUnAd9P80edHJ6r855tV3ZfBQwG3avos5ozwKp456UEHvAIe5TP7wV8pP4L+Fu+P1qhj2w95+6RuisLj+1/0m2Gsso7guypPFr6fo5f0Vr79SfC3k/1Ha21fyV+cAYVmtUdC3S9mIfQo81HqKOUoR/T+7Gg0K+Lj43UHq25BYRKHs+WHuH3LP6MwitVP9zWX1HhOVf3FL1v70DYv1HRLivYqefv+KgtU3d0CP1cBq2n+BHs91b3Pp7/vLXWf3hGWW2E1IDkQGDKz/Ze5fFekQ+DhAcvKjtaewZ7T0vPtl/BXrXypxfuNgE/8R49no+tvPpeKdQx8CoIzACPqq7cQAez/2fwYoCIlL7vYxtvlh/VvbfrP/X2G09W9366/9Na+zOttR8JoDdFVzbfgEeFVvAj7FzF531eHl+p2lcVvvfV/4ss/QzwqOZThbs31dvhFdszgd9SofdUnlUdC3HeZ7buBptqz7k9F+rsmA5q/8+AVdX5qp23ANL/7W/Tfe0dgO+n/Huttb/jAM/K7uXsaPMxQHi5+yj0nLuP5vKexbftCn7P1itVV3Av4G8LOrLyZwCvXABbdSvG8b+qCo+FOrbpbOk5b+/7f2Zr7ffcCXZT+b/YWvsfN+g531bQM/is8CrvN7gwIPTzqzRBFe+4Ss/2nY/xinaeqvN2D/jI2lcVHtt9mPpHVvjM2isrX4F+pEqvHrWxyjPwCDHm9d5jNwsMDDw+hzeLj66gFwHvZedxof1ga+2vCOBZrRXIXLjz8nl+9KceBfLTAISY7brK5aOKfaTyWDuwzwr4CPZqPm/jfmjh7khL71l4A9ducCR3z6A/AnjO3w1ezNnZ6kd5Pefp+Nwd7Xz/+fZfeEeFt1P/y9ba9xD0Bm8FagwGKlCoQh7WBXB/pvCc57Oac5FPAc3HcM6uvi/g4bVZBvso4Bl2lZOr/B3tOR6DELOFx7wfC3RYoeftBjO+OmttOAj07b/rhNdoq/GkQ//d9KiNK/Fs8S0YsLKzVUcIMQXIntGrJwgGfGTbo2q+FwAy6BfwFwBeqf0s8BHgGBg84DFnZzvPlt72/8EqjSe1+xcCeqXeHShUfgwMXntWea4FGMho37mmoNTfim6Yh9txDDfn6tWKPVt8Bf8uOfybUm6s1D+TpR+x86bk+OjNcnKV20fAY/7e++D35D3LbtutPar9z7hzwc6LIf/19ny+/8x8lMfbPlR9pfRs3Vn9O5T4E3lYxVcVfVR9pdZ8vOcEUNGzR3Qe8Bn4XoHOzeEX8F/6eXEGVX3nCjx/N2jZ0ntVevV4ztqqH4xRj+TsnXsE3tS+/46633aScs+c5p+11v52a+2/Q26P6m2gs9KjzUfVxqIcK7f6jpV9tPFZDs9pQKbyXj6vCndc1KsAr6BfwAPUUdFuROG5EMdWHwtyyg0w8KpohxX9KvAG/9WBtyDRf9Cm//76/9Ra+7cC/p96+9n9fw/7vEd1puaqat+h7//ZsQi8eiwX5emzOTwqvQd8JZdnyBnwBfwDAG9FPbTvFeA55zfg+0/H/dYZ6b3gMf+mtfbHKBhwXo+Qm4XnR32cx3uP6iKVjyw/Aq1svG2rqLtBi/8iyN5nz+a/T+vK4bdbeszPt1r6UeD7+fozd87nnw34P3FTZ/UYz7P2VvzDYpwFAVZ4bJPZ+krR7gjgq4W7pfAHKDza9D2BV0U7fqsOH89he8vf+7+PYukrhuLvt9b+PFTu1fv1/IiPq/RYxY8q9QYqPurjPH0WeAsqswq/gL+tlr1evIlyeM7Dq1V6fuSG1h0tOz6nr1bpMa/HY0zxr/ZYrgK3atN/6u6v36r6WMxjS86P7PhRnX3H32TLBT0DnvN6hJ6DAR6jKvtcsV/AOyvh7DftGPg9X7wZfQ7vvXSDb9Kh/Ufg7fPvP/iXVc4CPHrct7bW/i49xmMFj2BHqDt8Pwa/unoGeM7jFfBedb63rRTsVA6/FH5nhWfAFfDqTbrqm3ao6PysXlXpEXp+8YZzfZXH919pda+flBuFOmr/R25/hRaf248Cj4/esFKfvXzD4KrHciPAo9rzZw4ECnAOBKpI97A5fL8ZVPnsM1p7/qy+4zP4rc/i1XN4tO+c8/PzeYbbqu/4eE+9bYe23o4xhe9/QupX7knenfr6fYUKfabwC3hTxwu/abcn8NaXgpy3qWfqrOwYIExtVdBgsDlvZ9W3vrzn8mjp+cUbrtT/3Nbab7kTpHudtr+g86dvFlxV6PntOy+vX8A/OPAYDCpFu6OBR4uO4Htv2mEaYBD3a8QfjPGewyPYVqVH+Pt+s/iPnsf/pVv+rp67s6qryjw+hrNiW2TpK0W7lcPPhvOTfsVV1c57tt7L23G792otKnVk6/F9erbpqPqo8FylN/hte4f+G1prv2x2fi5wXH/S8D+d128X8F8UAHGqniaH9yy+UnhU9Ezd+3628WpbVLXPgLdgwtV3U3oEXr18Y4GDf1BGPaPHH5X96Rf9IZpKLOl/Y77/xhx+hl75LTj4CA6fraPlV2/escKrt+fWc/jK7HltEpXHQt3ewHMgqDyHV7l8lMOzwjPY1p9Z8iinx2AR/cYbfDGn9/ebWms/b8sc3enYP9xa6z9dh8DzD894vyaL375DGx799BwCjy/oIPizwG99tVZV7R+rSv92tZ9/zlDj+poBnkH2FD4DXim6p/JeDl+x9L3P2d9Lz26gw46P6Pr+/qOyv/tO0M6etqt7z98xL+efoFOv1GJ7fEXW4OVXa03x+dVa3I6P5jh/533RizcL+J2A9wp4ZqNH7D0DzXk9g23fsTjHx7DdR0fAio4Wnx/VoRtQVh5zfbb+j5TL/2hr7Y/f1B2fk/cXZrB4t+VNu74m1Jt4CL6y8x7w2DZ66cYCgilz9Cwe27CSY57OOft1c/gHBl4V8jyVx4o8V+ftGPzDExYg0Bgtm18AABhNSURBVOZjgY6Legw/V/r7/v6XY/tvsD3yz0TPqjkf99daa3+L/mqMKtDhNlR7DBLqTTp8tZbtPeb7Sq3Vizes8hZM+JVa/L6AD1YL2nrP4qvCnarOZwU8U2el9J66s8qjwqt9Xh7Pz+DVyzj8qC4KBPycvv9AzdWfy39fa+1PwWu0qOIGuCraGXQYBPhYLtL179gXguvl75wCcFBgpffU3gOe36RjR2CYPK3Cc6Fu78IdBgUu2lVzeH7cptIADBYGrak95vme4hvYbPnxO+bzqPp9u6n+17fWft1eUrxzP/2XYHTY+1+rqfxqK1Rb9aqtl8NzYc76waBhn9H6G3yVoh0fz9Y9UnsuzqkgoMCPtr1P1V1/Hr5g6Y8GPivcdVj6f5FdV4W5KI/HfJ9zf1NldgFe9Z7B9nJ7dA/fdJFfX43xouft/Y26H3B+nTW/UcdKzmquggHm7Aw3KjoDzZYdAwTvU2lABLvK6b3cncHnvP6lgMfAwMU6hjqz9Qy5V7hDZbc20fN4dAwe9NFjOQwsBjYrPBfueL8FlP735q7wO+v7XPxvgh0Lc/ymXN/Hv9EG4VVv0RnMGBQ84KMqvHIAHuDK2ldV/jDgt6q7Ut9hl7fxbTsGHWGOggDm6tFntPmZyqsAwFae7T9C3K9XFe/4ZR3O7dV+DAho6fF8V6jc999e+5dbaz8U5O2ozBYAUIWVncfiGwPP1tyz9AgzpgGs8J7KM+DRYzkFuWflo/zdU/03Lh8VeA40qmCnwFdFvAx8zusrAQBV3lN1Bb5tY1tvqq1yfrT+yt7jMba/j40FiV98+9vx96jef//tN9n8sPOIzCvAoQNA1eXHdGzhuWJvOT7+i3k/AsvVe++Rnafi2B7hPtXSXwL4Qh5voKJ7wG1bgc/y+MzSc3FvxtajE2DFZtAjxeeUwAsCGES+qrX2jSf+7Hy38H+jtfa9zh+ZZEVX3/FFGtzPj81G7LwFDy9H52DAtl1972uLtyvgUc3VZ1Tux1b4A4AfVfcop59ReLb+2AcX6bAqj4GFX7XlSj4Ci9V5VnRVE7Bt2LZ/7mr/q1prXzmcl9UP+Ic32P9b8Hfje2/83J3VW4GsnrmrdsqeI5R4TGTrlQMYKdpFSs8Wvwo8BoMPs/KsCp/l8VzUy3L43p+y8qjsCmq1H2FTnxlsgzKy82z50QGoAMDpBDuSXsz71TuD30H/m601Ax3VFAFTas2vxaK9Voqu9qtzsE1Ha+9ZdgwWCu7RYl2m7ErNI4V/CuBVcVBZ+koBz8vj7VhWdAwGntpzMIgUHoHO8vuu8hZsMFe34/jZvJ0XX8ZBuLG9ug4MQP24r2mt/aLbD97MqH7/A5L978n1//uzdVZWZaGVSqvc3FNuDgD8nW05qznaeYaeYc8svXIAI3a+ouoMuAv8HuquQKybO2i5sVI/m88jzOgK+sKPAkClcFeB3s6PNt+z29zGg7fyuA+DAH9mV2Lj0H/M9qtv/3+5+Ku0PS/vlfb+PL1X3v8d/KJIW4Qdrv4fQxUptsHOxTkEV0Hu5e6s4t61jMCuVN222f2q4hwGBLbwqPwIfvSZ933C4qWAf7vS+KfmOLhwIS8q3CmLP2LruWhXLeJ5aQBC2a+N83p+6YahZMuu8nkOHGzjWeH5HHZdtmgwCKpAjwvaFjmrmWeTLRCYuvfj1Is1Edj9/PynojltsH4jp2HH4L8YJLz93B5h5s9K5avA39XO76bwE8BHAYBtO7b1VJ3z+H6MZ+8rCu/l9wweqrxBxv1zAMB26CQU+CpYeM7CS18wYHKg/URFbqqOwHe4bDFXVDNSe1R1DhJeTQCf12PAwPYMNMOrbP6InffgZ/BRodkR2Dgv4G8j4dl63u7l8N52UzaGEANClNuztVeBgHPnaq6NfXPhjeHnAqGy9baNAVduiEHnxYqQ48K2AMBqberLeT3C5hXelPJjCsB9Mux4jorCq4DAUCtLnyk7w8y2HseYP6vvn8zR5Sx9QeWVunigs6p7Kh8FAFZ9hDWz+Z6Scx8KRPXozrPo6jxZQQ6Le1HgQtAt8NkiUnPhLVil8sreqwKap+QMfxVwryhn/Rm4eH24T133iJ1HiFnFGXDvuwc3jv8hsCNEKuIPbxv87Td8/kzlFdyZmimFV7B7+TqrNyu8lx7wcQhxto/Vu3/nol+/774N/43uFUGPbD0u4n6MV7xCsLzqt1dBZ0W3vjgIMKhs6VnpZ9R9FHYFvAJ7VN1Dhd9L3R8FeKXuDLoH/laV5wJdBDyrNQcDdgOcv3PfBjS7A1R1lWIY0Na/wc7fVUBHNVcW1hYyVuwRGgV53+8puirAqbxdnYMDBau75wYU5KpSjzZfjQVuU58ZYnZQPP5Pp/BegEHlZsvJ+0ZUnnNXVcTz8nd0AUrlPdC8XNv6szfwlIpnAYHVXKUXNsZo4fEzjkm04FjJPLX31D2qpPMxqOQIOyo492fXw/k7w+zl9whzBnsEfga6gtwDewEv/lRVFXhP8RX0Chx0BsqCs3Pw3r9HO27HWFHPlBqVXjkIZd+9a2bg8ToRcLT10QJEi6/sPefEGBj6Pn52r2BneLlPT6kj2L1AxC4AYbZg4TkbHAsVBFDRI0uvxtuF/W1CN/55KW/ilcUb3jb4PJ5VHxdipu4ItQc429tZi+/l+AixFyS8nJ1dhPrOwcBAt2CBx3iw4xgrN2VzbHYe55yBiJRRFcQ8pUf17ufDN/QQygjcTN25kKfUHwOUd68qAPA2hL0KPo+zy9oC/ktDw5ad1UzB7QGD4DC4nn3nY9g9cA6u0gfO41n1I/dhVh3vKRqDLNCzynhWFvP8KB82IFHBrU+GUVl57DsDnwORV41X18vHegrvQc5qHqk7Bwb1/RPw94Sd1XVYzb0D7qTyyu7jNs/OM1QKeGsT1QE84PuxXvEP1R9h75/58Z5SdXQwuN+mhvdH0Hv5prKyShk5F/aAjyy7gpy3qfOMAJ9B7wU6bxwQcG8MIzU/zc5fFXi8rsjWb1F5D+oIdoSQVdV7PMeKrQIG5v+Roit3wpBzJZ4VvxLUPcVCyE2ZvBxeqfpIAEBbH8HsOQwVFFSQUilKpOQM91Z1DxV+b3V/JOA5CLCaI/ye0nsWn7dXK/dRyhD1waqO7oFtuheA7H6tPf+rxqcCO7cxoHB7paodqTAX4TB4RGmAXYulFOo6VN9eOy9nV0quVJzbIbxK6RXcp6r7lYDna7EFm6n9XuAjMJUAUEkPEF5WcaXk6AZQuVWurgC362bV92w8jnG2QDObi4EBgVRwcrEN3cBo7s0wZ4W6CHLrCxWe1V4pepSze+Nqc7KAv41EBDyquVL+isKjQkbK74GtAOZ+rGhn203Z+dxow1VVHgHGnNy7bltMFhRGgVdKpRQOYTNo+V8s8GVVdS7MMZyV3N3LzyNXoIIAg+4FgUzVF/Cw+hBq5TY4f/fUXgGugsKIpWfIlLXmvB3PyY/iIvuPah8FA27HSs4KXwE9UxrPtjIkCIQKBL29PZNXeTPm7J4r8NQ5CySZdff2j0CfgY/7cV6eQ+Hf7m7s5+NtEJSyczDAhR1Z+hHoo9zZs/iRrWfL7uXiXjuD2bPueL14n17uzsEVF1302eDzFA4DAoPOaqsCQabanmJz0Kj0UwU/Al1Z+grsCvjTYVeqWl0IpXYT0Eeqz4qP4GeLvqrulXaqIp8d5z3OY1fA/bDTQJuOqQIHTOzHU/poDhFkhr1/VyqMbiEq7I0U0SoFQpW3c03B4OL+GG6vVqECngLds/AK7pcEXgWdUZX3lDwKAhmg0f6sip/Zd7beqj0quI2HChoGmWqvnFElUPNCZPgxL+fcHsHidmjbK2pbgV3Z+ZG+FfCRm4kgj/J1NabuXBzxOI4VobIQptpMqDwu1Az+SPXZ+rPqjUCfWf7M3mfnqgCO4+JZfZzXbOxwPiMFQuB5UY8oJ1fBVVW8YuEzO18BXrUZAZ2tPQaC6DO6oAU8jEC2WBn0SM09BxDZZzumCnK1XabWWLHn/F4FQg4UalxwYXnjyioeBQO089aO4bVFz9a/b49sdUXVlVPgpwER0KrWwEGNnYsKBlXIh9T9bZJ3/GEZjiqzxZyy2m9UeOVElKorGDyF5+Cg1Le3GYGY4VTBhLcpWL0ghAGIx0TdJwe58nxBQ0/NGSZWLS9/RlARIAZY7eNzqiIdBhkE2HMS2CefMwOcFd6z85FzknNyJOwIycyCKB1TAN67jkzlGfJRe18BP7PiXv7tBZHIVeD1ZwHDApIKiF7wy4K9sqm88FHVcDF7+TpCWAFZKS1beKXefP4K5Kzio6qe2XocKxx7FQTe9z888G93nT+im4V+VO0zNZwBXPUZuYMs0PD+6DsrOTu2LQ4usqK2Dx/bqWJeFAiqUCrgMXjM9FO18J7Sq8BXse4h7G+TeaCdP0XhDwY+UrMM7gysCvzWh2fPvcq6l27gORXMfBwrPO5XY8Mqr76rRYlgsxXmxe9ZZU/tM+VXef0s8Hyu6Lu6LxuvGXW/O+ynAV+EXqlRpFgj6l5VyQqIXiDwlJuhj0D2bL2Cl5/JI7we+LPAsyIiDBVoVN6f2XerBVTaKZsf5egj9j2Ce7fc/Qx1vxrw3vXsBb2X4yvIPHgj1Y+O8RwAg8zXol604XHKwPcg53HNLKla3Kz8CL+n3JEjYBC9qn3mCrIg4V0Db6985+Cnvnvb3ufmaCtvJ9qS41XU4r1NMY9X0EfKj/sqih/ZfAVsBv7M/uhRmheU+Dw8fyooVhwTz6GynQiPAkBt8x7TRcpq5/Hy/kyxlcpX3YcKVFXYK8Bfws4ruIYgHm28AXq+VgU6tongjxR9FPoI+CwYVK5D3ZN6jx6nInJEUTvPntoxaj9vU1AyOAw9fq+++sognwm7F/QU+N62D+g8ncK/r5r9K/YV0BE+bq8KYWzdPXhHtnNb9XYdBwEv0Fk7z85nzg33R+oT2Xzcpyr0CvRI4SNlV4BH0PO+7FqwPUNazeGngT8Ldl5MH6LOERuKKl+xo1tUXgUAD7askLcF+n4sv13nBacosNlU4ZhgMJidSqXg77EbOmUo1PdqXl2x7tUAoED2ro2DgvruBYNp2N8m9eBHcTj5mRLMLpTwuIOhn8nrPchQ6RXYlW3cRp2Loc/qEQz4iI335lwpmVrIWWXaU3GGL3p+zkBn31WakIFdCVIjgHv1j5CFM2FHxTgEbK/TIvDe9WXKzsdV8/kZ6COYVY4euQi8bqX6uJ/viQPAHkE9svMeCBFEyvZ7qs+AR6lA1gcrNbfn/dm9ZUEQxz4s1p0N+zMA78EdwcEBw8vhVQBQ9j5T+YrCq6DE7uJM4L2FOqLuVWizdlGRT8HOTqLqONS9ZfdrcD+Eut8N+PdR2qeANwM9wxMVyzLIq4Fh5Bxenwp6NY9Rqqb2RUoUKX0ESeXRnIIzs+dZAFCK7qm450g8lY+2l5XdGr6Mwu8AfLbIleX1tm2x/COFPgXxyLYoSL2vIVx1t88zdZpMsTzQlZpWFTez7ZXgMAL2VtgZ/iHg7wH73RX+bcRqKu9da1Ssiop3GTzVIFDJySttsuvJlB1dSAS/iAfhpkjd1Vt2Htx7QJ8BXzlHdn1VZX9I2J8deL6/iuozOKP5fabYWfU9Ol/lfrzAUJ3rKHfP8tmKYnoFs5E8W50nKhbydXNb5QoU0N79P4Sy76kCo6rxoX1R5T1rOqryCgpVyIvaVQKBV4ib7bcKfAR3ZO+rebyn+hWQPOBn1Ns7XwQ/n2cGdk/do/F7X/P3svKXAv5tBGvWfgZ6T/VGFb+izpnCe7BnfVdhV+MzmsNHKu+pWRV2BmxWrSuKnoHvwc5AR2nNQ6l71eZtVvBKB0XgR9TLU/6R3D5T4xGAM6iz/R70akxGIY+mKFrwszZ/Ftgtqu6BXL0/T9mj7ZdR9ssp/IDK7wF9VfUzCD2XUDkuCjyR+/CuvTKnWSDYauuValaVNmuH6cDMeVRwivLyirKXbPzbhJ34+mwUvbMFUBHnXducqPQZ9DPqbsdUgI/6H7k2LwDuMbdqQVcgieCqgD0D9MgxSpWzolw2Fi4HV4E9UspdIR7p7ADgq1a4orhbggAfO5paVO+jovTVKaks8q223gN1ti7AMGfKnsGv9tv4lRR+AZ8stx2gV8GsUs1XEI4qcbUYWO13b9CV8lcW7kyuW1H6DHi1f4uaj9yHB3tlvC5j4xG3PWxfVS2G2u0AfaViXQU8s9ijal0JClXQK/c5NPbUeFblM2WtKPgs2JnrqKj6Uyn7nrZvy2IKjx2AXim6d48Vpa/ANgr5TNCoXMdIAM8CfLVopyxtRTmrQaACelX5K3BXCnRRAPiwjq9k40cWyGEwVzreCXgVDKrQe8BVnUHWbrb/aA4zqCtDz21mVZ4h2QJ8BnhlfwX+p1T2h1D4t5GvvZBTuZ8I8iwoVNV8RMUrsEcKn13zDNjeMaPAe6BvrfBXoK44DeVQom1Poe5qwey5SHbr60DoM2i8ILG3ckdgjwaqSvCrzo1n8TML7O2vwDiSf1f626rqTwP7wwB/sNJn0FdhnA0C2fkz4LN5nLH4W3J5T92Vgo7AHfVbBd9T8dH7lQHzqnk7XuzMYqiqw67tBlU+gqBa1fYArgJaPT5qFwWbTMn3mNuqwldUtAJlxfLPgu8pdfaILdv/Ng+PAHumDLsCu1dng+Bni74C/9YCXwZtxRlkQTq7z5G5zhZ4JZ+vKOmoum8BfRT2bAw+Wc6PAvvIItiL11362RH6CvCZos8CnfVbvbbReVT9jizwEftbUfUsOMzWC7x+s+1ecHDX7gJ+F6zjTgahz6DwFHI0f87aV22+d70jQeDoWago/RZYtxwbQTsSsMIxfCTQ7UYqVvDohTPd/0Whz5S7sr/aJgtkM/NcVftqfr83uFl/p8D+NvAX+Qm4EYBeDfgMkKrSV4CsqHHmCKLrjeZu73kdVcUj1F+BXDnP7hb+UWHPFv9I4Lhr2wmlz+59C/hVO14JCNW+ePzvDbynsjPqXIW66jh4rKqO5u24R1R1vOG9F8bdwD8A+lHY9gZ4JOjMWPeRucqgGIFtC8DVY5eqO7P7NMC/z/DYq7hVUEbhq8K/R1A5M4CPWvsIvCq8I8Ekyt9xnLIA9gGXR1f26kIfUYFLtD1I6Y9OAaL+s6Cc7T9iXjJg9oB0tI8K7Nl1y7F6FtizRXzEQjmlz0noq+MxqvajKp5dRxXwartoTqqAZO2qap5Z8VmHkfXrjsEzwZ4trFPgPPIkdwI/c0+z1fUqwNV2I0OfAV2xy7Owzh43DfkbGA/4yK0yoUcsjsp5T2uzAfpqQDwK4MrcVNocNdaVIJC12QJz1nfF4j+9hecbvOeCOWohyn5PAL8SILLxzvZXzlEd136uCjSV/ir9ZG2y/VWAK/28HOiZ7axM8sO1uQj0VWgr8N9jHkeAqrTN2mT7K+lEulaf1cK/rMK/J3Rzj+1w3KogVtpV2lQDhLeoq+fYBZwB11ABudKmqvwu9K8C+9aFlEbNqzc4SfFnVHgG0pljZqaoCuFMABnpe6Tty1r4l1f4T1bhdrU/A+Y9Qc762gwRjO9IXyNtl6LPhOnbMdkC2ND14xy6UelnoJ91V1eer1FoZ8CdOceHhfhKFn4pfBCHdgJ/NgBsPW7PHH4GxijCbwF1y7Hv1/TKkOPEXFkx7mIRdoZ+VsmPgv/MMd0K6tbjF+xithfw5yn+ERDfc/52A3Kgsl8OWEvR9VDdc8GUJ+/eDQ9Q/a3KXxmSPeZ2T6jVNR/S/4LdXx57LIrK4nv4NgdBf4TqX32sD4H8LYI+6fvve07oAn5yNA8OAM8QCA4D+5Mi1IJ8aAUv4IeG69PGJ0H/KEXWUwB/j4QL9KmVu4CfGjZ90B0CQPXqR+b5VHDLN7AArw5V2G5kIexywlfo5MLgP9zwr7x83ylbwO87np/0tsCfH9wF+vzYRUcu4I8ZV7fXFQQ+Ds2C+7xFuIA/b6w/nOmV4V+Q32fhLeDvM+7pWZ8lGCyw06k+tcEC/tThnjvZI8G/AJ+b47OOWsCfNdI7n+cqQWABvvPEHtzdAv7gAV7drxG40ggs4K80G+ta1ggcPAIL+IMHeHW/RuBKI7CAv9JsrGtZI3DwCCzgDx7g1f0agSuNwAL+SrOxrmWNwMEjsIA/eIBX92sErjQCC/grzca6ljUCB4/AAv7gAV7drxG40ggs4K80G+ta1ggcPAIL+IMHeHW/RuBKI7CAv9JsrGtZI3DwCCzgDx7g1f0agSuNwAL+SrOxrmWNwMEjsIA/eIBX92sErjQCC/grzca6ljUCB4/A/wfXKtRWv1J7pwAAAABJRU5ErkJggg==","e":1},{"id":"image_2","w":44,"h":49,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAxCAYAAAChzEtEAAAE1ElEQVRoQ+1ZS2xbRRQ9M3Zsx/k5tFBRVDWVkCohQROJLTRhQZO0Cd4gFsWQJ7ElmE0l2CTdVWKBMUsWryEqC1jUuM2nCKlpxRKpAQmkSJWaiKpVS5La+djOi/0GXSfPcWw/v5nYEarkWXruvXPeuefe+ZjhORvsOcOLBuDDzliD4QbDJQzYSiJ4Ww9sZxABQ3DXZ05wjE+/q81bMQZn9BEP5++f8Le/3eH2tbrYTrgVI208TK/9apjmTx4fYrE+LVGJ+cFf9G6eQyALLM4OaIsy2bEFfH5Wnz/mbTnjYhyp3DYS2xmKlxQcvS6BLlMg4mL85OnWF+B3NZWtlRMCCxsr5JsUAuHpAe0qGRER2QzCgiEMoMNyFAx3TBMjTsArAibmXvQ2613+QAHIetbA/c1nyAnTAOChidfajlYEazkZZg5/rS+TDyAwIVyIMBMxACctGy93wcNdoPhESJMXXXYZIYOKgC/M6ONHvM1jxYDJmJhe2FjNAzjqaUbpfKWUPsps4FFmvfAN9LGUtWPelnwMAkuDMvIglUDCyExMDWgjdvKwBcw5H3uj/SVYurQCPNnaxD/pNdCctVg17RGQP9ee7rAMINDkwyl/oCwuzRHLCxsryal+bS+1MkXXP6N3uRgekDZPtx4pC04ACLDsuL+5isT2lmNWqE5IdlP9mm1t2U6QjhmDTqBfbencl7plI5VPqez410hh1UjnP77aoCIllg8EmAIP3dKD1A2oSHb05sZ6dgvHfW1oc+frTmpQ8S0baRz3tdraL6YSeRvqFtPntF4lDRcbW/Io/u3NwMtSQIuNSPcnmtvL/IhRmqOCpiE4eop7famD41niwozeKxhu1wr47/XlfFao6KwCI81aQKmlcYaRG+c0anu2QwbwuGAYsyJQ33xdoeAsP0ufFZAsMYGrbh8i1fqv5acMmFhyKp5K9JQBFpjIAeNOO9tBJLGP4XoBZgKXbw5o46rFoMxwvSRxmIDrUnSlkjg0wHQEZCbu1dol7iWfFLZnisUE+m4OaHN1lwQFPD+ri+LAsueIYp/fE4/3YWvyolOmKygXHTkM3tLnmMBZy5m6hMpOR72W+nDRWJrq17pU2c1nppqTHg4G3KZJaTtzkOBVfRhtFLz3YiRWuMHIrFEV8OToMAWrP9i9XSAZ+iZue5Ss9AFOgPdpV4YBVZtQNO7YWotjNgA3GC5hoCGJ/1US3386NMIY01VBKNhPhKJx2yu9clsjh68unn2Y2dp+RQGElKnX405e+uGuUg923OnI4MoHb11Pbqat9zUpMDJGbX7f/Jc//tYjYyvdh8nw64/e+eTp6tp3qoGd7Ds7/FcuXbv7hZNd6bzULjM5Olz3HS/L+SktEpN6sVRimIwnR4fpAFQ4ramyUm7PlkLRn+t/WrMWmhwdprtX4eZcO2Aod4e985LE6pPhoSBMdl3CVMqECfH5h9/eoBcl5SGl4d1z8TPl6DYOnPMe1XOwEsM7On5vERCFh+hawKseKZWLbrfw6Mn/41qA7vreCUXjto99TvGlJEFB6rhNXw5F48oPKMqSuBYOdpumue+678RGxXnO+0KRmPL1XhlwXhafDScg9v75OQjgLOedWiRW8W8wmXjSkqjTBvJHKBrvlgFmZ6MEuJaF6uX7H/Hd8kHfn0TWAAAAAElFTkSuQmCC","e":1},{"id":"image_3","w":12,"h":26,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAaCAYAAACD+r1hAAABO0lEQVQ4T82UwVHDMBBF//oCN7IVkA4wFZBUQOggHWDnQOwTySkMh1gd0AJUgNMBVICpAHKEGbKMpCRIxjKTQ2bYk63R09/9+jYBwNttEuMrGnA+n+j3tiID3KT3AM5BcsFjpZ+DtQFkvWOJA+lyqt5DRB0ABAvOi95fgG1pU0LT0DxWYZb0QPTonRrJKV+pp7qSASyUliCc/ajgFYcS1+dxgKQLopfaiQ+cFQN3bQus7VUALj2IKOXxXK+b8oEi6eCDKgBHDrSESMy50us+YGcZTUBy7ak4VnsKjgEVCMc+JH3OVRkARkOQ3DUZEAAa7gV45qyIQwq/5wDAWUH/DIBIP2Srn6ttivcOEKU7tkTTtovTX6EbQkACgAmh/vWsqPSgNsBANu4aOjFGCRaNLbmhM9BnNMQKHQDVN4dblA640HRRAAAAAElFTkSuQmCC","e":1},{"id":"image_4","w":12,"h":6,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAGCAYAAAD37n+BAAAAeElEQVQoU3WOwRWCQAxEZ7JnlE60AygBO7AEOoASbMEObMEOoBP0bsYDy3v7YDfHn5/JEPsJVWdiK/KarihN7t+eGzQ7jQJ6AOdDSASibgTqmkEvSM1R1LwyXkC+9eOdMXnYZAJPd3sAy5T7lB585NaWxCSw1DjP/6AWJJfe/KMJAAAAAElFTkSuQmCC","e":1},{"id":"image_5","w":12,"h":6,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAGCAYAAAD37n+BAAAAeElEQVQoU3WOwRWCQAxEZ7JnlE60AygBO7AEOoASbMEObMEOoBP0bsYDy3v7YDfHn5/JEPsJVWdiK/KarihN7t+eGzQ7jQJ6AOdDSASibgTqmkEvSM1R1LwyXkC+9eOdMXnYZAJPd3sAy5T7lB585NaWxCSw1DjP/6AWJJfe/KMJAAAAAElFTkSuQmCC","e":1},{"id":"image_6","w":7,"h":10,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAKCAYAAAB4zEQNAAAAmklEQVQoU2XQwRGCUAwE0N3gFaQES6AErUA6UDuwA7ES6UA6sAS1BDtg4EzW+Z8DDOwxbzOTCRGTF2Z+lZDL1GDo6zBlAJq/x9IYAnf3rqJZVgs4zRHAT97tSGYNiOMCIe8YNisBtxWaHYgkLSk+V0hdiE2+p/trieEoxussawFs5wXFzZAkPVN8TKivvC9GnB5ROvXB0Ddh9Acc3DXRlT5BzAAAAABJRU5ErkJggg==","e":1},{"id":"image_7","w":7,"h":10,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAKCAYAAAB4zEQNAAAAmklEQVQoU2XQwRGCUAwE0N3gFaQES6AErUA6UDuwA7ES6UA6sAS1BDtg4EzW+Z8DDOwxbzOTCRGTF2Z+lZDL1GDo6zBlAJq/x9IYAnf3rqJZVgs4zRHAT97tSGYNiOMCIe8YNisBtxWaHYgkLSk+V0hdiE2+p/trieEoxussawFs5wXFzZAkPVN8TKivvC9GnB5ROvXB0Ddh9Acc3DXRlT5BzAAAAABJRU5ErkJggg==","e":1},{"id":"image_8","w":95,"h":113,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAF8AAABxCAYAAACz6n+zAAATgklEQVR4Xu1dC4xdxXn+Zs65z72v9XONH6wN2GATsg4NTYGWtWqaNoQUYmgkwLA0EVWoVEzrtEmNYyc0USIIGEVRmyhp1iTpAwIYhaRqK4W1mgoSVLK0IQQMeG0MxrvG7N31vnzvnan+c9/3nsecc+65u9dipKsF33n8880/3/zzzz9zGd5L84YAm7eW32sYZyf4WnKAS9YvGetlUg4JwQ8A48MLbbwXEPiZDLRCfwm0Pkh5VRNYjB2UUh6A4IPA+HjT91ryOibZPgDnNn7HgIeEmNixkAZg/sEnwAQbAMMfuwAmK5kcQGHyQLFMJsO52CeB2+zqYMB+ISYGXLQTaNb5A99GS1V7LJm8HQXtAOOFIYC9X6UcA74gxMRelbxB55kH8DO9TJODprTirbdHzGjGriop+FpgfKQ0a3pLdNdL/y+YHEFhctCbKO5KtRd8PdPPhCCqSLsTs8W5JZ6UDEMMoDWgaX0A5AtSaP2m60oLRWkf+FpygEn23RbKHmxVNEBy4rogG2kP+J0GfAlxyfkW5MeHghqANoCf6WNc/DKoDgRaL5m2hWx/UG0EDH4mw7ighW1+Od4HelLwzZUNmjGDaZ1g79e00EuR+KLRwlz+ttnZw7Tou06Bgs9Y6oBL+911B4IuUN4bcJ6ifcRd5fa60udAC0UhRT6fmz19y8zpkX91K0tw4Bft+CfcCrQA82cl59cxIZ4uyxaOpRHtWlwRVUohpyfHrsrPHv8vN/IHBj7jKaIbEzPOjXgLJm+2TJ1c09GVWQXGeEW4Qm4WU9m3jkjB+6rmaaa3upcw70cw4HeodeM01AR4nOhGD9dlnc4eRz43Q17KkvuiaGQ4WUuBgH+WaX0F6FhyKUKRZB3w+TNTmJ44UaUgY4HGOOPisJMzr/Xgnz1cXwdyJN4N+tQmKQWmxo9BFPKVfy5rv6GAjI3YmaotB/9ssHAa6ScUTSKWWNrESrNT7+DMDC0H9UkK3s00eYD8V1JMWGLsGvxYovcTc9OnLmpsUHA+hDyGGRfvOnFnJ31vBXwj3dTNCCZv55L1SmBPS8CPp9d9Ww/Fb2Nc12dOjyE3O9lJGHqS1YxqqCKim9Onjhp/zRJxPfG+b/Cj0bXn6vHosKZHMrUNEdcV8mc8dWqhFyKrJppY3LS4luV27DtjB+n40hf4BHyoK/4S10KxJl6TAtPZt866ASA7PpbsaTIny/1XmvVF8EfoZM0z7XRl1v9SD8f7rDSVpt3ZNADE77Rzrd1A1fadFldaZB0TYwchhcEUUkxa4me54MZSvTvC0cyDTg2dDQNA2k6g6+Euy+7m5iYxMznmBEfxe4afQ+K3Pdv5ie4Lj2uhaI9KazQAJBhZAJ2USMPJT0MfK22n/rgCvgjAywA2eNrhEtdHUunSGac6nHPT74I+nZCIYsia4Vy3FdcD8FTfLIATUkwY58JWyZR2yKwMRVKf9AIiOZlmTo/W7fq81BNEGdJuPdKlBLpHja+ILZm8vhraYt4bU/C70hcM6ZGu5qAlRUSIhmhxWiizgBxh5JMhbbejl9ruKS+uZpgonv+agp9YdNG7jXa9Iu512YTIGwMwHxsyWkRpASXQG72QTn1RMifNK5EAe0sKdrFK5EOg4JflK88E4s9aJ5QTCG6/10Mx6OE4aFviFnBqqwWWW04KfplqXGhbwK8FkXbFZBXR2kA+cK+JwOV6BJoWhkZ/Q1GvVRnlSC7as1i5DJwrZ5Ba9H+QO/FbznmLOerAZ1p6SDK2N5Fa8UQraEdFCOq0FLnKTtlwWdT4TLgervA0aTYY96TVdrL4ttKYBqmFAdpczb2tHO1QDz5PSYpljGdW3tUu8FUGKKg8tCbN0v7ExwyUPATQp6jK/sCHxJOxzIqLQ6HYeUF1eiHUW7bGvNNMsRdSiwBMawH4WnqIfBJ6OPFsLLn0z1TNsoUApqoMRGtzU+/40vbatqROPscKgezHmbeVQ9DraIfz1KDhieN8i8b403bePdXOLpR8pOHE7WYnT35klHq8Upxr2kNi5k3lCxj11g5P7WDAgwQ+xakYp/WpHt+WhJ/O+S1bNnMJdL8U0yQL42ThVP45Eop9Z27q8KdUZW4wNTO95VN3KdFbjjYjjx85nzopBQp6GQimF62cUorF04/NjL98gypOTXZ+KewDksm9tSHdZOZFk0sdHVGqDQeVry2gl4SXPAzUOObS3T0HsyeGvZmaRp1l6mHy9tLlsorKq7pggwLWrl7atJ2Zm/Ttyrh49RIcPTmJiZk5x24YlFOKXGOMIZVens2ODtcdt9pVYrLDLUUWMzZcPodsrID8JuSObQwgcpS2xRnITie/kRe3RSoWwSVrluDKDavwuxtWYc3iFNYsSSE7PYc1f/EPCpIyFC2dYtL1MLoSi5AdHVaOCDHNyHlqr3H4C/w5A75sFeJdngnkLXTyiyv0RilL2T1huChcHuCvXpzERzefZ3wIdLP0s5eP4Zr7HnOWhYdgbLBKKRKJIxpL0UTYMv72sNKFCotRqsTVQwJ/y4BvOElTdtt6dWpZ1U90UsjPIU9/czOuLRbS8JuvuAg3X7ER71vdHPjU2K4a+KT1ZOVU4YvGkohEuloBPoBK2J98AYyPu7k9SDOCnF3kl6EZQf9NiWl60wyp9eUQyLRgEuBSFlxrdi2QV25YiZsu32iA7iYdPTmB933W4epYg9ZT/UQ5RD2M4e7xE8N0Edsx2fJT+UIAA/5ZAh/phBsmN11+ET73sQ8Z/O01XXPfD/Gzl980L95g25czpdLLDAcg+cbGR4eV7vk6Lg7lXS8kfgQGOt3y3iuvaDiUI2q58+o+3Ll1M9Lx4ixTTSyiA5wBQlaKZKdm8UdfegS/eqMhWsEAnupv2JtyDclUkdIk8OTE6LDSLUZH8KnCygAArwA4B0BCtXNB5qMFlGjFC+iGXBoDT5gPVnZqDk899wqOjGbx+M9fx2/ePFV1oDV0qmzpFLkVyra+EvhGpSX7H8BpALTMu1OxFo4CgU7U4pbPG0XgXWFAr94wsRLxiV+8ju0P/tiyB5FoAtFoRR+PZEeHbaMWyhWpg2+UyPQxTe4rLb4UHuHv+MjlgNAi+umtmw1T0W9iYQ0sVjUVnepL3fQNCIvA2Fg8jXC4avOr2vouwS+JWHMl0knoVnxPi+idV29WMhWV2rOhG6vym3f+Cw69aR6xlkguhqZVB5JxrB1/e9gx7skb+CUJ46m1g/nc3G05ilST1QVLCQCHTLTNv7lkKrpdRG2rZoBBN5oz3dTW8+wrJ7B1zyOmVacz9YF9qhstX+BnlvUZO2GSyPCtzE4YGyJRyHnCn2jlmr7iDtSPqWjXOFENUY6X9Hu7H8fzr9aboJqmI5FcUlcdk7h9fGzY8eUSf+D39PVLgcr91LIExkYpP2cMiHFALgvQOccHe+vvNJX9KeRbsdruewHJqoxbnm+s59g7p3Hpzn/C1GzV6UZcT5xfm1Rt/UDAt+r8G1+5tpVYuqrLL/C1jX30y0/hv196A7l8Hg2WjpFNSjw0MTbseKLlC3xqKL2sT4nsP7RuMR6943JXgLUsMy2wXXTQ3bIaKxXd8MAQnjnUuBlTs/V9i7PgwQ8QeBoBM/BVd7mtAJ+eTHR83+zGS1fjgRstL2m0XiWpxoCBpya+9tSLeODHv27k/OvHR4dLj+9Zd80/+Mv7hiANn49tIuBpANqW2gC8FfiPXXP+tVu/+8OnnPrqG/zU0r59jFWfQjFrMBkN4dm/+X2kXOwonQS3+56FOFiMwvf81KJW9plXxnDDg9Wzkwu7o/jRtReQkycrgX3hPN/XvW+w+Q3QJvecWnt1uWptfavid29dj7/cusFD7e6LtNKqUWm9EfzLlnfhBx9eV1OUZcF4/7L7BptetPWtG5mlfQOSwfL04ZNXrMPeazep9MNfHgawqPcNlJ/GV3760UrxZvDpK/MB8A++xUaLQP/UlWuxqrsa0eWng7Zlid+J0ly6DFolz9Vf+k/8+liRWbauTuHvt5g9M8SyoTzvraWgVoDfKwUON3akXQssHYawqP2ltlaBbFXPI8+M4O6HnzO+Ntf8ckm5f9n936/EcvoGn6o1s/UD53mdgxPo86TtjQOx59FhfPunhxzABwq6tnbFVwYNj2erwG+y9QOz6zmDoe0enWNBz4KZ5w5B5guWzTDg7qX3f884YG8J+JmePqIeGoCKh6nl7oQFDnoZ7bkXj6IwMW0zxlXqaQn41FKjyblxRQr/fpfj3stZEXUOFtIWrKY3dmDu5TdROGX7HM7BZfd/z4jnbB34Re2vW3g9ezE1VgQ8pBUjCzoo5Y6dRO6Nk3YS+wd/9DPbB2C8rkp+HbJjMXjdf4zc/OLxicrJgjL4RCl0kE1armstVIn2j1p+LIszrx4PhnZGPzPQx2ThgDR5M/Nbb8yM3ff0q5WYPEfw53FjFNSwiIlpzL54tPUL7om/uuU6xuj3SqTpTYnnThdw0+NFD98fbOzBd279oHUf2+T4Cgpkq3rlXA4zz79m2awnU5M0HlIMWQFPrWnpLqz7+rMgRxodnGw6xyK4jQ6xKVipw/hcdSCnn/mNRVaPm6zRndvJdedovnz/lMAfbuqxdSsshF2pKpBe8s3+7wjEFIU11SaP7oWxnbf0S7Cmg3IzwaIbzz0DsPr3butkAHiSbnR46VZnlGm29X041kZ33jIIMNufQSrDEt2wahSavswKJsPXHrcem4UIL22aRHYaPB0Hj0eKFplNImuHrJ5i8ulSHtu5nV7KU3oRPHLByjEWClneQugkyiHqoE0TLaLlxDSO8PkroC2qf1O5diyKtv47rTlMGd25XSlCgQQIrVwCLW0dxOwnaKmds4IAn33hMGTB/OHS6CW94F3moapiemZ/avvfOd5EV2JeN+DrSzOgj1VSjQxuJ9BmbZ0ZOYH8cev34rRFCUQs7nVJ4AvJbbscL0i0HHwyN0Mrre8+ke/duJAwD0nrOQ9yZhIiO+rYOm2UaMNkuXZpHLHL1pt+3WrwlcxMY3mJhhFZR/cnzNN8cr6+ehMgCsgfP2T8tUsLBvyxndt3SMDxgdNyZ6Ibbe4G0CEIRQnbJLIu8sdPGRYGdA1aKo7Q6iVgEfV4erPqDfApnG9qHIVTFneuSgWdaEdfmjYWXrMkhdiSvHG343VQJdp5d8dAJqfTTy6ZuxUaBYisXwWmW1MLT1vfqbByTJGVEdm0xnKRc+QRuqi88kKAawb1FE5a+1+MAbJZcJ1kaSn4JMyJnbfuZZBGOLhTipy/EixsraVWiy6dAM0+/5qlhUHxltFL1jo1b015sSSYHoaYGnekHarE1NSMhIqmZso6MCCxbZeSUitlKvdG1cXgaG5aXMlxdscCdiae51FxKEiDUDY57UAvVXMksW1X6+9kEf3k9QI9iGT7w8HaohRCPYusu8QZeLL5Ph0dQtAGxS4R9SgAENQ4ONYrJZ5M3rCrdVdBG1ssLsBsr9UaQNvvyHr7uEyeaL6ao6L5sQ+c53vhJT6noz4xNQcxlzMOcmhAadfqd1EH5N2Jbff4v4HuNMyGf5/zfkhZDT9mbFgKMRS7eO2DoAeTLJJZWJ8j56fiiG5a4ySW5fcEOs2uqt+lOWtkw0pb14FT4xrna2PXf87xMhzV44rznRqu/X5icM9XIcRfW69+5t5N0kjypzQmJwvDSTYyXc+MVDdXVF+tptOA0ODYmZBObdCPGCe23aMcBx8Y+JODuzdJgV/ZCWy14SprKFECJaIEfUW3ozfRqq1aLyOBHlq73AC5MdH+gkdCPqhHnXIC1XyqfGLw8/8HIS92q/3OGqaegwAl/7oxiIsSCJ+3wvMgOrWa47nu7uv3moaDm5UNTPOpscnB3XdIgW960X6njqp+b8yi46eM7OHe5arF3Odjcn/i4/c4ejJrKw4UfEP7/3E33Rarv6jaIMHZcJ7rZqEtdz9w8JW0vwNPt+qmhgetD5zzywI6cj+ZXfFQMUKt81JW47xP1bxsK+0Y3P/w7qtkHvZevg4NJ1H13bd9wa1tcGJwzzchxB22it1xgVTu7PrGvgfO+fUD4GB6Ev24fAdnHlkqC47+xPW7mi66qcrUVvAnH96zTObpXABNv7VYx4WdMQC3J7btcnxZxNbMVh2lVuUr8f+/dfIA+OH5ti+4jQOntAATBbXxMrOycnk0K+d1wW0agOLul1yvthRkvJ8wj9c8W2HPWw1sWznfYgY4UhD5Xg0n3DyFnBhyMzyU+Pguxzd0lGdQkC5lVSEM76fEU3a+/0pdNAuiIaXnGFXbV8hHQZc7/C6uC4p2aoUhK0gIfJ0J8ScKYBjgc5oFCm9iKtVnmUm+AM4G/JiTC8rasRNmcvDz26SQ9Hi9tSOutgK6y0VmaQAX58iiyfPcPjcuYrcDPa+cbyassRcQuBdCbHdcjBsHgmaCxsE05u5mekFA0lvKBUmvgP9Ci4U/4cVX0/HglzvgeRAaDWm75wHyNRHInD3DgK8lB76o8MsFbmE2z7/gNN9MzImH934WQnwMQv5Oa7pdqeWk5PynHOKLyYF7X2xx3Y7VdQT4dbMB7E9ZQfRLKS9VXhuqMMyAs9cA9hPGxU+St9570BGhADN0FPhmOEz9YM9HJHi3YPIDpuac5Ic5Cq/noR9K37z7UIBYuq6648F33eMFVOA98OdxMP4fKEtO2/U94iMAAAAASUVORK5CYII=","e":1},{"id":"image_9","w":35,"h":28,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAcCAYAAADr9QYhAAADMElEQVRYR8WXz08TQRTHv7Pd7bblR8ESD1wENUaJiU3swZscTPQkG2+UiyQevNmbR+tfIJ6NUQ/WeHL9C2xiCE2AUBItSAK2iSkKaUspzdofO2OmsG23hXYpFebUzbwfn743b94bsqYoXkoxgy6uxFb6slYo9Rgme13y4zuz4Y/tXJDVCWUHgLud4HH2135tY08rVFVcsrSjLM0NtrPBYZghJPT1gUgi6G4OrFxup3vk/olhbEMe2Ny1AJWTm6Ca1hFQI4xTlspasTQLSuJMQLgoldTpaJRnxLSqkbGPjgCCUN3UMxno6Uy3YKAVSjVbBFkGEpj6Pv+23kEN5tJFk+P/CmN4YuSdf2X+ofF5WjD7aQK8YOZiYcDzqdhCkAOdCoxRTW+83gF7UQwQ4JkpPVQcnVyNxE8VxgAIXfMFQPCiBsRe+mOLgTOB4RChsZtRgNw4AEr4YwsjZwbzfswXrE+XP7ZAzgzmw3XfOKP4YqTqTGFaRkYaHgZxOqpnqry1DZrLdeXSO6w3hcZ8cQAXGs/MfqMUBIhDHhBRAs3noWezHYFwpeX1JHRKj2yUjVEBjGq6r4RBcLtjzw2KHILD1K/6e0Yu2oIAeVLdJ8gSXfTu3zNdhtnZ07CxmTLB9DjsxXyhOAfW/KcZIdNGjyKrijIDhhrpCUPU2LG5uV6nbJpvqq2prhVU2sEPRVEYw6cTMlTUi6UyvsV/N5mS7SIKRdN8lABjAf/KompqC/yjW9PeejKFbN48A0mijZZ0/Sv3wxjChLFoI0S1a/MfK4oSJMzcvI4bqdRuHok/zfOPp7/39d1I+JEVe4QL/VSUgQJDtK7urehWZfjgxM9KfTnzTYdd1B5EIy6rxiowfFVeCQxLVhUNuZxWwEYy1QRiEwTmcbsmrbwKTGnikfkLeAkj9wD21AoQZQyZnIb0bv5QcY+7JzTY73rlAKKjqto07x6mRCopouAXn9HOrbBYl2FYlgWMWwHi94wKhgnr1juQJPh8VVWVdprk4KyEu/2Qq3OcFQjGr6gqL5CWq3KAOZAOtCTXCqVblLLzuk7PcR2bTUgLAtlyylKklQcboFoB4Tb+ASLIATv6FDmXAAAAAElFTkSuQmCC","e":1},{"id":"image_10","w":90,"h":79,"u":"","p":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABPCAYAAABrs9IqAAACTklEQVR4Xu3cvYoTYQCF4fOtP9EmM8HsCtuoyLqVMHgDbrGVoJcgewfaW7hegV6Cna0gbh37LUarxc5GJJslE/GHYNaRiSj+u5PMd5q8qQJJvpd5OASSYsJwOEzLw8nDz1IqHo0KLElFOHZ8q9PpFOFgMHgglbcaLXDYd4FSetbtLm+EwWC/F6Sr1SvJ/Zv6dPGKPty4reRgS5MTa3rfvqO7r9/o5XisRxfOQTiDwJnucvgJurX7VJPVSzpcXVPr4870yPHpa3oyejt9fj1pz5DhI79BQxJHYAq9fnlzr9U6eSpOglMrgRe7O+dDspKVcMQVGPXzAHRc4+npQBuQgTYhAw20UcCU4jsaaJOAKcOigTYJmDIsGmiTgCnDooE2CZgyLBpok4Apw6KBNgmYMiwaaJOAKcOigTYJmDIsGmiTgCnDooE2CZgyLBpok4Apw6KBNgmYMiwaaJOAKcOigTYJmDIsGmiTgCnDooE2CZgyLBpok4Apw6KBNgmYMiwaaJOAKcOigTYJmDIsGmiTgCnDooE2CZgyLBpok4Apw6KBNgmYMiwaaJOAKfNt0UNxp92Y5K9G/Xx6g8E9SesxS4t8dpDuFf18OyRns57Kr3fbrfGYKOi5pHc1PrNwbw2lehVydeGzQI9CqY1iP88XTm6OC64LDfKM2HWgQZ4Ruc5XB8hzIB8VGuQ5kY8CDXIDyP+DBrkh5H9Bg9wg8t+gQW4Y+U/QIEdA/hUa5EjIP0Jn/KyOqFz919FeyR4vldrmv4vI0GmapUWRF3EznP4FiECWXypnWgEAAAAASUVORK5CYII=","e":1},{"id":"image_11","w":381,"h":297,"u":"","p":"data:image/png;base64,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","e":1},{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 24","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[68.063,3.15],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.662546255074,0.382559922162,0.969393382353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[109.282,-120.425],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":86.9999924698869,"op":119.999989613637,"st":29.9999974034093,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 23","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[99.229,99.536,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[45.954,3.102],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.382559922162,0.770204491709,0.969393382353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[98.977,-113.699],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":88.9999922967808,"op":119.999989613637,"st":31.9999972303032,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 22","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100.437,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[20.174,2.707],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.969393382353,0.879534553079,0.382559922162,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[133.462,-113.021],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":88.9999922967808,"op":119.999989613637,"st":31.9999972303032,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 21","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[56.143,3.64],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.662745098039,0.384313755409,0.968627510819,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[103.697,-104.68],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":91.9999920371218,"op":119.999989613637,"st":34.9999969706441,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 20","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,199.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[62.165,3.358],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.384313755409,0.768627510819,0.968627510819,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[106.832,-96.321],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":94.9999917774627,"op":119.999989613637,"st":37.9999967109851,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 19","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100.276,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[22.938,2.995],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.968627510819,0.878431432387,0.384313755409,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87.969,-89.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":97.9999915178036,"op":119.999989613637,"st":40.999996451326,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 18","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[33.727,3.095],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.662745098039,0.384313755409,0.968627510819,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[119.363,-89.203],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":97.9999915178036,"op":119.999989613637,"st":40.999996451326,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Shape Layer 17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[41.743,2.937],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.384313755409,0.768627510819,0.968627510819,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[97.622,-80.782],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":100.999991258145,"op":119.999989613637,"st":43.9999961916669,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 16","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[68.063,3.15],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.662546255074,0.382559922162,0.969393382353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[109.282,-120.425],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":42.99999627822,"op":81.999992902652,"st":-7.9999993075758,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Shape Layer 15","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[99.229,99.536,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[45.954,3.102],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.382559922162,0.770204491709,0.969393382353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[98.977,-113.699],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44.9999961051139,"op":81.999992902652,"st":-5.99999948068185,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Shape Layer 14","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100.437,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[20.174,2.707],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.969393382353,0.879534553079,0.382559922162,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[133.462,-113.021],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":44.9999961051139,"op":81.999992902652,"st":-5.99999948068185,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Shape Layer 13","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[56.143,3.64],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.662745098039,0.384313755409,0.968627510819,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[103.697,-104.68],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":47.9999958454548,"op":81.999992902652,"st":-2.99999974034093,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Shape Layer 12","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,199.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[62.165,3.358],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.384313755409,0.768627510819,0.968627510819,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[106.832,-96.321],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":50.9999955857958,"op":81.999992902652,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Shape Layer 11","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100.276,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[22.938,2.995],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.968627510819,0.878431432387,0.384313755409,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87.969,-89.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":53.9999953261367,"op":81.999992902652,"st":2.99999974034093,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Shape Layer 10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[33.727,3.095],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.662745098039,0.384313755409,0.968627510819,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[119.363,-89.203],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":53.9999953261367,"op":81.999992902652,"st":2.99999974034093,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Shape Layer 9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[41.743,2.937],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.384313755409,0.768627510819,0.968627510819,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[97.622,-80.782],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":56.9999950664776,"op":81.999992902652,"st":5.99999948068185,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Shape Layer 8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[41.743,2.937],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.384313755409,0.768627510819,0.968627510819,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[97.622,-80.782],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":20.9999981823865,"op":37.9999967109851,"st":-29.9999974034093,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"Shape Layer 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[33.727,3.095],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.662745098039,0.384313755409,0.968627510819,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[119.363,-89.203],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":17.9999984420456,"op":37.9999967109851,"st":-32.9999971437502,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"Shape Layer 6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100.276,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[22.938,2.995],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.968627510819,0.878431432387,0.384313755409,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87.969,-89.002],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":17.9999984420456,"op":37.9999967109851,"st":-32.9999971437502,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,199.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[62.165,3.358],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.384313755409,0.768627510819,0.968627510819,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[106.832,-96.321],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":14.9999987017046,"op":37.9999967109851,"st":-35.9999968840911,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[56.143,3.64],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.662745098039,0.384313755409,0.968627510819,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[103.697,-104.68],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":11.9999989613637,"op":37.9999967109851,"st":-38.9999966244321,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100.437,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[20.174,2.707],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.969393382353,0.879534553079,0.382559922162,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[133.462,-113.021],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":8.99999922102278,"op":37.9999967109851,"st":-41.999996364773,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[99.229,99.536,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[45.954,3.102],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.382559922162,0.770204491709,0.969393382353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[98.977,-113.699],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":8.99999922102278,"op":37.9999967109851,"st":-41.999996364773,"bm":0},{"ddd":0,"ind":24,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[68.063,3.15],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.005913000013,0.005768999866,0.005768999866,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.662546255074,0.382559922162,0.969393382353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[109.282,-120.425],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":6.99999939412883,"op":37.9999967109851,"st":-43.9999961916669,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":2,"nm":"light2","refId":"image_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[46]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":31,"s":[63]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":42,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":53,"s":[25]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":63,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":73,"s":[73]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":83,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":93,"s":[52]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":103,"s":[100]},{"t":111.999990306061,"s":[51]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[198,179.5,0],"ix":2},"a":{"a":0,"k":[234.5,142,0],"ix":1},"s":{"a":0,"k":[33.902,33.902,100],"ix":6}},"ao":0,"ip":0,"op":119.999989613637,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":2,"nm":"light","refId":"image_1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[45]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":31,"s":[39]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":42,"s":[88]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":53,"s":[56]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":63,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":73,"s":[57]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":83,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":93,"s":[23]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":103,"s":[100]},{"t":111.999990306061,"s":[59]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[199,282,0],"ix":2},"a":{"a":0,"k":[126,126.5,0],"ix":1},"s":{"a":0,"k":[32.323,32.323,100],"ix":6}},"ao":0,"ip":0,"op":119.999989613637,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":2,"nm":"flower","refId":"image_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[78.684,318.243,0],"ix":2},"a":{"a":0,"k":[19.623,46.1,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":119.999989613637,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":2,"nm":"finger","refId":"image_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0.88]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":5,"s":[12.307]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":8,"s":[-0.007]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":17,"s":[12.307]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[-0.007]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":29,"s":[12.307]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":32,"s":[-0.007]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":42,"s":[12.307]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":45,"s":[-0.007]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":55,"s":[12.307]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":58,"s":[-0.007]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":69,"s":[12.307]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":72,"s":[-0.007]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":83,"s":[12.307]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":86,"s":[-0.007]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":98,"s":[12.307]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":101,"s":[-0.007]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":111,"s":[12.307]},{"t":113.999990132955,"s":[-0.007]}],"ix":10},"p":{"a":0,"k":[235.192,165.108,0],"ix":2},"a":{"a":0,"k":[7.92,23.571,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":119.999989613637,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":2,"nm":"leftbrowe","parent":9,"refId":"image_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[36.704,62.945,0],"to":[0,0.833,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":5,"s":[36.704,67.945,0],"to":[0,0,0],"ti":[0,0.833,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":8,"s":[36.704,62.945,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":23,"s":[36.704,62.945,0],"to":[0,0.833,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":28,"s":[36.704,67.945,0],"to":[0,0,0],"ti":[0,0.833,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":32,"s":[36.704,62.945,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":53,"s":[36.704,62.945,0],"to":[0,0.833,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":57,"s":[36.704,67.945,0],"to":[0,0,0],"ti":[0,0.833,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":61,"s":[36.704,62.945,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":79,"s":[36.704,62.945,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":83,"s":[36.704,67.945,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":87,"s":[36.704,62.945,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":104,"s":[36.704,62.945,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":107,"s":[36.704,67.945,0],"to":[0,0,0],"ti":[0,0,0]},{"t":110.999990392614,"s":[36.704,62.945,0]}],"ix":2},"a":{"a":0,"k":[5.897,2.794,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":119.999989613637,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":2,"nm":"rightbrowe","parent":9,"refId":"image_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[70.486,54.976,0],"to":[0,0.833,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":5,"s":[70.486,59.976,0],"to":[0,0,0],"ti":[0,0.833,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":8,"s":[70.486,54.976,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":23,"s":[70.486,54.976,0],"to":[0,0.833,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":28,"s":[70.486,59.976,0],"to":[0,0,0],"ti":[0,0.833,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":32,"s":[70.486,54.976,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":53,"s":[70.486,54.976,0],"to":[0,0.833,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":57,"s":[70.486,59.976,0],"to":[0,0,0],"ti":[0,0.833,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":61,"s":[70.486,54.976,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":79,"s":[70.486,54.976,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":83,"s":[70.486,59.976,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":87,"s":[70.486,54.976,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":104,"s":[70.486,54.976,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":107,"s":[70.486,59.976,0],"to":[0,0,0],"ti":[0,0,0]},{"t":110.999990392614,"s":[70.486,54.976,0]}],"ix":2},"a":{"a":0,"k":[5.897,2.794,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":119.999989613637,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":2,"nm":"lefteye","parent":9,"refId":"image_6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[39.674,73.055,0],"ix":2},"a":{"a":0,"k":[3.414,4.87,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":5,"s":[100,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":23,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":28,"s":[100,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":32,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":53,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":57,"s":[100,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":61,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":79,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":83,"s":[100,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":87,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":104,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":107,"s":[100,0,100]},{"t":110.999990392614,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":0,"op":119.999989613637,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":2,"nm":"right eye","parent":9,"refId":"image_7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[71.895,68.173,0],"ix":2},"a":{"a":0,"k":[3.414,4.869,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":5,"s":[100,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":23,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":28,"s":[100,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":32,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":53,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":57,"s":[100,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":61,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":79,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":83,"s":[100,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":87,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":104,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":107,"s":[100,0,100]},{"t":110.999990392614,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":0,"op":119.999989613637,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":2,"nm":"head","refId":"image_8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1.216]},"o":{"x":[0.333],"y":[0]},"t":5,"s":[7.186]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[-0.035]},"t":16,"s":[5.937]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[0.992]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":32,"s":[0.573]},{"i":{"x":[0.667],"y":[1.236]},"o":{"x":[0.333],"y":[0]},"t":42,"s":[7.868]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[-0.136]},"t":52,"s":[4.11]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":59,"s":[-0.463]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":66,"s":[5.12]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":76,"s":[1.555]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":82,"s":[5.892]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":91,"s":[2.622]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":97,"s":[5.811]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":106,"s":[3.98]},{"t":112.999990219508,"s":[0.794]}],"ix":10},"p":{"a":0,"k":[201.124,162.967,0],"ix":2},"a":{"a":0,"k":[55.113,109.357,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":119.999989613637,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":2,"nm":"cup","refId":"image_9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[324.484,290.727,0],"ix":2},"a":{"a":0,"k":[34.103,-1.214,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":119.999989613637,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"codescroll","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,200,0],"ix":2},"a":{"a":0,"k":[200,200,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":400,"h":400,"ip":0,"op":119.999989613637,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":2,"nm":"code","refId":"image_10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[266.532,145.04,0],"ix":2},"a":{"a":0,"k":[0.553,79.236,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1.19,1.19,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[0,0,100]},{"t":4.99999956723488,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":0,"op":119.999989613637,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":2,"nm":"backgorund","refId":"image_11","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[202.825,191.457,0],"ix":2},"a":{"a":0,"k":[190.393,148.102,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":119.999989613637,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/base/activity/BaseActivity.kt b/BaseMvvm/src/main/java/com/holo/architecture/base/activity/BaseActivity.kt new file mode 100644 index 0000000..0f6d430 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/base/activity/BaseActivity.kt @@ -0,0 +1,182 @@ +package com.holo.architecture.base.activity + +import android.graphics.Color +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.viewbinding.ViewBinding +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.customview.customView +import com.gyf.immersionbar.ktx.immersionBar +import com.gyf.immersionbar.ktx.navigationBarHeight +import com.holo.architecture.R +import com.holo.architecture.ext.showErrToast +import com.holo.architecture.logger.KLog +import com.holo.architecture.network.ListState +import com.holo.architecture.network.NetworkStateManager +import com.holo.architecture.network.State +import com.holo.architecture.utils.ViewBindingUtil +import com.holo.architecture.utils.netstatus.NetType +import com.holo.architecture.widget.TransBehavior +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +/** + * @Desc Activity基类 + * @Author holo + * @Date 2022/1/7 + */ +abstract class BaseActivity : AppCompatActivity() { + + protected lateinit var binding: VB + private var loadingDialog: MaterialDialog? = null + private var dialogJob: Job? = null + + /** + * 供子类初始化view + * @param savedInstanceState Bundle? + */ + abstract fun initView(savedInstanceState: Bundle?) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ViewBindingUtil.inflateWithGeneric(this, layoutInflater) + setContentView(binding.root) + immersionBar { + transparentBar() + titleBar(R.id.title_bar) + statusBarDarkFont(true) + navigationBarColor(R.color.transparent) + navigationBarDarkIcon(true) + fullScreen(true) + } + if (bottomPaddingView() != null) { + bottomPaddingView()!!.setPadding(0, 0, 0, navigationBarHeight) + } else { + binding.root.setPadding(0, 0, 0, navigationBarHeight) + } + + initView(savedInstanceState) + observeViewModel() + NetworkStateManager.mNetworkStateCallback.observe(this) { netType -> + onNetworkStateChanged(netType) + } + initData() + } + + open fun initData() {} + + /** + * 虚拟导航栏是否需要自动padding + * @return Boolean + */ + open fun bottomPaddingView(): View? = null + + /** + * 网络变化监听 子类重写 + */ + open fun onNetworkStateChanged(netType: @NetType String) {} + + /** + * 创建LiveData数据观察者 + */ + abstract fun observeViewModel() + + protected fun observer(flow: Flow>, showLoading: Boolean = true, success: (t: T) -> Unit, error: ((code: Int) -> Unit)? = null) { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + flow.collect { state -> + when (state) { + is State.Loading -> if (showLoading) showLoading() + is State.Success -> { + hideLoading() + success.invoke(state.data) + } + is State.Error -> { + hideLoading() + showErrToast(state.err) + error?.invoke(state.err.errCode) + } + } + } + } + } + } + + protected fun observer(flow: Flow>, loading: () -> Unit, success: (t: T) -> Unit, error: (() -> Unit)? = null) { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + flow.collect { state -> + when (state) { + is State.Loading -> loading.invoke() + is State.Success -> { + hideLoading() + success.invoke(state.data) + } + is State.Error -> { + hideLoading() + showErrToast(state.err) + error?.invoke() + } + } + } + } + } + } + + protected fun observer(flow: Flow>, action: (t: T?) -> Unit) { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + flow.collect { state -> + when (state) { + is State.Success -> action.invoke(state.data) + is State.Error -> action.invoke(null) + is State.Loading -> {} + } + } + } + } + } + + protected fun observerList(flow: Flow>, action: (st: ListState) -> Unit) { + lifecycleScope.launchWhenCreated { + repeatOnLifecycle(Lifecycle.State.STARTED) { + flow.collect { state -> + action.invoke(state) + } + } + } + } + + fun showLoading() { + if (loadingDialog == null) { + loadingDialog = MaterialDialog(this, TransBehavior) + .customView(R.layout.dialog_loading) + .cancelOnTouchOutside(false) + .cancelable(false) + } + if (loadingDialog?.isShowing != true) { + dialogJob?.cancel() + dialogJob = lifecycleScope.launch { + delay(1000) + loadingDialog?.show() + delay(5000) + KLog.d("Holo", "5s超时隐藏") + hideLoading() + } + } + } + + fun hideLoading() { + dialogJob?.cancel() + dialogJob = null + if (isDestroyed || isFinishing) return + loadingDialog?.dismiss() + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/base/bean/BaseBean.kt b/BaseMvvm/src/main/java/com/holo/architecture/base/bean/BaseBean.kt new file mode 100644 index 0000000..79bd96a --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/base/bean/BaseBean.kt @@ -0,0 +1,20 @@ +package com.holo.architecture.base.bean + +import com.google.gson.Gson + +/** + * + * + * @Author holo + * @Date 2022/1/15 + */ +open class BaseBean { + + /** + * 重写toString方法,对象直接打印json串 + * @return String + */ + override fun toString(): String { + return Gson().toJson(this) + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/base/fragment/BaseFragment.kt b/BaseMvvm/src/main/java/com/holo/architecture/base/fragment/BaseFragment.kt new file mode 100644 index 0000000..e629f62 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/base/fragment/BaseFragment.kt @@ -0,0 +1,180 @@ +package com.holo.architecture.base.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.viewbinding.ViewBinding +import com.holo.architecture.base.activity.BaseActivity +import com.holo.architecture.ext.showErrToast +import com.holo.architecture.network.ListState +import com.holo.architecture.network.State +import com.holo.architecture.utils.ViewBindingUtil +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +/** + * @Desc + * @Author holo + * @Date 2022/1/7 + */ +abstract class BaseFragment : Fragment() { + + //是否第一次加载 + protected var isFirst: Boolean = true + + private var _binding: VB? = null + protected val binding: VB get() = _binding!! + protected var bindNotNull = false + + /** + * 供子类初始化view + * @param savedInstanceState Bundle? + */ + abstract fun initView(savedInstanceState: Bundle?) + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + _binding = ViewBindingUtil.inflateWithGeneric(this, inflater, container, false) + bindNotNull = true + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + isFirst = true + initView(savedInstanceState) + initData() + observeViewModel() + } + + override fun onDestroyView() { + super.onDestroyView() + bindNotNull = false + _binding = null + } + + /** + * 创建观察者 + */ + abstract fun observeViewModel() + + protected fun observer(flow: Flow>, showLoading: Boolean = true, success: (t: T) -> Unit, error: (() -> Unit)? = null) { + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + flow.collect { state -> + when (state) { + is State.Loading -> if (showLoading) showLoading() + is State.Success -> { + hideLoading() + success.invoke(state.data) + } + is State.Error -> { + hideLoading() + showErrToast(state.err) + error?.invoke() + } + } + } + } + } + } + + protected fun observer(flow: Flow>, loading: () -> Unit, success: (t: T) -> Unit, error: (() -> Unit)? = null) { + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + flow.collect { state -> + when (state) { + is State.Loading -> loading.invoke() + is State.Success -> { + hideLoading() + success.invoke(state.data) + } + is State.Error -> { + hideLoading() + showErrToast(state.err) + error?.invoke() + } + } + } + } + } + } + + protected fun observer(flow: Flow>, action: (t: T?) -> Unit) { + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + flow.collect { state -> + when (state) { + is State.Success -> action.invoke(state.data) + is State.Error -> action.invoke(null) + is State.Loading -> {} + } + } + } + } + } + + protected fun observerObj(flow: Flow, action: (t: T?) -> Unit) { + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + flow.collect { action.invoke(it) } + } + } + } + + protected fun observerList(flow: Flow>, action: (st: ListState) -> Unit) { + lifecycleScope.launchWhenCreated { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + flow.collect { state -> + action.invoke(state) + } + } + } + } + + private fun showLoading() { + if (activity is BaseActivity<*>) { + (activity as BaseActivity<*>).showLoading() + } + } + + private fun hideLoading() { + if (activity is BaseActivity<*>) { + (activity as BaseActivity<*>).hideLoading() + } + } + + /** + * Fragment执行onViewCreated后触发的方法 + */ + open fun initData() {} + + /** + * 懒加载 + */ + open fun lazyLoadData() {} + + override fun onResume() { + super.onResume() + onVisible() + } + + /** + * 是否需要懒加载 + */ + private fun onVisible() { + if (lifecycle.currentState == Lifecycle.State.STARTED && isFirst) { + //延迟加载0.12秒加载 避免fragment跳转动画和渲染ui同时进行,出现些微的小卡顿 + view?.postDelayed({ + lazyLoadData() + isFirst = false + }, 120) + } + } + +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/base/repository/IRepository.kt b/BaseMvvm/src/main/java/com/holo/architecture/base/repository/IRepository.kt new file mode 100644 index 0000000..5aa6d80 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/base/repository/IRepository.kt @@ -0,0 +1,16 @@ +package com.holo.architecture.base.repository + +/** + * @Desc + * @Author holo + * @Date 2022/1/6 + */ + +/** + * Repository 是用于配置基本存储库类的接口。 + */ +interface IRepository + +interface IRemoteData + +interface ILocalData \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/base/viewmodel/BaseViewModel.kt b/BaseMvvm/src/main/java/com/holo/architecture/base/viewmodel/BaseViewModel.kt new file mode 100644 index 0000000..b3acc3d --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/base/viewmodel/BaseViewModel.kt @@ -0,0 +1,16 @@ +package com.holo.architecture.base.viewmodel + +import androidx.lifecycle.ViewModel + +/** + * @Desc + * @Author holo + * @Date 2022/1/6 + */ +open class BaseViewModel : ViewModel() { + + /** + * 分页请求,页码 + */ + protected var pageNo: Int = 1 +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/ext/BaseCommonExt.kt b/BaseMvvm/src/main/java/com/holo/architecture/ext/BaseCommonExt.kt new file mode 100644 index 0000000..049a6f5 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/ext/BaseCommonExt.kt @@ -0,0 +1,110 @@ +package com.holo.architecture.ext + +import android.content.Context +import android.content.res.Resources +import android.text.Html +import android.text.Spanned +import android.util.TypedValue +import android.widget.Toast +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import com.holo.architecture.BuildConfig +import com.holo.architecture.network.AppException + +/** + * @Desc 一些基础的扩展函数 + * @Author holo + * @Date 2022/1/10 + */ + +/** + * dp值转换成px + * + * @return + */ +fun Float.dp2px(): Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics) + +/** + * sp值转换成px + * + * @return + */ +fun Float.sp2px(): Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics) + +/** + * dp值转换成px + * + * @return + */ +fun Int.dp2px(): Int = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics).toInt() + +/** + * sp值转换成px + * + * @return + */ +fun Int.sp2px(): Int = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this.toFloat(), Resources.getSystem().displayMetrics).toInt() + +/** + * 显示toast + * @receiver Context + * @param message CharSequence? + */ +fun Context.showToast(message: CharSequence?) { + message?.let { + Toast.makeText(this, it, Toast.LENGTH_SHORT).show() + } +} + +/** + * 显示toast + * @receiver Context + * @param message Int + */ +fun Context.showToast(@StringRes message: Int) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() +} + +fun Context.showErrToast(err: AppException) { + val msg = if (BuildConfig.DEBUG) "${err.errorMsg} Code:[${err.errCode}]" else err.errorMsg + Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() +} + +/** + * Fragment中显示toast + * @receiver Fragment + * @param message CharSequence? + */ +fun Fragment.showToast(message: CharSequence?) { + activity?.run { + message?.let { + Toast.makeText(this, it, Toast.LENGTH_SHORT).show() + } + } +} + +/** + * Fragment中显示toast + * @receiver Fragment + * @param message Int + */ +fun Fragment.showToast(@StringRes message: Int) { + activity?.run { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() + } +} + +fun Fragment.showErrToast(err: AppException) { + activity?.run { + val msg = if (BuildConfig.DEBUG) "${err.errorMsg} Code:[${err.errCode}]" else err.errorMsg + Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() + } +} + +fun String.toHtml(flag: Int = Html.FROM_HTML_MODE_LEGACY): Spanned { + return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + Html.fromHtml(this, flag) + } else { + Html.fromHtml(this) + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/ext/GsonExt.kt b/BaseMvvm/src/main/java/com/holo/architecture/ext/GsonExt.kt new file mode 100644 index 0000000..5de9d52 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/ext/GsonExt.kt @@ -0,0 +1,16 @@ +package com.holo.architecture.ext + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +/** + * Gson拓展 + * + * @Author holo + * @Date 2022/3/4 + */ + +fun Gson.typedToJson(src: T): String = toJson(src) + +inline fun Gson.fromJson(json: String): T = + fromJson(json, object : TypeToken() {}.type) diff --git a/BaseMvvm/src/main/java/com/holo/architecture/ext/Intents.kt b/BaseMvvm/src/main/java/com/holo/architecture/ext/Intents.kt new file mode 100644 index 0000000..9d096ca --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/ext/Intents.kt @@ -0,0 +1,141 @@ +package com.holo.architecture.ext + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import com.holo.architecture.network.AppException +import java.io.Serializable + +/** + * 启动新的Activity,同[Activity.startActivity] + * + * 请注意,如果从 [android.app.Activity] 上下文外部调用此方法,则 Intent 必须包含 [Intent.FLAG_ACTIVITY_NEW_TASK] 启动标志。 + * 这是因为,如果不是从现有的 Activity 开始,就没有现有的任务可以放置新的 Activity,因此需要将其放置在自己的单独任务中。 + * + * ``` + * 示例: + * //不携带参数 + * startActivity() + * + * //携带参数(可连续多个键值对) + * startActivity("Key" to "Value") + * ``` + * + * @param params extras键值对 + * @receiver Context + * @param params Array> + */ +inline fun Context.startActivity(vararg params: Pair) = + this.startActivity(Intent(this, T::class.java).putExtras(params)) + + +/** + * Fragment启动新的Activity,同[Activity.startActivity] + * + * ``` + * 示例: + * //不携带参数 + * startActivity() + * + * //携带参数(可连续多个键值对) + * startActivity("Key" to "Value") + * ``` + * + * @param params extras键值对 + * @receiver Context + * @param params Array> + */ +inline fun Fragment.startActivity(vararg params: Pair) = + this.startActivity(Intent(this.activity, T::class.java).putExtras(params)) + +/** + * 启动新的Activity并返回结果 + * + * ``` + * 示例: + * //不携带参数 + * startActivityForResult{resultCode, result -> } + * + * //携带参数(可连续多个键值对) + * startActivityForResult("Key" to "Value"){resultCode, result -> } + * ``` + * + * @param params extras键值对 + * @param callback 目标[Activity.setResult]之后回调 + * @receiver Context + * @param params Array> + */ +inline fun AppCompatActivity.startActivityForResult( + vararg params: Pair, + crossinline callback: ((resultCode: Int, result: Intent?) -> Unit) +) = this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + callback.invoke(it.resultCode, it.data) +}.launch(Intent(this, T::class.java).putExtras(params)) + + +/** + * Fragment启动新的Activity并返回结果 + * + * ``` + * 示例: + * //不携带参数 + * startActivityForResult{resultCode, result -> } + * + * //携带参数(可连续多个键值对) + * startActivityForResult("Key" to "Value"){resultCode, result -> } + * ``` + * + * @param params extras键值对 + * @param callback 目标[Activity.setResult]之后回调 + * @receiver Context + * @param params Array> + */ +inline fun Fragment.startActivityForResult( + vararg params: Pair, + crossinline callback: ((resultCode: Int, result: Intent?) -> Unit) +) = this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + callback.invoke(it.resultCode, it.data) +}.launch(Intent(this.activity, T::class.java).putExtras(params)) + + +fun Intent.putExtras(params: Array>): Intent { + if (params.isEmpty()) return this + params.forEach { + when (val value = it.second) { + null -> putExtra(it.first, null as Serializable?) + is Int -> putExtra(it.first, value) + is Long -> putExtra(it.first, value) + is CharSequence -> putExtra(it.first, value) + is String -> putExtra(it.first, value) + is Float -> putExtra(it.first, value) + is Double -> putExtra(it.first, value) + is Char -> putExtra(it.first, value) + is Short -> putExtra(it.first, value) + is Boolean -> putExtra(it.first, value) + is Serializable -> putExtra(it.first, value) + is Bundle -> putExtra(it.first, value) + is Parcelable -> putExtra(it.first, value) + is Array<*> -> when { + value.isArrayOf() -> putExtra(it.first, value) + value.isArrayOf() -> putExtra(it.first, value) + value.isArrayOf() -> putExtra(it.first, value) + else -> throw AppException("Intent extra ${it.first} has wrong type ${value.javaClass.name}") + } + is IntArray -> putExtra(it.first, value) + is LongArray -> putExtra(it.first, value) + is FloatArray -> putExtra(it.first, value) + is DoubleArray -> putExtra(it.first, value) + is CharArray -> putExtra(it.first, value) + is ShortArray -> putExtra(it.first, value) + is BooleanArray -> putExtra(it.first, value) + else -> throw AppException("Intent extra ${it.first} has wrong type ${value.javaClass.name}") + } + return@forEach + } + return this +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/ext/LogExt.kt b/BaseMvvm/src/main/java/com/holo/architecture/ext/LogExt.kt new file mode 100644 index 0000000..d2ec4b7 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/ext/LogExt.kt @@ -0,0 +1,16 @@ +package com.holo.architecture.ext + +import com.holo.architecture.logger.KLog + +/** + * @Desc + * @Author holo + * @Date 2022/1/10 + */ + +fun String.logV(tag: String? = null) = KLog.v(tag, this) +fun String.logD(tag: String? = null) = KLog.d(tag, this) +fun String.logI(tag: String? = null) = KLog.i(tag, this) +fun String.logW(tag: String? = null) = KLog.w(tag, this) +fun String.logE(tag: String? = null) = KLog.e(tag, this) +fun String.logA(tag: String? = null) = KLog.a(tag, this) \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/ext/NavigationExt.kt b/BaseMvvm/src/main/java/com/holo/architecture/ext/NavigationExt.kt new file mode 100644 index 0000000..c7965f6 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/ext/NavigationExt.kt @@ -0,0 +1,42 @@ +package com.holo.architecture.ext + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.navigation.NavController +import androidx.navigation.Navigation +import androidx.navigation.fragment.NavHostFragment + +/** + * + * + * @Author holo + * @Date 2022/2/18 + */ +fun Fragment.nav(): NavController { + return NavHostFragment.findNavController(this) +} + +fun nav(view: View): NavController { + return Navigation.findNavController(view) +} + +var lastNavTime = 0L + +/** + * 防止短时间内多次快速跳转Fragment出现的bug + * @param resId 跳转的action Id + * @param bundle 传递的参数 + * @param interval 多少毫秒内不可重复点击 默认0.5秒 + */ +fun NavController.navigateAction(resId: Int, bundle: Bundle? = null, interval: Long = 500) { + val currentTime = System.currentTimeMillis() + if (currentTime >= lastNavTime + interval) { + lastNavTime = currentTime + try { + navigate(resId, bundle) + }catch (ignore:Exception){ + //防止出现 当 fragment 中 action 的 duration设置为 0 时,连续点击两个不同的跳转会导致如下崩溃 #issue53 + } + } +} diff --git a/BaseMvvm/src/main/java/com/holo/architecture/ext/SystemServiceExt.kt b/BaseMvvm/src/main/java/com/holo/architecture/ext/SystemServiceExt.kt new file mode 100644 index 0000000..26d4ed2 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/ext/SystemServiceExt.kt @@ -0,0 +1,64 @@ +package com.holo.architecture.ext + +import android.app.* +import android.app.job.JobScheduler +import android.content.ClipboardManager +import android.content.Context +import android.hardware.SensorManager +import android.location.LocationManager +import android.media.AudioManager +import android.media.MediaRouter +import android.net.ConnectivityManager +import android.net.wifi.WifiManager +import android.os.BatteryManager +import android.os.PowerManager +import android.os.Vibrator +import android.os.storage.StorageManager +import android.telephony.CarrierConfigManager +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.view.LayoutInflater +import android.view.WindowManager +import android.view.accessibility.AccessibilityManager +import android.view.inputmethod.InputMethodManager +import androidx.core.content.ContextCompat + +/** + * @Desc 系统服务扩展 + * @Author holo + * @Date 2022/1/12 + */ + +/** + * Return system service which type is [T] + */ +inline fun Context.getSystemService(): T? = + ContextCompat.getSystemService(this, T::class.java) + +val Context.windowManager get() = getSystemService() +val Context.clipboardManager get() = getSystemService() +val Context.layoutInflater get() = getSystemService() +val Context.activityManager get() = getSystemService() +val Context.powerManager get() = getSystemService() +val Context.alarmManager get() = getSystemService() +val Context.notificationManager get() = getSystemService() +val Context.keyguardManager get() = getSystemService() +val Context.locationManager get() = getSystemService() +val Context.searchManager get() = getSystemService() +val Context.storageManager get() = getSystemService() +val Context.vibrator get() = getSystemService() +val Context.connectivityManager get() = getSystemService() +val Context.wifiManager get() = getSystemService() +val Context.audioManager get() = getSystemService() +val Context.mediaRouter get() = getSystemService() +val Context.telephonyManager get() = getSystemService() +val Context.sensorManager get() = getSystemService() +val Context.subscriptionManager get() = getSystemService() +val Context.carrierConfigManager get() = getSystemService() +val Context.inputMethodManager get() = getSystemService() +val Context.uiModeManager get() = getSystemService() +val Context.downloadManager get() = getSystemService() +val Context.batteryManager get() = getSystemService() +val Context.jobScheduler get() = getSystemService() +val Context.accessibilityManager get() = getSystemService() + diff --git a/BaseMvvm/src/main/java/com/holo/architecture/ext/ViewExt.kt b/BaseMvvm/src/main/java/com/holo/architecture/ext/ViewExt.kt new file mode 100644 index 0000000..ec7e960 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/ext/ViewExt.kt @@ -0,0 +1,187 @@ +package com.holo.architecture.ext + +import android.app.Service +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.view.animation.CycleInterpolator +import android.view.inputmethod.InputMethodManager +import android.widget.Button +import android.widget.EditText +import android.widget.TextView +import androidx.activity.OnBackPressedCallback +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.RecyclerView + +/** + * + * + * @Author holo + * @Date 2022/2/14 + */ + +fun View.showKeyboard() { + this.requestFocus() + (this.context.getSystemService(Service.INPUT_METHOD_SERVICE) as? InputMethodManager) + ?.showSoftInput(this, 0) +} + +fun View.hideKeyboard() { + (this.context.getSystemService(Service.INPUT_METHOD_SERVICE) as? InputMethodManager) + ?.hideSoftInputFromWindow(this.windowToken, 0) +} + +fun T.isVisible(): Boolean { + return if (this is Button) { + this.visibility == View.VISIBLE && this.text.trim().isNotBlank() + } else { + this.visibility == View.VISIBLE + } +} + +fun T.isNotVisible(): Boolean = !isVisible() + +fun View.toVisible(visible: Boolean = true) { + this.visibility = if (visible) View.VISIBLE else View.GONE +} + +fun View.toGone() { + this.visibility = View.GONE +} + +fun View.toInvisible(visible: Boolean = true) { + this.visibility = if (visible) View.VISIBLE else View.INVISIBLE +} + +/** + * Extension function to simplify setting an afterTextChanged action to EditText components. + */ +fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) { + this.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(editable: Editable?) { + afterTextChanged.invoke(editable.toString()) + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + }) +} + +fun RecyclerView.onChildViewDetachedFromWindow(childViewDetached: (view: View) -> Unit) { + this.addOnChildAttachStateChangeListener(object : RecyclerView.OnChildAttachStateChangeListener { + override fun onChildViewAttachedToWindow(view: View) { + + } + + override fun onChildViewDetachedFromWindow(view: View) { + childViewDetached.invoke(view) + } + }) +} + +/** + * 检查EditText输入框是否没有输入内容 + * + * @param message 提示语 + * @return + */ +fun TextView.checkBlank(activity: AppCompatActivity, message: String): String? { + val text = this.text.toString() + if (text.isBlank()) { + activity.showToast(message) + return null + } + return text +} + +/** + * 检查EditText输入框是否没有输入内容 + * + * @param message 提示语 + * @return + */ +fun TextView.checkBlank(activity: AppCompatActivity, @StringRes message: Int): String? { + return checkBlank(activity, context.getString(message)) +} + +/** + * 检查EditText输入框是否没有输入内容 + * + * @param message 提示语 + * @return + */ +fun TextView.checkBlank(fragment: Fragment, message: String): String? { + val text = this.text.toString() + if (text.isBlank()) { + fragment.activity?.showToast(message) + return null + } + return text +} + +/** + * 检查EditText输入框是否没有输入内容 + * + * @param message 提示语 + * @return + */ +fun TextView.checkBlank(fragment: Fragment, @StringRes message: Int): String? { + return checkBlank(fragment, context.getString(message)) +} + +fun EditText.isBlank(): Boolean = this.text?.isBlank() == true + +fun EditText.isNotBlank(): Boolean = this.text?.isNotBlank() == true + + +typealias OnBackPressedTypeAlias = () -> Boolean + +/** + * 解决 Fragment 中 OnBackPressed 事件, 默认结束当前Fragment依附的Activity + * @param type true:结束当前Activity,false:响应callback回调 + */ +fun Fragment.setOnHandleBackPressed(type: Boolean = true, callback: OnBackPressedTypeAlias? = null) { + requireActivity().onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (type) { + requireActivity().finish() + } else { + if (callback?.invoke() == true) { + requireActivity().finish() + } + } + } + }) +} + +/** + * 防止重复点击事件 默认0.5秒内不可重复点击 + * @param interval 时间间隔 默认0.2秒 + * @param action 执行方法 + */ +var lastClickTime = 0L +fun View.clickNoRepeat(interval: Long = 200, action: (view: View) -> Unit) { + setOnClickListener { + val currentTime = System.currentTimeMillis() + if (lastClickTime != 0L && (currentTime - lastClickTime < interval)) { + return@setOnClickListener + } + lastClickTime = currentTime + action(it) + } +} + +/** + * View左右抖动动画 + * @receiver View + */ +fun View.shakeAnimate() { + this.animate() + .translationX(20f) + .setDuration(500) + .setInterpolator(CycleInterpolator(3f)) + .start() +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/ext/lifecycle/ActivityStack.kt b/BaseMvvm/src/main/java/com/holo/architecture/ext/lifecycle/ActivityStack.kt new file mode 100644 index 0000000..cbcd156 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/ext/lifecycle/ActivityStack.kt @@ -0,0 +1,85 @@ +package com.holo.architecture.ext.lifecycle + +import android.app.Activity +import java.util.* + +/** + * @Desc + * @Author holo + * @Date 2022/1/10 + */ +object ActivityStack { + + private val mActivityList = Stack() + + /** + * 获取当前栈顶的activity + */ + val currentActivity: Activity? + get() = + if (mActivityList.isEmpty()) null + else mActivityList.last() + + /** + * 栈中activity数量 + */ + val size: Int + get() = + mActivityList.size + + /** + * activity入栈 + */ + fun pushActivity(activity: Activity) { + if (mActivityList.contains(activity)) { + if (mActivityList.last() != activity) { + mActivityList.remove(activity) + mActivityList.add(activity) + } + } else { + mActivityList.add(activity) + } + } + + /** + * activity出栈 + */ + fun popActivity(activity: Activity) { + mActivityList.remove(activity) + } + + /** + * 关闭当前activity + */ + fun finishCurrentActivity() { + currentActivity?.finish() + } + + /** + * 关闭传入的activity + */ + fun finishActivity(activity: Activity) { + mActivityList.remove(activity) + activity.finish() + } + + /** + * 关闭传入的activity类名 + */ + fun finishActivity(clazz: Class<*>) { + for (activity in mActivityList) + if (activity.javaClass == clazz) + activity.finish() + } + + /** + * 关闭所有的activity + */ + fun finishAllActivity() { + for (activity in mActivityList) { + activity.finish() + } + mActivityList.clear() + } + +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/ext/lifecycle/LifecycleCallBack.kt b/BaseMvvm/src/main/java/com/holo/architecture/ext/lifecycle/LifecycleCallBack.kt new file mode 100644 index 0000000..74b3cf9 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/ext/lifecycle/LifecycleCallBack.kt @@ -0,0 +1,45 @@ +package com.holo.architecture.ext.lifecycle + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import com.holo.architecture.logger.KLog + +/** + * @Desc + * @Author holo + * @Date 2022/1/10 + */ +internal class LifecycleCallBack : Application.ActivityLifecycleCallbacks { + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + ActivityStack.pushActivity(activity) + KLog.d(this::class.java.simpleName, "onActivityCreated : ${activity.localClassName}") + } + + override fun onActivityStarted(activity: Activity) { +// KLog.d(this::class.java.simpleName, "onActivityStarted : ${activity.localClassName}") + } + + override fun onActivityResumed(activity: Activity) { +// KLog.d(this::class.java.simpleName, "onActivityResumed : ${activity.localClassName}") + } + + override fun onActivityPaused(activity: Activity) { +// KLog.d(this::class.java.simpleName, "onActivityPaused : ${activity.localClassName}") + } + + + override fun onActivityDestroyed(activity: Activity) { + ActivityStack.popActivity(activity) + KLog.d(this::class.java.simpleName, "onActivityDestroyed : ${activity.localClassName}") + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { +// KLog.d(this::class.java.simpleName, "onActivitySaveInstanceState : ${activity.localClassName}") + } + + override fun onActivityStopped(activity: Activity) { + KLog.d(this::class.java.simpleName, "onActivityStopped : ${activity.localClassName}") + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/initializer/BaseInitializer.kt b/BaseMvvm/src/main/java/com/holo/architecture/initializer/BaseInitializer.kt new file mode 100644 index 0000000..4b9c896 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/initializer/BaseInitializer.kt @@ -0,0 +1,32 @@ +package com.holo.architecture.initializer + +import android.app.Application +import android.content.Context +import android.net.NetworkRequest +import androidx.startup.Initializer +import com.holo.architecture.ext.connectivityManager +import com.holo.architecture.ext.lifecycle.LifecycleCallBack +import com.holo.architecture.network.NetworkStateCallback + +val appContext: Application by lazy { BaseInitializer.app } + +/** + * @Desc + * @Author holo + * @Date 2022/1/10 + */ +class BaseInitializer : Initializer { + internal companion object { + lateinit var app: Application + } + + override fun create(context: Context) { + app = context.applicationContext as Application + app.registerActivityLifecycleCallbacks(LifecycleCallBack()) + + //监听网络变化状态 + appContext.connectivityManager?.registerNetworkCallback(NetworkRequest.Builder().build(), NetworkStateCallback(app)) + } + + override fun dependencies(): List>> = emptyList() +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/logger/KLog.kt b/BaseMvvm/src/main/java/com/holo/architecture/logger/KLog.kt new file mode 100644 index 0000000..e56a5ee --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/logger/KLog.kt @@ -0,0 +1,284 @@ +package com.holo.architecture.logger + +import android.text.TextUtils +import android.util.Log +import com.holo.architecture.logger.klog.BaseLog.printDefault +import com.holo.architecture.logger.klog.FileLog.printFile +import com.holo.architecture.logger.klog.JsonLog +import com.holo.architecture.logger.klog.XmlLog +import java.io.File +import java.io.PrintWriter +import java.io.StringWriter + +/** + * Created on 2020/8/11. + * @author Holo8 + */ +object KLog { + + internal val LINE_SEPARATOR = System.getProperty("line.separator") + internal const val NULL_TIPS = "Log with null object" + + private const val PARAM = "Param" + private const val NULL = "null" + private const val TAG_DEFAULT = "KLog" + + internal const val JSON_INDENT = 4 + + internal const val V = 0x1 + internal const val D = 0x2 + internal const val I = 0x3 + internal const val W = 0x4 + internal const val E = 0x5 + internal const val A = 0x6 + + private const val JSON = 0x7 + private const val XML = 0x8 + + private const val STACK_TRACE_INDEX_5 = 5 + private const val STACK_TRACE_INDEX_4 = 4 + + private var mGlobalTag: String? = null + private var mIsGlobalTagEmpty = true + private var IS_SHOW_LOG = true + + fun init(isShowLog: Boolean, tag: String? = null) { + IS_SHOW_LOG = isShowLog + mGlobalTag = tag + mIsGlobalTagEmpty = mGlobalTag.isNullOrEmpty() + } + + fun v(msg: String?) { + printLog(V, null, msg) + } + + fun v(tag: String?, vararg objects: String?) { + printLog(V, tag, *objects) + } + + fun d(msg: String?) { + printLog(D, null, msg) + } + + fun d(tag: String?, vararg objects: String?) { + printLog(D, tag, *objects) + } + + fun i(msg: String?) { + printLog(I, null, msg) + } + + fun i(tag: String?, vararg objects: String?) { + printLog(I, tag, *objects) + } + + fun w(msg: String?) { + printLog(W, null, msg) + } + + fun w(tag: String?, vararg objects: String?) { + printLog(W, tag, *objects) + } + + fun e(msg: String?) { + printLog(E, null, msg) + } + + fun e(tr: Throwable) { + e(null, "", tr) + } + + fun e(tag: String?, msg: String, tr: Throwable) { + printLog(E, tag, msg.plus("\n").plus(tr.message).plus("\n").plus(Log.getStackTraceString(tr))) + } + + fun e(tag: String?, vararg objects: String?) { + printLog(E, tag, *objects) + } + + fun a(msg: String?) { + printLog(A, null, msg) + } + + fun a(tag: String?, vararg objects: String?) { + printLog(A, tag, *objects) + } + + fun json(jsonFormat: String?) { + printLog(JSON, null, jsonFormat) + } + + fun json(tag: String?, jsonFormat: String?) { + printLog(JSON, tag, jsonFormat) + } + + fun xml(xml: String?) { + printLog(XML, null, xml) + } + + fun xml(tag: String?, xml: String?) { + printLog(XML, tag, xml) + } + + fun file(targetDirectory: File, msg: String) { + printFile(null, targetDirectory, null, msg) + } + + fun file(tag: String?, targetDirectory: File, msg: String) { + printFile(tag, targetDirectory, null, msg) + } + + fun file(tag: String?, targetDirectory: File, fileName: String?, msg: String) { + printFile(tag, targetDirectory, fileName, msg) + } + + fun debug(msg: String?) { + printDebug(null, msg) + } + + fun debug(tag: String?, vararg objects: String?) { + printDebug(tag, *objects) + } + + fun trace() { + printStackTrace() + } + + private fun printStackTrace() { + if (!IS_SHOW_LOG) { + return + } + val tr = Throwable() + val sw = StringWriter() + val pw = PrintWriter(sw) + tr.printStackTrace(pw) + pw.flush() + val message = sw.toString() + val traceString = message.split("\\n\\t".toRegex()).toTypedArray() + val sb = java.lang.StringBuilder() + sb.append("\n") + for (trace in traceString) { + if (trace.contains("at KLog")) { + continue + } + sb.append(trace).append("\n") + } + val contents = wrapperContent(STACK_TRACE_INDEX_4, null, sb.toString()) + val tag = contents[0] + val msg = contents[1] + val headString = contents[2] + printDefault(D, tag, headString + msg) + } + + private fun printLog(type: Int, tagStr: String?, vararg messages: String?) { + if (!IS_SHOW_LOG) { + return; + } + val contents: Array = wrapperContent(STACK_TRACE_INDEX_5, tagStr, *messages) + val tag = contents[0] + val msg = contents[1] + val headString = contents[2] + when (type) { + V, D, I, W, E, A -> { + printDefault(type, tag, headString + msg) + } + JSON -> JsonLog.printJson(tag, msg, headString) + XML -> XmlLog.printXml(tag, msg, headString) + } + } + + private fun wrapperContent( + stackTraceIndex: Int, + tagStr: String?, + vararg messages: String? + ): Array { + val stackTrace = Thread.currentThread().stackTrace + val targetElement = stackTrace[stackTraceIndex] + + val fileName = targetElement.fileName + var className = targetElement.className + val suffix = fileName.substring(fileName.lastIndexOf(".")) + val classNameInfo = className.split("\\.".toRegex()) + + if (classNameInfo.isNotEmpty()) { + className = classNameInfo[classNameInfo.size - 1] + suffix + } + + if (className.contains("$")) { + className = className.split("\\$".toRegex())[0] + suffix + } + + val methodName = targetElement.methodName + var lineNumber = targetElement.lineNumber + + if (lineNumber < 0) { + lineNumber = 0 + } + + var tag = tagStr ?: className + + if (mIsGlobalTagEmpty && TextUtils.isEmpty(tag)) { + tag = TAG_DEFAULT + } else if (!mIsGlobalTagEmpty) { + tag = mGlobalTag + } + + val msg = getObjectsString(*messages) + val headString = "[ ($className:$lineNumber)#$methodName ] " + return arrayOf(tag, msg, headString) + } + + private fun getObjectsString(vararg messages: String?): String { + when { + messages.isNullOrEmpty() -> { + return NULL_TIPS + } + messages.size > 1 -> { + val stringBuilder = StringBuilder() + stringBuilder.append("\n") + for (i in messages.indices) { + val msg = messages[i] + if (msg == null) { + stringBuilder.append(PARAM).append("[").append(i).append("]").append(" = ").append( + NULL + ).append("\n") + } else { + stringBuilder.append(PARAM).append("[").append(i).append("]").append(" = ").append(msg.toString()).append("\n") + } + } + return stringBuilder.toString() + } + else -> { + return messages[0].toString() + } + } + } + + private fun printDebug(tagStr: String?, vararg messages: String?) { + val contents = wrapperContent(STACK_TRACE_INDEX_5, tagStr, *messages) + val tag = contents[0] + val msg = contents[1] + val headString = contents[2] + printDefault(D, tag, headString + msg) + } + + private fun printFile( + tagStr: String?, + targetDirectory: File, + fileName: String?, + objectMsg: String + ) { + if (!IS_SHOW_LOG) { + return + } + val contents = wrapperContent(STACK_TRACE_INDEX_5, tagStr, objectMsg) + val tag = contents[0] + val msg = contents[1] + val headString = contents[2] + printFile(tag, targetDirectory, fileName, headString, msg) + } + + internal fun apiLog(cmdId: Int, msg: String) { + d("Request cmdId:$cmdId, body:$msg") + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/logger/KLogUtil.kt b/BaseMvvm/src/main/java/com/holo/architecture/logger/KLogUtil.kt new file mode 100644 index 0000000..cc4071d --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/logger/KLogUtil.kt @@ -0,0 +1,23 @@ +package com.holo.architecture.logger + +import android.text.TextUtils +import android.util.Log + +/** + * Created on 2020/8/11. + * @author Holo + */ +internal object KLogUtil { + + fun isEmpty(line: String): Boolean { + return TextUtils.isEmpty(line) || line == "\n" || line == "\t" || TextUtils.isEmpty(line.trim { it <= ' ' }) + } + + fun printLine(tag: String?, isTop: Boolean) { + if (isTop) { + Log.d(tag, "╔═══════════════════════════════════════════════════════════════════════════════════════") + } else { + Log.d(tag, "╚═══════════════════════════════════════════════════════════════════════════════════════") + } + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/logger/klog/BaseLog.kt b/BaseMvvm/src/main/java/com/holo/architecture/logger/klog/BaseLog.kt new file mode 100644 index 0000000..7349b64 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/logger/klog/BaseLog.kt @@ -0,0 +1,40 @@ +package com.holo.architecture.logger.klog + +import android.util.Log +import com.holo.architecture.logger.KLog + +/** + * Created on 2020/8/11. + * @author Holo + */ +internal object BaseLog { + + private const val MAX_LENGTH = 4000 + + fun printDefault(type: Int, tag: String, msg: String) { + var index = 0 + val length = msg.length + val countOfSub = length / MAX_LENGTH + if (countOfSub > 0) { + for (i in 0 until countOfSub) { + val sub = msg.substring(index, index + MAX_LENGTH) + printSub(type, tag, sub) + index += MAX_LENGTH + } + printSub(type, tag, msg.substring(index, length)) + } else { + printSub(type, tag, msg) + } + } + + private fun printSub(type: Int, tag: String, sub: String) { + when (type) { + KLog.V -> Log.v(tag, sub) + KLog.D -> Log.d(tag, sub) + KLog.I -> Log.i(tag, sub) + KLog.W -> Log.w(tag, sub) + KLog.E -> Log.e(tag, sub) + KLog.A -> Log.wtf(tag, sub) + } + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/logger/klog/FileLog.kt b/BaseMvvm/src/main/java/com/holo/architecture/logger/klog/FileLog.kt new file mode 100644 index 0000000..c0362d2 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/logger/klog/FileLog.kt @@ -0,0 +1,59 @@ +package com.holo.architecture.logger.klog + +import android.util.Log +import java.io.* +import java.util.* + +/** + * Created on 2020/8/11. + * @author Holo + */ +internal object FileLog { + + private const val FILE_PREFIX = "KLog_" + private const val FILE_FORMAT = ".log" + + fun printFile( + tag: String?, + targetDirectory: File, + fileName: String?, + headString: String, + msg: String + ) { + if (save(targetDirectory, fileName ?: getFileName(), msg)) { + Log.d(tag, headString + " save log success ! location is >>>" + targetDirectory.absolutePath + "/" + fileName) + } else { + Log.e(tag, headString + "save log fails !") + } + } + + private fun save(dic: File, fileName: String, msg: String): Boolean { + val file = File(dic, fileName) + return try { + val outputStream: OutputStream = FileOutputStream(file) + val outputStreamWriter = OutputStreamWriter(outputStream, "UTF-8") + outputStreamWriter.write(msg) + outputStreamWriter.flush() + outputStream.close() + true + } catch (e: FileNotFoundException) { + e.printStackTrace() + false + } catch (e: UnsupportedEncodingException) { + e.printStackTrace() + false + } catch (e: IOException) { + e.printStackTrace() + false + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + private fun getFileName(): String { + val random = Random() + return FILE_PREFIX + java.lang.Long.toString(System.currentTimeMillis() + random.nextInt(10000)) + .substring(4) + FILE_FORMAT + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/logger/klog/JsonLog.kt b/BaseMvvm/src/main/java/com/holo/architecture/logger/klog/JsonLog.kt new file mode 100644 index 0000000..76c7e39 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/logger/klog/JsonLog.kt @@ -0,0 +1,42 @@ +package com.holo.architecture.logger.klog + +import android.util.Log +import com.holo.architecture.logger.KLog +import com.holo.architecture.logger.KLogUtil +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +/** + * Created on 2020/8/11. + * @author Holo + */ +internal object JsonLog { + + fun printJson(tag: String?, msg: String, headString: String) { + var message: String = try { + when { + msg.startsWith("{") -> { + val jsonObject = JSONObject(msg) + jsonObject.toString(KLog.JSON_INDENT) + } + msg.startsWith("[") -> { + val jsonArray = JSONArray(msg) + jsonArray.toString(KLog.JSON_INDENT) + } + else -> { + msg + } + } + } catch (e: JSONException) { + msg + } + KLogUtil.printLine(tag, true) + message = headString + KLog.LINE_SEPARATOR + message + val lines = message.split(KLog.LINE_SEPARATOR!!).toTypedArray() + for (line in lines) { + Log.d(tag, "║ $line") + } + KLogUtil.printLine(tag, false) + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/logger/klog/XmlLog.kt b/BaseMvvm/src/main/java/com/holo/architecture/logger/klog/XmlLog.kt new file mode 100644 index 0000000..05516fc --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/logger/klog/XmlLog.kt @@ -0,0 +1,56 @@ +package com.holo.architecture.logger.klog + +import android.util.Log +import com.holo.architecture.logger.KLog +import com.holo.architecture.logger.KLogUtil.isEmpty +import com.holo.architecture.logger.KLogUtil.printLine +import java.io.StringReader +import java.io.StringWriter +import javax.xml.transform.OutputKeys +import javax.xml.transform.Source +import javax.xml.transform.TransformerFactory +import javax.xml.transform.stream.StreamResult +import javax.xml.transform.stream.StreamSource + +/** + * Created on 2020/8/11. + * @author Holo + */ +internal object XmlLog { + + fun printXml(tag: String?, xml: String?, headString: String) { + var xml = xml + if (xml != null) { + xml = formatXML(xml) + xml = """ + $headString + $xml + """.trimIndent() + } else { + xml = headString + KLog.NULL_TIPS + } + printLine(tag, true) + val lines = xml.split(KLog.LINE_SEPARATOR!!).toTypedArray() + for (line in lines) { + if (!isEmpty(line)) { + Log.d(tag, "║ $line") + } + } + printLine(tag, false) + } + + private fun formatXML(inputXML: String): String { + return try { + val xmlInput: Source = StreamSource(StringReader(inputXML)) + val xmlOutput = StreamResult(StringWriter()) + val transformer = TransformerFactory.newInstance().newTransformer() + transformer.setOutputProperty(OutputKeys.INDENT, "yes") + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2") + transformer.transform(xmlInput, xmlOutput) + xmlOutput.writer.toString().replaceFirst(">".toRegex(), ">\n") + } catch (e: Exception) { + e.printStackTrace() + inputXML + } + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/network/AppException.kt b/BaseMvvm/src/main/java/com/holo/architecture/network/AppException.kt new file mode 100644 index 0000000..28d19c6 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/network/AppException.kt @@ -0,0 +1,25 @@ +package com.holo.architecture.network + +/** + * @Desc 自定义错误信息异常 + * + * @Author holo + * @Date 2022/1/11 + */ +data class AppException( + var errorMsg: String, //错误消息 + var errCode: Int = 0, //错误码 + var errorLog: String = "", //错误日志 + var throwable: Throwable? = null +) : RuntimeException(errorMsg) { + + constructor(msg: String, code: Int) : this(msg, code, "", null) + + constructor(error: HoloError, e: Throwable? = null) : this(error.getValue(), error.getKey(), e?.message ?: "", e) + + override fun toString(): String { + return "AppException(errorMsg='$errorMsg', errCode=$errCode, errorLog='$errorLog', throwable=${throwable?.message})" + } + + +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/network/BaseResponse.kt b/BaseMvvm/src/main/java/com/holo/architecture/network/BaseResponse.kt new file mode 100644 index 0000000..42518fa --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/network/BaseResponse.kt @@ -0,0 +1,20 @@ +package com.holo.architecture.network + +/** + * @Desc 服务器返回数据的基类 + * 必须实现抽象方法,根据自己的业务判断返回请求结果是否成功 + * + * @Author holo + * @Date 2022/1/11 + */ +abstract class BaseResponse { + + //抽象方法,用户的基类继承该类时,需要重写该方法 + abstract fun isSuccess(): Boolean + + abstract fun getResponseData(): T + + abstract fun getResponseCode(): Int + + abstract fun getResponseMsg(): String +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/network/ExceptionHandle.kt b/BaseMvvm/src/main/java/com/holo/architecture/network/ExceptionHandle.kt new file mode 100644 index 0000000..6afc0cc --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/network/ExceptionHandle.kt @@ -0,0 +1,63 @@ +package com.holo.architecture.network + +import android.net.ParseException +import com.google.gson.JsonParseException +import com.google.gson.stream.MalformedJsonException +import org.apache.http.conn.ConnectTimeoutException +import org.json.JSONException +import retrofit2.HttpException +import java.net.ConnectException + +/** + * @Desc 根据异常返回相关的错误信息工具类 + * + * @Author holo + * @Date 2022/1/11 + */ +object ExceptionHandle { + + fun handleException(e: Throwable?): AppException { + e?.printStackTrace() + val ex: AppException + e?.let { + when (it) { + is HttpException -> { + ex = AppException(HoloError.NETWORK_ERROR, e) + return ex + } + is JsonParseException, is JSONException, is ParseException, is MalformedJsonException -> { + ex = AppException(HoloError.PARSE_ERROR, e) + return ex + } + is ConnectException -> { + ex = AppException(HoloError.NETWORK_ERROR, e) + return ex + } + is javax.net.ssl.SSLException -> { + ex = AppException(HoloError.SSL_ERROR, e) + return ex + } + is ConnectTimeoutException -> { + ex = AppException(HoloError.TIMEOUT_ERROR, e) + return ex + } + is java.net.SocketTimeoutException -> { + ex = AppException(HoloError.TIMEOUT_ERROR, e) + return ex + } + is java.net.UnknownHostException -> { + ex = AppException(HoloError.TIMEOUT_ERROR, e) + return ex + } + is AppException -> return it + + else -> { + ex = AppException(HoloError.UNKNOWN, e) + return ex + } + } + } + ex = AppException(HoloError.UNKNOWN, e) + return ex + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/network/HoloError.kt b/BaseMvvm/src/main/java/com/holo/architecture/network/HoloError.kt new file mode 100644 index 0000000..7483a9c --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/network/HoloError.kt @@ -0,0 +1,47 @@ +package com.holo.architecture.network + +/** + * @Desc 错误枚举类 + * + * @Author holo + * @Date 2022/1/11 + */ +enum class HoloError(private val code: Int, private val err: String) { + + /** + * 未知错误 + */ + UNKNOWN(1000, "请求失败,请稍后再试"), + /** + * 解析错误 + */ + PARSE_ERROR(1001, "解析错误,请稍后再试"), + /** + * 网络错误 + */ + NETWORK_ERROR(1002, "网络连接错误,请稍后重试"), + + /** + * 证书出错 + */ + SSL_ERROR(1004, "证书出错,请稍后再试"), + + /** + * 连接超时 + */ + TIMEOUT_ERROR(1006, "网络连接超时,请稍后重试"), + + /** + * 无网络 + */ + NO_NETWORK(1007,"请检查您的网络连接状态"); + + fun getValue(): String { + return err + } + + fun getKey(): Int { + return code + } + +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/network/ListState.kt b/BaseMvvm/src/main/java/com/holo/architecture/network/ListState.kt new file mode 100644 index 0000000..f4af348 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/network/ListState.kt @@ -0,0 +1,46 @@ +package com.holo.architecture.network + +import com.holo.architecture.base.bean.BaseBean + +/** + * 分页查询数据通用base + * + * @Author holo + * @Date 2022/2/9 + */ +data class ListState( + /** + * 是否请求成功 + */ + val isSuccess: Boolean, + + /** + * 是否为刷新 + */ + val isRefresh: Boolean = true, + + /** + * 是否还有更多 + */ + val hasMore: Boolean = true, + + /** + * 是否没有数据 + */ + val isEmpty: Boolean = true, + + /** + * 列表数据 + */ + val listData: List = listOf(), + + /** + * 数据总条数 + */ + val total:Int = 0, + + /** + * 错误信息 + */ + val err: AppException = AppException("") +) : BaseBean() \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/network/NetworkStateCallback.kt b/BaseMvvm/src/main/java/com/holo/architecture/network/NetworkStateCallback.kt new file mode 100644 index 0000000..357f97f --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/network/NetworkStateCallback.kt @@ -0,0 +1,77 @@ +package com.holo.architecture.network + +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import com.holo.architecture.logger.KLog +import com.holo.architecture.utils.netstatus.NetType +import com.holo.architecture.utils.netstatus.NetUtils + +/** + * @Desc + * + * @Author holo + * @Date 2022/1/13 + */ +class NetworkStateCallback(val application: Application) : ConnectivityManager.NetworkCallback() { + + private companion object { + const val TAG = "NetworkState" + } + + // 网络状态广播监听 + private val receiver = NetStatusReceiver() + + init { + val filter = IntentFilter() + filter.addAction("android.net.conn.CONNECTIVITY_CHANGE") + application.registerReceiver(receiver, filter) + post(NetUtils.getNetStatus(application)) + } + + override fun onAvailable(network: Network) { + super.onAvailable(network) + KLog.i(TAG, "net connect success! 网络已连接") + } + + override fun onLost(network: Network) { + super.onLost(network) + KLog.i(TAG, "net disconnect! 网络已断开连接") + post(NetType.NONE) + } + + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + super.onCapabilitiesChanged(network, networkCapabilities) + KLog.i(TAG, "net status change! 网络连接改变") + // 表明此网络连接成功验证 + val type = NetUtils.getNetStatus(networkCapabilities) + if (type == NetworkStateManager.mNetworkStateCallback.value) return + post(type) + } + + // 执行 + private fun post(str: @NetType String) { + NetworkStateManager.mNetworkStateCallback.postValue(str) + NetworkStateManager.netType = str + } + + /** + * Receiver方式监听网络状态变化 + * Android 7.0+动态监听还有效 + */ + inner class NetStatusReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context?, intent: Intent?) { + context ?: return + intent ?: return + val type = NetUtils.getNetStatus(context) + if (type == NetworkStateManager.mNetworkStateCallback.value) return + post(type) + } + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/network/NetworkStateManager.kt b/BaseMvvm/src/main/java/com/holo/architecture/network/NetworkStateManager.kt new file mode 100644 index 0000000..2e3c592 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/network/NetworkStateManager.kt @@ -0,0 +1,18 @@ +package com.holo.architecture.network + +import com.holo.architecture.utils.netstatus.NetType +import com.kunminx.architecture.ui.callback.UnPeekLiveData + +/** + * 网络状态变化LiveData监听 + * + * @Author holo + * @Date 2022/1/14 + */ +object NetworkStateManager { + + val mNetworkStateCallback = UnPeekLiveData<@NetType String>() + + var netType: String = NetType.NONE + +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/network/State.kt b/BaseMvvm/src/main/java/com/holo/architecture/network/State.kt new file mode 100644 index 0000000..e9372e7 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/network/State.kt @@ -0,0 +1,43 @@ +package com.holo.architecture.network + +/** + * @Desc 包含请求状态的结果集密封类 + * + * @Author holo + * @Date 2022/1/12 + */ +sealed class State { + class Loading : State() + + data class Success(val data: T) : State() + + data class Error(val err: AppException) : State() + + fun isLoading(): Boolean = this is Loading + + fun isSuccessful(): Boolean = this is Success + + fun isFailed(): Boolean = this is Error + + companion object { + + /** + * Returns [State.Loading] instance. + */ + fun loading() = Loading() + + /** + * Returns [State.Success] instance. + * @param data Data to emit with status. + */ + fun success(data: T) = + Success(data) + + /** + * Returns [State.Error] instance. + * @param message Description of failure. + */ + fun error(message: AppException) = + Error(message) + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/utils/DeviceIdUtil.kt b/BaseMvvm/src/main/java/com/holo/architecture/utils/DeviceIdUtil.kt new file mode 100644 index 0000000..e0f0f83 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/utils/DeviceIdUtil.kt @@ -0,0 +1,157 @@ +package com.holo.architecture.utils + +import android.annotation.SuppressLint +import android.os.Build +import android.provider.Settings +import com.holo.architecture.initializer.appContext +import java.security.MessageDigest +import java.util.* + + +/** + * 计算设备唯一标识 + * + * @Author holo + * @Date 2022/3/23 + */ +object DeviceIdUtil { + /** + * 获得设备硬件标识 + * + * @return 设备硬件标识 + */ + fun getDeviceId(): String { + val sbDeviceId = StringBuilder() + + //获得AndroidId(无需权限) + val androidId = getAndroidId() + //获得设备序列号(无需权限) + val serial = getSERIAL() + //获得硬件UUID(根据硬件相关属性,生成UUID)(无需权限) + val uuid = getDeviceUUID().replace("-", "") + + //追加 AndroidId + if (androidId.isNotEmpty()) { + sbDeviceId.append(androidId) + sbDeviceId.append("|") + } + //追加serial + if (serial.isNotEmpty()) { + sbDeviceId.append(serial) + sbDeviceId.append("|") + } + //追加硬件uuid + if (uuid.isNotEmpty()) { + sbDeviceId.append(uuid) + } + + //生成SHA1,统一DeviceId长度 + if (sbDeviceId.isNotEmpty()) { + try { + val hash = getHashByString(sbDeviceId.toString()) + val sha1 = bytesToHex(hash) + if (sha1.isNotEmpty()) { + //返回最终的DeviceId + return sha1 + } + } catch (ex: java.lang.Exception) { + ex.printStackTrace() + } + } + + //如果以上硬件标识数据均无法获得, + //则DeviceId默认使用系统随机数,这样保证DeviceId不为空 + return UUID.randomUUID().toString().replace("-", "") + } + + /** + * 获得设备的AndroidId + * + * @return 设备的AndroidId + */ + @SuppressLint("HardwareIds") + private fun getAndroidId(): String { + try { + return Settings.Secure.getString( + appContext.contentResolver, + Settings.Secure.ANDROID_ID + ) + } catch (ex: java.lang.Exception) { + ex.printStackTrace() + } + return "" + } + + /** + * 获得设备序列号(如:WTK7N16923005607), 个别设备无法获取 + * Android 6.0及以上版本是需要动态申请READ_PHONE_STATE权限 + * + * @return 设备序列号 + */ + private fun getSERIAL(): String { + try { + return Build.SERIAL + } catch (ex: java.lang.Exception) { + ex.printStackTrace() + } + return "" + } + + /** + * 获得设备硬件uuid + * 使用硬件信息,计算出一个随机数 + * + * @return 设备硬件uuid + */ + private fun getDeviceUUID(): String { + return try { + val dev = "3883756".plus(Build.BOARD.length % 10) + .plus(Build.BRAND.length % 10) + .plus(Build.DEVICE.length % 10) + .plus(Build.HARDWARE.length % 10) + .plus(Build.ID.length % 10) + .plus(Build.MODEL.length % 10) + .plus(Build.PRODUCT.length % 10) + .plus(Build.SERIAL.length % 10) + UUID( + dev.hashCode().toLong(), + Build.SERIAL.hashCode().toLong() + ).toString() + } catch (ex: java.lang.Exception) { + ex.printStackTrace() + "" + } + } + + /** + * 取SHA1 + * @param data 数据 + * @return 对应的hash值 + */ + private fun getHashByString(data: String): ByteArray { + return try { + val messageDigest: MessageDigest = MessageDigest.getInstance("SHA1") + messageDigest.reset() + messageDigest.update(data.toByteArray(charset("UTF-8"))) + messageDigest.digest() + } catch (e: Exception) { + "".toByteArray() + } + } + + /** + * 转16进制字符串 + * @param data 数据 + * @return 16进制字符串 + */ + private fun bytesToHex(data: ByteArray): String { + val sb = StringBuilder() + var stmp: String + for (n in data.indices) { + stmp = Integer.toHexString(data[n].toInt() and 0xFF) + if (stmp.length == 1) sb.append("0") + sb.append(stmp) + } + return sb.toString().toUpperCase(Locale.CHINA) + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/utils/ViewBindingUtil.kt b/BaseMvvm/src/main/java/com/holo/architecture/utils/ViewBindingUtil.kt new file mode 100644 index 0000000..3f4f24f --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/utils/ViewBindingUtil.kt @@ -0,0 +1,76 @@ +package com.holo.architecture.utils + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.ComponentActivity +import androidx.databinding.ViewDataBinding +import androidx.fragment.app.Fragment +import androidx.viewbinding.ViewBinding +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.ParameterizedType + +/** + * + * + * @Author holo + * @Date 2022/2/9 + */ +object ViewBindingUtil { + + @JvmStatic + fun inflateWithGeneric(genericOwner: Any, layoutInflater: LayoutInflater): VB = + withGenericBindingClass(genericOwner) { clazz -> + clazz.getMethod("inflate", LayoutInflater::class.java).invoke(null, layoutInflater) as VB + }.also { binding -> + if (genericOwner is ComponentActivity && binding is ViewDataBinding) { + binding.lifecycleOwner = genericOwner + } + } + + @JvmStatic + fun inflateWithGeneric(genericOwner: Any, parent: ViewGroup): VB = + inflateWithGeneric(genericOwner, LayoutInflater.from(parent.context), parent, false) + + @JvmStatic + fun inflateWithGeneric(genericOwner: Any, layoutInflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean): VB = + withGenericBindingClass(genericOwner) { clazz -> + clazz.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java) + .invoke(null, layoutInflater, parent, attachToParent) as VB + }.also { binding -> + if (genericOwner is Fragment && binding is ViewDataBinding) { + binding.lifecycleOwner = genericOwner.viewLifecycleOwner + } + } + + @JvmStatic + fun bindWithGeneric(genericOwner: Any, view: View): VB = + withGenericBindingClass(genericOwner) { clazz -> + clazz.getMethod("bind", View::class.java).invoke(null, view) as VB + }.also { binding -> + if (genericOwner is Fragment && binding is ViewDataBinding) { + binding.lifecycleOwner = genericOwner.viewLifecycleOwner + } + } + + private fun withGenericBindingClass(genericOwner: Any, block: (Class) -> VB): VB { + var genericSuperclass = genericOwner.javaClass.genericSuperclass + var superclass = genericOwner.javaClass.superclass + while (superclass != null) { + if (genericSuperclass is ParameterizedType) { + genericSuperclass.actualTypeArguments.forEach { + try { + return block.invoke(it as Class) + } catch (e: NoSuchMethodException) { + } catch (e: ClassCastException) { + } catch (e: InvocationTargetException) { + throw e.targetException + } + } + } + genericSuperclass = superclass.genericSuperclass + superclass = superclass.superclass + } + throw IllegalArgumentException("There is no generic of ViewBinding.") + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/utils/flowbus/ApplicationScopeViewModelProvider.kt b/BaseMvvm/src/main/java/com/holo/architecture/utils/flowbus/ApplicationScopeViewModelProvider.kt new file mode 100644 index 0000000..0a36082 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/utils/flowbus/ApplicationScopeViewModelProvider.kt @@ -0,0 +1,33 @@ +package com.holo.architecture.utils.flowbus + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner +import com.holo.architecture.initializer.appContext + +/** + * Application级别的ViewModel功能 + * + * @Author holo + * @Date 2022/3/4 + */ +object ApplicationScopeViewModelProvider : ViewModelStoreOwner { + + private val eventViewModelStore: ViewModelStore = ViewModelStore() + + override fun getViewModelStore(): ViewModelStore { + return eventViewModelStore + } + + private val mApplicationProvider: ViewModelProvider by lazy { + ViewModelProvider( + ApplicationScopeViewModelProvider, + ViewModelProvider.AndroidViewModelFactory.getInstance(appContext) + ) + } + + fun getApplicationScopeViewModel(modelClass: Class): T { + return mApplicationProvider[modelClass] + } +} diff --git a/BaseMvvm/src/main/java/com/holo/architecture/utils/flowbus/EventBusCore.kt b/BaseMvvm/src/main/java/com/holo/architecture/utils/flowbus/EventBusCore.kt new file mode 100644 index 0000000..d96600f --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/utils/flowbus/EventBusCore.kt @@ -0,0 +1,121 @@ +package com.holo.architecture.utils.flowbus + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.holo.architecture.ext.logW +import com.holo.architecture.logger.KLog +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlin.collections.set + +/** + * FlowBus核心类 + * + * @Author holo + * @Date 2022/3/4 + */ +class EventBusCore : ViewModel() { + + /** + * 正常事件 + */ + private val eventFlows: HashMap> = HashMap() + + /** + * 粘性事件 + */ + private val stickyEventFlows: HashMap> = HashMap() + + private fun getEventFlow(eventName: String, isSticky: Boolean): MutableSharedFlow { + return if (isSticky) { + stickyEventFlows[eventName] + } else { + eventFlows[eventName] + } ?: MutableSharedFlow( + replay = if (isSticky) 1 else 0, + extraBufferCapacity = Int.MAX_VALUE //避免挂起导致数据发送失败 + ).also { + if (isSticky) { + stickyEventFlows[eventName] = it + } else { + eventFlows[eventName] = it + } + } + } + + fun observeEvent( + lifecycleOwner: LifecycleOwner, + eventName: String, + minState: Lifecycle.State, + dispatcher: CoroutineDispatcher, + isSticky: Boolean, + onReceived: (T) -> Unit + ): Job { + "observe Event:$eventName".logW("FlowBus") + return lifecycleOwner.launchWhenStateAtLeast(minState) { + getEventFlow(eventName, isSticky).collect { value -> + this.launch(dispatcher) { + invokeReceived(value, onReceived) + } + } + } + } + + suspend fun observeWithoutLifecycle( + eventName: String, + isSticky: Boolean, + onReceived: (T) -> Unit + ) { + getEventFlow(eventName, isSticky).collect { value -> + invokeReceived(value, onReceived) + } + } + + + fun postEvent(eventName: String, value: Any, timeMillis: Long) { + "post Event:$eventName".logW("FlowBus") + listOfNotNull( + getEventFlow(eventName, false), + getEventFlow(eventName, true) + ).forEach { flow -> + viewModelScope.launch { + delay(timeMillis) + flow.emit(value) + } + } + } + + + fun removeStickEvent(eventName: String) { + stickyEventFlows.remove(eventName) + } + + fun clearStickEvent(eventName: String) { + stickyEventFlows[eventName]?.resetReplayCache() + } + + + private fun invokeReceived(value: Any, onReceived: (T) -> Unit) { + try { + onReceived.invoke(value as T) + } catch (e: ClassCastException) { + KLog.e("FlowBus", "class cast error on message received: $value", e) + } catch (e: Exception) { + KLog.e("FlowBus", "error on message received: $value", e) + } + } + + + fun getEventObserverCount(eventName: String): Int { + val stickyObserverCount = stickyEventFlows[eventName]?.subscriptionCount?.value ?: 0 + val normalObserverCount = eventFlows[eventName]?.subscriptionCount?.value ?: 0 + return stickyObserverCount + normalObserverCount + } + +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/utils/flowbus/EventUtils.kt b/BaseMvvm/src/main/java/com/holo/architecture/utils/flowbus/EventUtils.kt new file mode 100644 index 0000000..3ae96aa --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/utils/flowbus/EventUtils.kt @@ -0,0 +1,105 @@ +package com.holo.architecture.utils.flowbus + +import androidx.lifecycle.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +/** + * 获取事件监听者数量 + * ``` + * getEventObserverCount(T::class.java) + * ``` + * @param event Class + * @return Int + */ +inline fun getEventObserverCount(event: Class): Int { + return ApplicationScopeViewModelProvider.getApplicationScopeViewModel(EventBusCore::class.java) + .getEventObserverCount(event.name) +} + +/** + * 获取事件监听者数量 + * ``` + * getEventObserverCount(fragment, T::class.java) + * getEventObserverCount(activity, T::class.java) + * ``` + * @param scope ViewModelStoreOwner + * @param event Class + * @return Int + */ +inline fun getEventObserverCount(scope: ViewModelStoreOwner, event: Class): Int { + return ViewModelProvider(scope).get(EventBusCore::class.java) + .getEventObserverCount(event.name) +} + + +/** + * 移除粘性事件 + *``` + * removeStickyEvent(T::class.java) + *``` + * @param event Class + */ +inline fun removeStickyEvent(event: Class) { + ApplicationScopeViewModelProvider.getApplicationScopeViewModel(EventBusCore::class.java) + .removeStickEvent(event.name) +} + +/** + * 移除粘性事件 + *``` + * removeStickyEvent(fragment, T::class.java) + * removeStickyEvent(activity, T::class.java) + *``` + * @param scope ViewModelStoreOwner + * @param event Class + */ +inline fun removeStickyEvent(scope: ViewModelStoreOwner, event: Class) { + ViewModelProvider(scope).get(EventBusCore::class.java) + .removeStickEvent(event.name) +} + + +/** + * 清除粘性事件缓存,粘性事件实例还在,但没有了ReplayCache,新观察者不会收到回调 + * ``` + * clearStickyEvent(T::class.java) + * ``` + * @param event Class + */ +inline fun clearStickyEvent(event: Class) { + ApplicationScopeViewModelProvider.getApplicationScopeViewModel(EventBusCore::class.java) + .clearStickEvent(event.name) +} + +/** + * 清除粘性事件缓存,粘性事件实例还在,但没有了ReplayCache,新观察者不会收到回调 + *``` + * clearStickyEvent(fragment, T::class.java) + * clearStickyEvent(activity, T::class.java) + *``` + * @param scope ViewModelStoreOwner + * @param event Class + */ +inline fun clearStickyEvent(scope: ViewModelStoreOwner, event: Class) { + ViewModelProvider(scope).get(EventBusCore::class.java) + .clearStickEvent(event.name) +} + + +/** + * 生命周期感知 + * @receiver LifecycleOwner + * @param minState State + * @param block [@kotlin.ExtensionFunctionType] SuspendFunction1 + * @return Job + */ +fun LifecycleOwner.launchWhenStateAtLeast( + minState: Lifecycle.State, + block: suspend CoroutineScope.() -> T +): Job { + return lifecycleScope.launch { + lifecycle.whenStateAtLeast(minState, block) + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/utils/flowbus/ObserveEvent.kt b/BaseMvvm/src/main/java/com/holo/architecture/utils/flowbus/ObserveEvent.kt new file mode 100644 index 0000000..3b7605f --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/utils/flowbus/ObserveEvent.kt @@ -0,0 +1,145 @@ +package com.holo.architecture.utils.flowbus + +import androidx.activity.ComponentActivity +import androidx.annotation.MainThread +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import kotlinx.coroutines.* + + +/** + * 监听Application全局范围事件 + * ``` + * //接收 App Scope事件 + * observeEvent { + * ... + * } + * ``` + * @receiver LifecycleOwner + * @param dispatcher CoroutineDispatcher 线程切换,默认主线程[Dispatchers.Main] + * @param minActiveState State 指定可感知的最小生命周期状态,默认[Lifecycle.State.STARTED] + * @param isSticky Boolean 是否粘性事件,默认false + * @param onReceived Function1 + * @return Job + */ +@MainThread +inline fun LifecycleOwner.observeEvent( + dispatcher: CoroutineDispatcher = Dispatchers.Main.immediate, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + isSticky: Boolean = false, + noinline onReceived: (T) -> Unit +): Job { + return ApplicationScopeViewModelProvider.getApplicationScopeViewModel(EventBusCore::class.java) + .observeEvent( + this, + T::class.java.name, + minActiveState, + dispatcher, + isSticky, + onReceived + ) +} + +/** + * 监听Fragment 范围事件 + * ``` + * //接收 Fragment Scope事件 + * observeEvent(scope = fragment) { + * ... + * } + * ``` + * @param scope Fragment 接收事件的fragment + * @param dispatcher CoroutineDispatcher 线程切换,默认主线程[Dispatchers.Main] + * @param minActiveState State 指定可感知的最小生命周期状态,默认[Lifecycle.State.STARTED] + * @param isSticky Boolean 是否粘性事件,默认false + * @param onReceived Function1 + * @return Job + */ +@MainThread +inline fun observeEvent( + scope: Fragment, + dispatcher: CoroutineDispatcher = Dispatchers.Main.immediate, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + isSticky: Boolean = false, + noinline onReceived: (T) -> Unit +): Job { + return ViewModelProvider(scope).get(EventBusCore::class.java) + .observeEvent( + scope, + T::class.java.name, + minActiveState, + dispatcher, + isSticky, + onReceived + ) +} + +/** + * Fragment 监听Activity 范围事件 + * ``` + * //接收 Activity Scope事件 + * observeEvent(scope = requireActivity()) { + * ... + * } + * ``` + * @param scope ComponentActivity Activity + * @param dispatcher CoroutineDispatcher 线程切换,默认主线程[Dispatchers.Main] + * @param minActiveState State 指定可感知的最小生命周期状态,默认[Lifecycle.State.STARTED] + * @param isSticky Boolean 是否粘性事件,默认false + * @param onReceived Function1 + * @return Job + */ +@MainThread +inline fun observeEvent( + scope: ComponentActivity, + dispatcher: CoroutineDispatcher = Dispatchers.Main.immediate, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + isSticky: Boolean = false, + noinline onReceived: (T) -> Unit +): Job { + return ViewModelProvider(scope).get(EventBusCore::class.java) + .observeEvent( + scope, + T::class.java.name, + minActiveState, + dispatcher, + isSticky, + onReceived + ) +} + +@MainThread +inline fun observeEvent( + coroutineScope: CoroutineScope, + isSticky: Boolean = false, + noinline onReceived: (T) -> Unit +): Job { + return coroutineScope.launch { + ApplicationScopeViewModelProvider.getApplicationScopeViewModel(EventBusCore::class.java) + .observeWithoutLifecycle( + T::class.java.name, + isSticky, + onReceived + ) + } +} + +@MainThread +inline fun observeEvent( + scope: ViewModelStoreOwner, + coroutineScope: CoroutineScope, + isSticky: Boolean = false, + noinline onReceived: (T) -> Unit +): Job { + return coroutineScope.launch { + ViewModelProvider(scope).get(EventBusCore::class.java) + .observeWithoutLifecycle( + T::class.java.name, + isSticky, + onReceived + ) + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/utils/flowbus/PostEvent.kt b/BaseMvvm/src/main/java/com/holo/architecture/utils/flowbus/PostEvent.kt new file mode 100644 index 0000000..bd89e72 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/utils/flowbus/PostEvent.kt @@ -0,0 +1,37 @@ +package com.holo.architecture.utils.flowbus + +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner + +/** + * Application全局范围的事件 + * ``` + * 示例: + * postEvent(AppScopeEvent("form TestFragment")) + * ``` + * @param event T 事件 + * @param timeMillis Long 延迟发送,毫秒 + */ +inline fun postEvent(event: T, timeMillis: Long = 0L) { + ApplicationScopeViewModelProvider.getApplicationScopeViewModel(EventBusCore::class.java) + .postEvent(T::class.java.name, event!!, timeMillis) +} + + +/** + * 限定范围的事件 + * ``` + * 示例: + * //fragment 内部范围 + * postEvent(fragment,FragmentEvent("form TestFragment")) + * + * //Activity 内部范围 + * postEvent(requireActivity(),ActivityEvent("form TestFragment")) + * ``` + * @param event T 事件 + * @param timeMillis Long 延迟发送,毫秒 + */ +inline fun postEvent(scope: ViewModelStoreOwner, event: T, timeMillis: Long = 0L) { + ViewModelProvider(scope).get(EventBusCore::class.java) + .postEvent(T::class.java.name, event!!, timeMillis) +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/utils/netstatus/NetType.kt b/BaseMvvm/src/main/java/com/holo/architecture/utils/netstatus/NetType.kt new file mode 100644 index 0000000..58335bc --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/utils/netstatus/NetType.kt @@ -0,0 +1,23 @@ +package com.holo.architecture.utils.netstatus + +/** + * 网络状态类型 + * + * @Author holo + * @Date 2022/1/14 + */ +@Target(AnnotationTarget.TYPE) +@kotlin.annotation.Retention(AnnotationRetention.SOURCE) +annotation class NetType { + + companion object { + // wifi + const val WIFI = "WIFI" + // 手机网络 + const val MOBILE_NET = "NET" + // 未识别网络 + const val NET_UNKNOWN = "NET_UNKNOWN" + // 没有网络 + const val NONE = "NONE" + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/utils/netstatus/NetUtils.kt b/BaseMvvm/src/main/java/com/holo/architecture/utils/netstatus/NetUtils.kt new file mode 100644 index 0000000..f637990 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/utils/netstatus/NetUtils.kt @@ -0,0 +1,117 @@ +package com.holo.architecture.utils.netstatus + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.wifi.WifiManager +import android.os.Build +import com.holo.architecture.network.NetworkStateManager + +/** + * # 网络连接状态工具类 + * + * @author holo + * @since 2022/1/13 + */ +object NetUtils { + + /** + * 手机是否已经联网 + * @return Boolean + */ + fun isConnected(): Boolean { + return NetworkStateManager.netType != NetType.NONE + } + + /** + * 检查当前连接的网络是否为5G WI-FI + */ + fun is5GWifiConnected(context: Context): Boolean { + val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val wifiInfo = wifiManager.connectionInfo ?: return false + // 频段 + var frequency = 0 + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.LOLLIPOP) { + frequency = wifiInfo.frequency + } else { + val ssid = wifiInfo.ssid ?: return false + if (ssid.length < 2) return false + val sid = ssid.substring(1, ssid.length - 1) + for (scan in wifiManager.scanResults) { + if (scan.SSID == sid) { + frequency = scan.frequency + break + } + } + } + return frequency in 4900..5900 + } + + /** + * 获取当前连接Wi-Fi名 + * # 如果没有定位权限,获取到的名字将为 unknown ssid + */ + fun getConnectedWifiSSID(context: Context): String { + val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val wifiInfo = wifiManager.connectionInfo ?: return "" + return wifiInfo.ssid.substring(1, wifiInfo.ssid.length - 1) + } + + /** + * 获取网络状态 + */ + fun getNetStatus(context: Context): @NetType String { + val manager = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val net = manager.activeNetwork + val ties = manager.getNetworkCapabilities(net) ?: return NetType.NONE + return getNetStatus(ties) + } else { + val netInfo = manager.activeNetworkInfo + if (netInfo == null || !netInfo.isAvailable) { + return NetType.NONE + } + return when (netInfo.type) { + ConnectivityManager.TYPE_MOBILE -> NetType.MOBILE_NET + ConnectivityManager.TYPE_WIFI -> NetType.WIFI + else -> NetType.NET_UNKNOWN + } + } + } + + /** + * 获取网络状态 + */ + fun getNetStatus(ties: NetworkCapabilities): @NetType String { + return if (!ties.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) { + NetType.NONE + } else { + if (ties.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + ties.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE) + ) { + // 使用WI-FI + NetType.WIFI + } else if (ties.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || + ties.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) + ) { + // 使用蜂窝网络 + NetType.MOBILE_NET + } else { + // 未知网络,包括蓝牙、VPN、LoWPAN + NetType.NET_UNKNOWN + } + } + } + + /** + * 获取连接中的网络 + */ + fun getActiveNetwork(context: Context): Network? { + val manager = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return manager.activeNetwork + } + return null + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/widget/TitleBar.kt b/BaseMvvm/src/main/java/com/holo/architecture/widget/TitleBar.kt new file mode 100644 index 0000000..95e42f2 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/widget/TitleBar.kt @@ -0,0 +1,128 @@ +package com.holo.architecture.widget + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Canvas +import android.graphics.Color +import android.util.AttributeSet +import android.view.Gravity +import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.graphics.drawable.toDrawable +import com.holo.architecture.R +import com.holo.architecture.databinding.LayoutTitleBarBinding +import com.holo.architecture.ext.clickNoRepeat +import com.holo.architecture.ext.dp2px +import com.holo.architecture.ext.sp2px +import com.holo.architecture.ext.toVisible + +/** + * + * + * @Author holo + * @Date 2022/2/16 + */ +class TitleBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + ConstraintLayout(context, attrs, defStyleAttr) { + + private lateinit var binding: LayoutTitleBarBinding + + init { + initLayout(context, attrs) + } + + private fun initLayout(context: Context, attrs: AttributeSet?) { + val root = View.inflate(context, R.layout.layout_title_bar, this) + binding = LayoutTitleBarBinding.bind(root) + + val typed = context.obtainStyledAttributes(attrs, R.styleable.TitleBar) + val barBackground = typed.getDrawable(R.styleable.TitleBar_barBackground) ?: Color.WHITE.toDrawable() + + val navigationIcon = typed.getDrawable(R.styleable.TitleBar_navigationIcon) + val navigationIconTint = typed.getColor(R.styleable.TitleBar_navigationIconTint, Color.parseColor("#272727")) + + val title = typed.getString(R.styleable.TitleBar_title) + val titleTextColor = typed.getColor(R.styleable.TitleBar_titleTextColor, Color.parseColor("#272727")) + val titleTextSize = typed.getDimension(R.styleable.TitleBar_titleTextSize, 18f.sp2px()) + val titleCentered = typed.getBoolean(R.styleable.TitleBar_titleCentered, true) + + val menuIcon = typed.getDrawable(R.styleable.TitleBar_menuIcon) + + val menuTitle = typed.getString(R.styleable.TitleBar_menuTitle) + val menuTitleColor = typed.getColor(R.styleable.TitleBar_menuTitleColor, Color.parseColor("#272727")) + val menuTitleSize = typed.getDimension(R.styleable.TitleBar_menuTitleSize, 15f.sp2px()) + + val menuBtnTitle = typed.getString(R.styleable.TitleBar_menuBtnTitle) + val menuBtnTextColor = typed.getColor(R.styleable.TitleBar_menuBtnTextColor, Color.WHITE) + val menuBtnTextSize = typed.getDimension(R.styleable.TitleBar_menuBtnTextSize, 15f.sp2px()) + val menuBtnBackgroundColor = typed.getColor(R.styleable.TitleBar_menuBtnBackgroundColor, -1) + val menuBtnBackgroundDrawable = typed.getDrawable(R.styleable.TitleBar_menuBtnBackgroundDrawable) + val menuBtnCornerRadius = typed.getDimensionPixelSize(R.styleable.TitleBar_menuBtnCornerRadius, 5.dp2px()) + + val showDivider = typed.getBoolean(R.styleable.TitleBar_barShowDivider, false) + typed.recycle() + + binding.titleBar.background = barBackground + + navigationIcon?.let { icon -> + if (navigationIcon != null) { + binding.ivNavigation.setImageDrawable(icon) + } else { + binding.ivNavigation.setImageResource(R.drawable.ic_back) + } + } + binding.ivNavigation.imageTintList = ColorStateList.valueOf(navigationIconTint) + + binding.barTitle.text = title + binding.barTitle.setTextColor(titleTextColor) + binding.barTitle.paint.textSize = titleTextSize + binding.barTitle.gravity = if (titleCentered) Gravity.CENTER else Gravity.START + + binding.ivMenu.toVisible(menuIcon != null) + menuIcon?.let { icon -> + binding.ivMenu.setImageDrawable(icon) + } + + binding.tvMenu.toVisible(!menuTitle.isNullOrBlank()) + if (!menuTitle.isNullOrBlank()) { + binding.tvMenu.text = menuTitle + binding.tvMenu.setTextColor(menuTitleColor) + binding.tvMenu.paint.textSize = menuTitleSize + } + + binding.btnMenu.toVisible(!menuBtnTitle.isNullOrBlank()) + if (!menuBtnTitle.isNullOrBlank()) { + binding.btnMenu.text = menuBtnTitle + binding.btnMenu.setTextColor(menuBtnTextColor) + binding.btnMenu.paint.textSize = menuBtnTextSize + + if (menuBtnBackgroundColor != -1) { + binding.btnMenu.backgroundTintList = ColorStateList.valueOf(menuBtnBackgroundColor) + } + menuBtnBackgroundDrawable?.let { drawable -> + binding.btnMenu.background = drawable + binding.btnMenu.backgroundTintList = null + } + binding.btnMenu.cornerRadius = menuBtnCornerRadius + } + binding.barDivider.toVisible(showDivider) + } + + override fun onDraw(canvas: Canvas?) { + super.onDraw(canvas) + + val params = binding.barTitle.layoutParams as LayoutParams + params.marginStart = binding.clMenu.width + params.marginEnd = binding.clMenu.width + binding.barTitle.layoutParams = params + } + + fun setTitle(title: String) { + binding.barTitle.text = title + } + + fun init(title: String = "", backAction: () -> Unit) { + setTitle(title) + binding.ivNavigation.clickNoRepeat { backAction.invoke() } + } +} \ No newline at end of file diff --git a/BaseMvvm/src/main/java/com/holo/architecture/widget/TransBehavior.kt b/BaseMvvm/src/main/java/com/holo/architecture/widget/TransBehavior.kt new file mode 100644 index 0000000..8949939 --- /dev/null +++ b/BaseMvvm/src/main/java/com/holo/architecture/widget/TransBehavior.kt @@ -0,0 +1,120 @@ +package com.holo.architecture.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.view.LayoutInflater +import android.view.ViewGroup +import android.view.Window +import android.view.WindowManager +import androidx.annotation.ColorInt +import androidx.core.view.isVisible +import com.afollestad.materialdialogs.DialogBehavior +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.R +import com.afollestad.materialdialogs.WhichButton +import com.afollestad.materialdialogs.actions.getActionButton +import com.afollestad.materialdialogs.internal.main.DialogLayout +import com.afollestad.materialdialogs.utils.MDUtil.getWidthAndHeight +import kotlin.math.min + +/** + * + * + * @Author holo + * @Date 2022/2/17 + */ +object TransBehavior : DialogBehavior { + override fun getThemeRes(isDark: Boolean): Int { + return if (isDark) { + R.style.MD_Dark + } else { + R.style.MD_Light + } + } + + @SuppressLint("InflateParams") + override fun createView( + creatingContext: Context, + dialogWindow: Window, + layoutInflater: LayoutInflater, + dialog: MaterialDialog + ): ViewGroup { + return layoutInflater.inflate( + R.layout.md_dialog_base, + null, + false + ) as ViewGroup + } + + override fun getDialogLayout(root: ViewGroup): DialogLayout { + return root as DialogLayout + } + + override fun setWindowConstraints( + context: Context, + window: Window, + view: DialogLayout, + maxWidth: Int? + ) { + if (maxWidth == 0) { + // Postpone + return + } + + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + val wm = window.windowManager ?: return + val res = context.resources + val (windowWidth, windowHeight) = wm.getWidthAndHeight() + + val windowVerticalPadding = + res.getDimensionPixelSize(R.dimen.md_dialog_vertical_margin) + view.maxHeight = windowHeight - windowVerticalPadding * 2 + + val lp = WindowManager.LayoutParams().apply { + copyFrom(window.attributes) + + val windowHorizontalPadding = + res.getDimensionPixelSize(R.dimen.md_dialog_horizontal_margin) + val calculatedWidth = windowWidth - windowHorizontalPadding * 2 + val actualMaxWidth = + maxWidth ?: res.getDimensionPixelSize(R.dimen.md_dialog_max_width) + width = min(actualMaxWidth, calculatedWidth) + } + window.attributes = lp + } + + override fun setBackgroundColor( + view: DialogLayout, + @ColorInt color: Int, + cornerRadius: Float + ) { + view.cornerRadii = floatArrayOf( + cornerRadius, cornerRadius, // top left + cornerRadius, cornerRadius, // top right + 0f, 0f, // bottom left + 0f, 0f // bottom right + ) + view.background = GradientDrawable().apply { + this.cornerRadius = cornerRadius + setColor(Color.TRANSPARENT) + } + } + + override fun onPreShow(dialog: MaterialDialog) = Unit + + override fun onPostShow(dialog: MaterialDialog) { + val negativeBtn = dialog.getActionButton(WhichButton.NEGATIVE) + if (negativeBtn.isVisible) { + negativeBtn.post { negativeBtn.requestFocus() } + return + } + val positiveBtn = dialog.getActionButton(WhichButton.POSITIVE) + if (positiveBtn.isVisible) { + positiveBtn.post { positiveBtn.requestFocus() } + } + } + + override fun onDismiss(): Boolean = false +} \ No newline at end of file diff --git a/BaseMvvm/src/main/res/anim/fragment_close_enter.xml b/BaseMvvm/src/main/res/anim/fragment_close_enter.xml new file mode 100644 index 0000000..506fd30 --- /dev/null +++ b/BaseMvvm/src/main/res/anim/fragment_close_enter.xml @@ -0,0 +1,40 @@ + + + + + + \ No newline at end of file diff --git a/BaseMvvm/src/main/res/anim/fragment_close_exit.xml b/BaseMvvm/src/main/res/anim/fragment_close_exit.xml new file mode 100644 index 0000000..be1dd7e --- /dev/null +++ b/BaseMvvm/src/main/res/anim/fragment_close_exit.xml @@ -0,0 +1,41 @@ + + + + + + \ No newline at end of file diff --git a/BaseMvvm/src/main/res/anim/fragment_fade_enter.xml b/BaseMvvm/src/main/res/anim/fragment_fade_enter.xml new file mode 100644 index 0000000..939f446 --- /dev/null +++ b/BaseMvvm/src/main/res/anim/fragment_fade_enter.xml @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/BaseMvvm/src/main/res/anim/fragment_fade_exit.xml b/BaseMvvm/src/main/res/anim/fragment_fade_exit.xml new file mode 100644 index 0000000..3b4d7ae --- /dev/null +++ b/BaseMvvm/src/main/res/anim/fragment_fade_exit.xml @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/BaseMvvm/src/main/res/anim/fragment_fast_out_extra_slow_in.xml b/BaseMvvm/src/main/res/anim/fragment_fast_out_extra_slow_in.xml new file mode 100644 index 0000000..c3baa5d --- /dev/null +++ b/BaseMvvm/src/main/res/anim/fragment_fast_out_extra_slow_in.xml @@ -0,0 +1,18 @@ + + + + diff --git a/BaseMvvm/src/main/res/anim/fragment_open_enter.xml b/BaseMvvm/src/main/res/anim/fragment_open_enter.xml new file mode 100644 index 0000000..41dfbea --- /dev/null +++ b/BaseMvvm/src/main/res/anim/fragment_open_enter.xml @@ -0,0 +1,42 @@ + + + + + + \ No newline at end of file diff --git a/BaseMvvm/src/main/res/anim/fragment_open_exit.xml b/BaseMvvm/src/main/res/anim/fragment_open_exit.xml new file mode 100644 index 0000000..9b97832 --- /dev/null +++ b/BaseMvvm/src/main/res/anim/fragment_open_exit.xml @@ -0,0 +1,41 @@ + + + + + + + \ No newline at end of file diff --git a/BaseMvvm/src/main/res/drawable/ic_back.xml b/BaseMvvm/src/main/res/drawable/ic_back.xml new file mode 100644 index 0000000..6a94943 --- /dev/null +++ b/BaseMvvm/src/main/res/drawable/ic_back.xml @@ -0,0 +1,10 @@ + + + diff --git a/BaseMvvm/src/main/res/layout/dialog_loading.xml b/BaseMvvm/src/main/res/layout/dialog_loading.xml new file mode 100644 index 0000000..3b83a0d --- /dev/null +++ b/BaseMvvm/src/main/res/layout/dialog_loading.xml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/BaseMvvm/src/main/res/layout/layout_title_bar.xml b/BaseMvvm/src/main/res/layout/layout_title_bar.xml new file mode 100644 index 0000000..99e04ad --- /dev/null +++ b/BaseMvvm/src/main/res/layout/layout_title_bar.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BaseMvvm/src/main/res/values/attrs.xml b/BaseMvvm/src/main/res/values/attrs.xml new file mode 100644 index 0000000..32bbc5b --- /dev/null +++ b/BaseMvvm/src/main/res/values/attrs.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BaseMvvm/src/main/res/values/colors.xml b/BaseMvvm/src/main/res/values/colors.xml new file mode 100644 index 0000000..572cd52 --- /dev/null +++ b/BaseMvvm/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #FFF + #00000000 + \ No newline at end of file diff --git a/BaseMvvm/src/main/res/values/dimens.xml b/BaseMvvm/src/main/res/values/dimens.xml new file mode 100644 index 0000000..dff62f3 --- /dev/null +++ b/BaseMvvm/src/main/res/values/dimens.xml @@ -0,0 +1,6 @@ + + + + 17sp + 17sp + \ No newline at end of file diff --git a/BaseMvvm/src/main/res/values/styles.xml b/BaseMvvm/src/main/res/values/styles.xml new file mode 100644 index 0000000..fbb1911 --- /dev/null +++ b/BaseMvvm/src/main/res/values/styles.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0eedc1f --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# WanAndroid diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..0140f72 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,121 @@ +plugins { + id("com.android.application") + kotlin("android") + kotlin("kapt") + id("kotlin-parcelize") + id("dagger.hilt.android.plugin") +} + +android { + compileSdk = ModuleConfig.compileSdkVersion + + defaultConfig { + applicationId = "com.holo.wanandroid" + minSdk = ModuleConfig.minSdkVersion + targetSdk = ModuleConfig.targetSdkVersion + versionCode = ModuleConfig.versionCode + versionName = ModuleConfig.versionName + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + javaCompileOptions.annotationProcessorOptions.arguments["room.schemaLocation"] = "$projectDir/schemas" + javaCompileOptions.annotationProcessorOptions.arguments["dagger.hilt.disableModulesHaveInstallInCheck"] = "true" + } + buildFeatures { + viewBinding = true + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation("androidx.appcompat:appcompat:1.3.0") + implementation("com.google.android.material:material:1.4.0") + implementation("androidx.constraintlayout:constraintlayout:2.0.4") + //单元测试组件库 + testImplementation(Testing.jUnit) + androidTestImplementation(Testing.extJUnit) + androidTestImplementation(Testing.testRunner) + androidTestImplementation(Testing.espresso) + + // BaseMvvm + implementation(project(":BaseMvvm")) + + // Android + implementation(Android.appcompat) + implementation(Android.recyclerView) + implementation(Android.activityKtx) + implementation(Android.coreKtx) + implementation(Android.constraintLayout) + implementation(Android.swipeRefreshLayout) + implementation(Android.material) + implementation(Android.flexBox) + implementation(MaterialDialogs.core) + implementation(MaterialDialogs.bottomSheets) + implementation(MaterialDialogs.lifecycle) + + // Navigation + implementation(Navigation.uiKtx) + implementation(Navigation.fragmentKtx) + + // Architecture Components + implementation(Lifecycle.viewModel) + implementation(Lifecycle.liveData) + implementation(Lifecycle.runtimeKtx) + + // Hilt + Dagger + implementation(Hilt.hiltAndroid) + implementation(Hilt.hiltViewModel) + kapt(Hilt.daggerCompiler) + kapt(Hilt.hiltCompiler) + + // Room components + implementation(Room.runtime) + implementation(Room.ktx) + kapt(Room.compiler) + + // Coil-kt + implementation(Depends.coil) + + // Retrofit + implementation(Retrofit.retrofit) + implementation(Retrofit.convertGson) + implementation(Retrofit.loggingInterceptor) + implementation(Depends.persistentCookieJar) + + //腾讯内存映射组件 + implementation(Depends.mmkv) + + //webView组件 + implementation(Depends.agentWeb) + + //阴影 + implementation(Depends.shadowLayout) + + implementation(Depends.BRVAH) + implementation(SmartRefreshLayout.core) + implementation(SmartRefreshLayout.headerMaterial) + + implementation(Depends.lottie) + implementation(Depends.magicIndicator) + + implementation(Depends.banner) + implementation(Depends.likeView) + implementation(Depends.loadState) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..ff59496 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/holo/wanandroid/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/holo/wanandroid/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..9d4e8e4 --- /dev/null +++ b/app/src/androidTest/java/com/holo/wanandroid/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.holo.wanandroid + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.holo.wanandroid", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fc90c73 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/Constant.kt b/app/src/main/java/com/holo/wanandroid/Constant.kt new file mode 100644 index 0000000..99f8546 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/Constant.kt @@ -0,0 +1,14 @@ +package com.holo.wanandroid + +/** + * + * + * @Author holo + * @Date 2022/4/16 + */ +object Constant { + const val KEY_ID = "id" + const val KEY_TREE_BEAN = "tree_bean" + const val KEY_WEB_URL = "web_url" + const val KEY_WEB_TITLE = "web_title" +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/WanApp.kt b/app/src/main/java/com/holo/wanandroid/WanApp.kt new file mode 100644 index 0000000..11db009 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/WanApp.kt @@ -0,0 +1,18 @@ +package com.holo.wanandroid + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +@HiltAndroidApp +class WanApp : Application() { + + override fun onCreate() { + super.onCreate() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/data/dto/ArticleBean.kt b/app/src/main/java/com/holo/wanandroid/data/dto/ArticleBean.kt new file mode 100644 index 0000000..4923fa2 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/data/dto/ArticleBean.kt @@ -0,0 +1,56 @@ +package com.holo.wanandroid.data.dto + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +data class ArticleBean( + var apkLink: String = "", + var audit: Int = 0, + var author: String = "", + var canEdit: Boolean = false, + var chapterId: Int = 0, + var chapterName: String = "", + var collect: Boolean = false, + var courseId: Int = 0, + var desc: String = "", + var descMd: String = "", + var envelopePic: String = "", + var fresh: Boolean = false, + var host: String = "", + var id: Int = 0, + var link: String = "", + var niceDate: String = "", + var niceShareDate: String = "", + var origin: String = "", + var prefix: String = "", + var projectLink: String = "", + var publishTime: Long = 0L, + var realSuperChapterId: Int = 0, + var selfVisible: Int = 0, + var shareDate: Long = 0L, + var shareUser: String = "", + var superChapterId: Int = 0, + var superChapterName: String = "", + var tags: List = listOf(), + var title: String = "", + var type: Int = 0, + var userId: Int = 0, + var visible: Int = 0, + var zan: Int = 0 +) { + fun authorShow(): String { + return if (author.isNullOrEmpty()) { + shareUser + } else { + author + } + } +} + +data class ArticleTag( + var name: String = "", + var url: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/data/dto/BannerBean.kt b/app/src/main/java/com/holo/wanandroid/data/dto/BannerBean.kt new file mode 100644 index 0000000..abc5ab0 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/data/dto/BannerBean.kt @@ -0,0 +1,18 @@ +package com.holo.wanandroid.data.dto + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +data class BannerBean( + val desc: String, + val id: Int, + val imagePath: String, + val isVisible: Int, + val order: Int, + val title: String, + val type: Int, + val url: String +) \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/data/dto/CategoryBean.kt b/app/src/main/java/com/holo/wanandroid/data/dto/CategoryBean.kt new file mode 100644 index 0000000..36212ca --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/data/dto/CategoryBean.kt @@ -0,0 +1,27 @@ +package com.holo.wanandroid.data.dto + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +@Parcelize +data class CategoryBean( + val author: String = "", + val children: List = listOf(), + val courseId: Int = 0, + val cover: String = "", + val desc: String = "", + val id: Int = 0, + val lisense: String = "", + val lisenseLink: String = "", + val name: String = "", + val order: Int = 0, + val parentChapterId: Int = 0, + val userControlSetTop: Boolean = false, + val visible: Int = 0 +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/data/dto/CollectBean.kt b/app/src/main/java/com/holo/wanandroid/data/dto/CollectBean.kt new file mode 100644 index 0000000..2f98803 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/data/dto/CollectBean.kt @@ -0,0 +1,37 @@ +package com.holo.wanandroid.data.dto + +import com.chad.library.adapter.base.entity.MultiItemEntity + +/** + * + * + * @Author holo + * @Date 2022/4/17 + */ +data class CollectBean( + val author: String, + val chapterId: Int, + val chapterName: String, + val courseId: Int, + val desc: String, + val envelopePic: String, + val id: Int, + val link: String, + val niceDate: String, + val origin: String, + val originId: Int, + val publishTime: Long, + val title: String, + val userId: Int, + val visible: Int, + val zan: Int +) : MultiItemEntity { + + override val itemType: Int + get() = if (envelopePic.isNullOrEmpty()) CollectType.COLLECT_ARTICLE else CollectType.COLLECT_PROJECTS +} + +object CollectType { + const val COLLECT_ARTICLE = 0 + const val COLLECT_PROJECTS = 1 +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/data/dto/CollectUrlBean.kt b/app/src/main/java/com/holo/wanandroid/data/dto/CollectUrlBean.kt new file mode 100644 index 0000000..e7caef2 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/data/dto/CollectUrlBean.kt @@ -0,0 +1,17 @@ +package com.holo.wanandroid.data.dto + +/** + * + * + * @Author holo + * @Date 2022/4/17 + */ +data class CollectUrlBean( + var icon: String, + var id: Int, + var link: String, + var name: String, + var order: Int, + var userId: Int, + var visible: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/data/dto/IntegralBean.kt b/app/src/main/java/com/holo/wanandroid/data/dto/IntegralBean.kt new file mode 100644 index 0000000..b737b6d --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/data/dto/IntegralBean.kt @@ -0,0 +1,21 @@ +package com.holo.wanandroid.data.dto + +/** + * + * + * @Author holo + * @Date 2022/4/18 + */ +data class IntegralBean( + val coinCount: Int = 0, + val level: Int = 0, + val nickname: String = "", + val rank: String = "", + val userId: Int = 0, + val username: String = "" +){ + + fun getShowName(): String { + return nickname.ifEmpty { username } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/data/dto/IntegralInfoBean.kt b/app/src/main/java/com/holo/wanandroid/data/dto/IntegralInfoBean.kt new file mode 100644 index 0000000..545a4d2 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/data/dto/IntegralInfoBean.kt @@ -0,0 +1,18 @@ +package com.holo.wanandroid.data.dto + +/** + * + * + * @Author holo + * @Date 2022/4/18 + */ +data class IntegralInfoBean( + val coinCount: Int, + val date: Long, + val desc: String, + val id: Int, + val reason: String, + val type: Int, + val userId: Int, + val userName: String +) \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/data/dto/NavigationBean.kt b/app/src/main/java/com/holo/wanandroid/data/dto/NavigationBean.kt new file mode 100644 index 0000000..5962d21 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/data/dto/NavigationBean.kt @@ -0,0 +1,13 @@ +package com.holo.wanandroid.data.dto + +/** + * + * + * @Author holo + * @Date 2022/4/16 + */ +data class NavigationBean( + val articles: List = listOf(), + val cid: Int = 0, + val name: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/data/dto/ProjectBean.kt b/app/src/main/java/com/holo/wanandroid/data/dto/ProjectBean.kt new file mode 100644 index 0000000..bae3ffb --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/data/dto/ProjectBean.kt @@ -0,0 +1,51 @@ +package com.holo.wanandroid.data.dto + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +data class ProjectBean( + var apkLink: String = "", + var audit: Int = 0, + var author: String = "", + var canEdit: Boolean = false, + var chapterId: Int = 0, + var chapterName: String = "", + var collect: Boolean = false, + var courseId: Int = 0, + var desc: String = "", + var descMd: String = "", + var envelopePic: String = "", + var fresh: Boolean = false, + var host: String = "", + var id: Int = 0, + var link: String = "", + var niceDate: String = "", + var niceShareDate: String = "", + var origin: String = "", + var prefix: String = "", + var projectLink: String = "", + var publishTime: Long = 0L, + var realSuperChapterId: Int = 0, + var selfVisible: Int = 0, + var shareDate: Long = 0L, + var shareUser: String = "", + var superChapterId: Int = 0, + var superChapterName: String = "", + var tags: List, + var title: String = "", + var type: Int = 0, + var userId: Int = 0, + var visible: Int = 0, + var zan: Int = 0 +){ + fun authorShow(): String { + return if (author.isNullOrEmpty()) { + shareUser + } else { + author + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/data/dto/TreeBean.kt b/app/src/main/java/com/holo/wanandroid/data/dto/TreeBean.kt new file mode 100644 index 0000000..bda6d00 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/data/dto/TreeBean.kt @@ -0,0 +1,27 @@ +package com.holo.wanandroid.data.dto + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * + * + * @Author holo + * @Date 2022/4/16 + */ +@Parcelize +data class TreeBean( + val author: String = "", + val children: List = listOf(), + val courseId: Int = 0, + val cover: String = "", + val desc: String = "", + val id: Int = 0, + val lisense: String = "", + val lisenseLink: String = "", + val name: String = "", + val order: Int = 0, + val parentChapterId: Int = 0, + val userControlSetTop: Boolean = false, + val visible: Int = 0 +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/data/dto/UserInfoBean.kt b/app/src/main/java/com/holo/wanandroid/data/dto/UserInfoBean.kt new file mode 100644 index 0000000..98f2767 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/data/dto/UserInfoBean.kt @@ -0,0 +1,33 @@ +package com.holo.wanandroid.data.dto + +import android.os.Parcelable +import com.holo.architecture.base.bean.BaseBean +import kotlinx.parcelize.Parcelize + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +@Parcelize +data class UserInfoBean( + val admin: Boolean, + val chapterTops: List, + val coinCount: Int, + val collectIds: List, + val email: String, + val icon: String, + val id: Int, + val nickname: String, + val password: String, + val publicName: String, + val token: String, + val type: Int, + val username: String +) : BaseBean(), Parcelable { + + fun getShowName(): String { + return nickname.ifEmpty { username } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/data/remote/UserRemoteData.kt b/app/src/main/java/com/holo/wanandroid/data/remote/UserRemoteData.kt new file mode 100644 index 0000000..610f83b --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/data/remote/UserRemoteData.kt @@ -0,0 +1,34 @@ +package com.holo.wanandroid.data.remote + +import com.holo.architecture.base.repository.IRemoteData +import com.holo.architecture.network.ListState +import com.holo.architecture.network.State +import com.holo.wanandroid.data.dto.IntegralBean +import com.holo.wanandroid.data.dto.IntegralInfoBean +import com.holo.wanandroid.data.dto.UserInfoBean +import com.holo.wanandroid.ext.processCall +import com.holo.wanandroid.ext.processCallObj +import com.holo.wanandroid.ext.processListCall +import com.holo.wanandroid.network.WanService +import javax.inject.Inject + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +class UserRemoteData @Inject constructor(private val service: WanService) : IRemoteData { + + suspend fun doLogin(account: String, psw: String): State = processCall { service.login(account, psw) } + + suspend fun doLogout(): State = processCall { service.logout() } + + suspend fun doRegister(account: String, psw: String, rePsw: String): State = processCall { service.register(account, psw, rePsw) } + + suspend fun getMyIntegral(): IntegralBean? = processCallObj { service.getMyIntegral() } + + suspend fun getMyIntegralList(page: Int, refresh: Boolean): ListState = processListCall(refresh) { service.getMyIntegralList(page) } + + suspend fun getIntegralRankList(page: Int, refresh: Boolean): ListState = processListCall(refresh) { service.getIntegralRankList(page) } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/data/remote/WanRemoteData.kt b/app/src/main/java/com/holo/wanandroid/data/remote/WanRemoteData.kt new file mode 100644 index 0000000..cd13fa3 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/data/remote/WanRemoteData.kt @@ -0,0 +1,60 @@ +package com.holo.wanandroid.data.remote + +import com.holo.architecture.base.repository.IRemoteData +import com.holo.architecture.network.ListState +import com.holo.architecture.network.State +import com.holo.wanandroid.data.dto.* +import com.holo.wanandroid.ext.processCall +import com.holo.wanandroid.ext.processCallObj +import com.holo.wanandroid.ext.processListCall +import com.holo.wanandroid.network.WanService +import javax.inject.Inject + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +class WanRemoteData @Inject constructor(private val service: WanService) : IRemoteData { + + suspend fun getBannerList(): State> = processCall { service.getBanner() } + + suspend fun getTopArticleList(): List? = processCallObj { service.getTopArticleList() } + + suspend fun getArticleList(page: Int): ListState = processListCall(page) { service.getArticleList(page) } + + suspend fun getProjectCategories(): List? = processCallObj { service.getProjectCategories() } + + suspend fun getProjectList(page: Int, categoryId: Int): ListState = processListCall(page) { service.getProjectList(page, categoryId) } + + suspend fun getLatestProjectList(page: Int): ListState = processListCall(page) { service.getLatestProjectList(page) } + + suspend fun getPlazaList(page: Int): ListState = processListCall(page) { service.getPlazaList(page) } + + suspend fun getQAList(page: Int): ListState = processListCall(page) { service.getQAList(page) } + + suspend fun getTreeList(): List? = processCallObj { service.getTreeList() } + + suspend fun getTreeChildList(page: Int, treeId: Int): ListState = processListCall(page) { service.getTreeChildList(page, treeId) } + + suspend fun getNavigationList(): List? = processCallObj { service.getNavigationList() } + + suspend fun getPublicNumList(): List? = processCallObj { service.getPublicNumList() } + + suspend fun getPublicArticleList(page: Int, pnId: Int): ListState = processListCall(page) { service.getPublicNumArticleList(page, pnId) } + + suspend fun collect(id: Int): State = processCall { service.collect(id) } + + suspend fun unCollect(id: Int): State = processCall { service.unCollect(id) } + + suspend fun infoUnCollect(id: Int, originId: Int): State = processCall { service.infoUnCollect(id, originId) } + + suspend fun collectUrl(name: String, link: String) = processCall { service.collectUrl(name, link) } + + suspend fun unCollectUrl(id: Int) = processCall { service.unCollectUrl(id) } + + suspend fun getCollectList(page: Int): ListState = processListCall(page) { service.getCollectList(page) } + + suspend fun getCollectUrlList() = processCallObj { service.getCollectUrlList() } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/di/DispatcherModule.kt b/app/src/main/java/com/holo/wanandroid/di/DispatcherModule.kt new file mode 100644 index 0000000..a912a76 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/di/DispatcherModule.kt @@ -0,0 +1,26 @@ +package com.holo.wanandroid.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import javax.inject.Singleton + +/** + * @Desc + * + * @Author holo + * @Date 2022/1/12 + */ +@Module +@InstallIn(SingletonComponent::class) +object DispatcherModule { + + @Provides + @Singleton + fun provideIODispatcher(): CoroutineDispatcher { + return Dispatchers.IO + } +} diff --git a/app/src/main/java/com/holo/wanandroid/di/NetworkModule.kt b/app/src/main/java/com/holo/wanandroid/di/NetworkModule.kt new file mode 100644 index 0000000..4d05e3d --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/di/NetworkModule.kt @@ -0,0 +1,80 @@ +package com.holo.wanandroid.di + +import com.franmontiel.persistentcookiejar.PersistentCookieJar +import com.franmontiel.persistentcookiejar.cache.SetCookieCache +import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor +import com.holo.architecture.initializer.appContext +import com.holo.wanandroid.BuildConfig +import com.holo.wanandroid.network.CacheInterceptor +import com.holo.wanandroid.network.WanApi +import com.holo.wanandroid.network.WanService +import com.holo.wanandroid.network.converter.MyGsonConverterFactory +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.Cache +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import java.io.File +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +/** + * + * + * @Author holo + * @Date 2022/3/22 + */ +@Module +@InstallIn(SingletonComponent::class) +class NetworkModule { + + /** + * OkHttp初始化并注入 + * @return OkHttpClient + */ + @Provides + @Singleton + fun provideOkHttpClient(): OkHttpClient { + val logInterceptor = HttpLoggingInterceptor() + if (BuildConfig.DEBUG) { + logInterceptor.level = HttpLoggingInterceptor.Level.BODY + } else { + logInterceptor.level = HttpLoggingInterceptor.Level.NONE + } + return OkHttpClient.Builder() + .cache(Cache(File(appContext.cacheDir, "wan_cache"), 6 * 1024 * 1024))//设置缓存配置 缓存最大6M + .cookieJar(PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(appContext))) + .addInterceptor(logInterceptor) + .addInterceptor(CacheInterceptor()) + .connectTimeout(WanApi.DEFAULT_TIMEOUT, TimeUnit.SECONDS) + .readTimeout(WanApi.DEFAULT_TIMEOUT, TimeUnit.SECONDS) + .writeTimeout(WanApi.DEFAULT_TIMEOUT, TimeUnit.SECONDS) + .build() + } + + /** + * Retrofit初始化并注入 + * @param okHttpClient OkHttpClient + * @return Retrofit + */ + @Provides + @Singleton + fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit { + return Retrofit.Builder() + .client(okHttpClient) + .baseUrl(WanApi.API_BASE) +// .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(MyGsonConverterFactory.create()) + .build() + } + + @Provides + @Singleton + fun provideChainService(retrofit: Retrofit): WanService { + return retrofit.create(WanService::class.java) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/event/LoginEvent.kt b/app/src/main/java/com/holo/wanandroid/event/LoginEvent.kt new file mode 100644 index 0000000..11a373c --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/event/LoginEvent.kt @@ -0,0 +1,13 @@ +package com.holo.wanandroid.event + +import com.holo.architecture.network.State +import com.holo.wanandroid.data.dto.UserInfoBean + +/** + * + * + * @Author holo + * @Date 2022/4/18 + */ +class LoginEvent(val state: State) { +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/event/LogoutEvent.kt b/app/src/main/java/com/holo/wanandroid/event/LogoutEvent.kt new file mode 100644 index 0000000..79e715c --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/event/LogoutEvent.kt @@ -0,0 +1,12 @@ +package com.holo.wanandroid.event + +import com.holo.architecture.network.State + +/** + * + * + * @Author holo + * @Date 2022/4/17 + */ +class LogoutEvent(val state: State) { +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ext/CustomViewExt.kt b/app/src/main/java/com/holo/wanandroid/ext/CustomViewExt.kt new file mode 100644 index 0000000..6f7d7c5 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ext/CustomViewExt.kt @@ -0,0 +1,195 @@ +package com.holo.wanandroid.ext + +import android.view.View +import android.widget.TextView +import androidx.activity.ComponentActivity +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewbinding.ViewBinding +import androidx.viewpager2.adapter.FragmentStateAdapter +import androidx.viewpager2.widget.ViewPager2 +import com.afollestad.materialdialogs.LayoutMode +import com.afollestad.materialdialogs.MaterialDialog +import com.afollestad.materialdialogs.bottomsheets.BottomSheet +import com.afollestad.materialdialogs.customview.customView +import com.afollestad.materialdialogs.lifecycle.lifecycleOwner +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.google.android.material.button.MaterialButton +import com.google.android.material.textfield.TextInputEditText +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.architecture.ext.* +import com.holo.architecture.network.ListState +import com.holo.architecture.network.State +import com.holo.architecture.utils.flowbus.observeEvent +import com.holo.loadstate.LoadService +import com.holo.wanandroid.R +import com.holo.wanandroid.data.dto.UserInfoBean +import com.holo.wanandroid.event.LoginEvent +import com.holo.wanandroid.ui.discovery.NavDiscoveryFragment +import com.holo.wanandroid.ui.home.NavHomeFragment +import com.holo.wanandroid.ui.login.LoginActivity +import com.holo.wanandroid.ui.mine.NavMineFragment +import com.holo.wanandroid.ui.mine.UserViewModel +import com.holo.wanandroid.ui.projects.NavProjectsFragment +import com.holo.wanandroid.ui.publicnum.NavPublicNumFragment +import com.scwang.smart.refresh.layout.SmartRefreshLayout + +/** + * BRVAH便捷使用ViewBinding + */ +fun BaseViewHolder.getBinding(bind: (View) -> VB): VB = + itemView.getTag(Int.MIN_VALUE) as? VB ?: bind(itemView).also { itemView.setTag(Int.MIN_VALUE, it) } + +/** + * 初始化普通的toolbar 只设置标题 + */ +fun Toolbar.init(titleStr: String = ""): Toolbar { + title = titleStr + findViewById(R.id.tv_toolbar_title)?.let { + title = null + it.text = titleStr.toHtml() + } + return this +} + +/** + * 初始化有返回键的toolbar + */ +fun Toolbar.initClose( + titleStr: String = "", + backImg: Int = R.mipmap.icon_back, + onBack: (toolbar: Toolbar) -> Unit +): Toolbar { + title = titleStr.toHtml() + findViewById(R.id.tv_toolbar_title)?.let { + title = null + it.text = titleStr.toHtml() + } + setNavigationIcon(backImg) + setNavigationOnClickListener { onBack.invoke(this) } + return this +} + +/** + * 初始化首页ViewPager2 + * @receiver ViewPager2 + * @param activity AppCompatActivity + * @return ViewPager2 + */ +fun ViewPager2.initMain(activity: AppCompatActivity): ViewPager2 { + //是否可滑动 + this.isUserInputEnabled = false + this.offscreenPageLimit = 5 + //设置适配器 + adapter = object : FragmentStateAdapter(activity) { + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> NavHomeFragment() + 1 -> NavProjectsFragment() + 2 -> NavDiscoveryFragment() + 3 -> NavPublicNumFragment() + else -> NavMineFragment() + } + } + + override fun getItemCount() = 5 + } + return this +} + +fun ViewPager2.init(activity: FragmentActivity, fragments: List>): ViewPager2 { + this.offscreenPageLimit = fragments.size + //设置适配器 + adapter = object : FragmentStateAdapter(activity) { + override fun getItemCount(): Int = fragments.size + + override fun createFragment(position: Int): Fragment = fragments[position] + } + return this +} + +fun parseListState( + st: ListState, + loadSir: LoadService?, + refresh: SmartRefreshLayout, + adapter: BaseQuickAdapter, + emptyTip: String? = null, + emptyIcon: Int? = null +) { + refresh.finishRefresh() + if (st.isSuccess) { + //成功 + when { + //是第一页 + st.isRefresh -> { + if (st.isEmpty) { + loadSir?.showEmptyCus(emptyTip, emptyIcon) + } else { + adapter.setList(st.listData) + loadSir?.hide() + } + // 如果没有更多数据,直接禁用加载更多 + if (!st.hasMore) { + adapter.loadMoreModule.loadMoreEnd() + } + } + //不是第一页 + else -> { + adapter.addData(st.listData) + if (st.hasMore) { + adapter.loadMoreModule.loadMoreComplete() + } else { + adapter.loadMoreModule.loadMoreEnd() + } + } + } + } else { + //失败 + if (st.isRefresh) { + //如果是第一页,则显示错误界面,并提示错误信息 + loadSir?.showFailedCus(st.err.errorMsg) + } else { + adapter.loadMoreModule.loadMoreFail() + } + } +} + +fun ComponentActivity.showLoginDialog() { + showToast("请先登录") + val dialog = MaterialDialog(this, BottomSheet(LayoutMode.WRAP_CONTENT)).lifecycleOwner(this).show { + title(R.string.str_login_tip) + customView(R.layout.dialog_login, scrollable = true, horizontalPadding = true) + }.show { + val viewModel: UserViewModel by viewModels() + val etAccount = this.findViewById(R.id.et_account) + etAccount.requestFocus() + etAccount.postDelayed({ etAccount.showKeyboard() }, 160) + + val etPsw = this.findViewById(R.id.et_psw) + val btnLogin = this.findViewById(R.id.btn_login) + fun btnEnable() { + btnLogin.isEnabled = etAccount.isNotBlank() && etPsw.isNotBlank() + } + etAccount.afterTextChanged { btnEnable() } + etPsw.afterTextChanged { btnEnable() } + + this.findViewById(R.id.tv_to_register).clickNoRepeat { + dismiss() + startActivity() + } + + btnLogin.clickNoRepeat { + viewModel.doLogin(etAccount.text.toString(), etPsw.text.toString()) +// doLogin.invoke(etAccount.text.toString(), etPsw.text.toString()) + } + } + observeEvent { event -> + if (event.state is State.Success) { + dialog.dismiss() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ext/LoadServiceExt.kt b/app/src/main/java/com/holo/wanandroid/ext/LoadServiceExt.kt new file mode 100644 index 0000000..607d0b1 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ext/LoadServiceExt.kt @@ -0,0 +1,88 @@ +package com.holo.wanandroid.ext + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.annotation.LayoutRes +import androidx.recyclerview.widget.RecyclerView +import com.holo.architecture.initializer.appContext +import com.holo.loadstate.LoadService +import com.holo.loadstate.LoadState +import com.holo.wanandroid.R + +/** + * + * + * @Author holo + * @Date 2022/3/5 + */ + +fun loadServiceInit(view: View, callback: () -> Unit): LoadService { + return loadServiceInit(view, R.layout.layout_loadsir_loading, callback) +} + +fun loadServiceInit(view: View, @LayoutRes loadLayoutId: Int, callback: () -> Unit): LoadService { + val loadSir = LoadState.register(view) { + showLoading() + callback.invoke() + }.config { + loading(loadLayoutId) + failed(R.layout.layout_loadsir_failed, R.id.btn_retry) + empty(R.layout.layout_loadsir_empty) + configColorBuilder { + setBaseColor(appContext.getColor(R.color.bgShimmer)) + setHighlightColor(appContext.getColor(R.color.white)) + } + } + loadSir.showLoading() + return loadSir +} + +fun loadServiceInit(recyclerView: RecyclerView, adapter: RecyclerView.Adapter<*>, callback: () -> Unit): LoadService { + return loadServiceInit(recyclerView, adapter, R.layout.layout_loadsir_loading, callback) +} + +fun loadServiceInit(recyclerView: RecyclerView, adapter: RecyclerView.Adapter<*>, @LayoutRes loadLayoutId: Int, callback: () -> Unit): LoadService { + val loadSir = LoadState.register(recyclerView, adapter) { + showLoading() + callback.invoke() + }.config { + loading(loadLayoutId) + failed(R.layout.layout_loadsir_failed, R.id.btn_retry) + empty(R.layout.layout_loadsir_empty) + configColorBuilder { + setBaseColor(appContext.getColor(R.color.bgShimmer)) + setHighlightColor(appContext.getColor(R.color.white)) + } + } + loadSir.showLoading() + return loadSir +} + +/** + * 设置空布局 + */ +fun LoadService.showEmptyCus(emptyTip: String? = null, @DrawableRes icon: Int? = null) { + this.showEmpty().apply { + emptyTip?.let { + findViewById(R.id.tv_empty).text = it + } + icon?.let { + findViewById(R.id.iv_empty).setImageResource(it) + } + } +} + +/** + * 设置错误布局 + * @param message 错误布局显示的提示内容 + */ +fun LoadService.showFailedCus(message: String? = null) { + this.showFailed().apply { + message?.let { + this.findViewById(R.id.tv_failed).text = message + } + } +} + diff --git a/app/src/main/java/com/holo/wanandroid/ext/NetworkExt.kt b/app/src/main/java/com/holo/wanandroid/ext/NetworkExt.kt new file mode 100644 index 0000000..c6716bf --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ext/NetworkExt.kt @@ -0,0 +1,153 @@ +package com.holo.wanandroid.ext + +import com.holo.architecture.base.repository.IRemoteData +import com.holo.architecture.base.repository.IRepository +import com.holo.architecture.logger.KLog +import com.holo.architecture.network.* +import com.holo.architecture.utils.netstatus.NetUtils +import com.holo.wanandroid.network.PageBean +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject + +/** + * + * + * @Author holo + * @Date 2022/3/23 + */ + +fun IRemoteData.buildRequestBody(vararg params: Pair): RequestBody { + val reqObj = JSONObject() + + params.forEach { (key, value) -> + if (value != null) { + reqObj.put(key, value) + } + } + + return reqObj.toString().toRequestBody("application/json;charset=utf-8".toMediaType()) +} + +/** + * 处理响应结果,转为State包裹对象 + * + * @receiver IRemoteData + * @param block SuspendFunction0> 请求函数 + * @return State + */ +inline fun IRemoteData.processCall(block: () -> BaseResponse): State { + val resp = kotlin.runCatching { + if (!NetUtils.isConnected()) { + throw AppException(HoloError.NO_NETWORK) + } + block() + }.getOrElse { + KLog.e(it) + val appError = ExceptionHandle.handleException(it) + return State.error(appError) + } + return if (resp.isSuccess()) { + State.success(resp.getResponseData()) + } else { + State.error(AppException(resp.getResponseMsg(), resp.getResponseCode())) + } +} + +/** + * 处理响应结果,不关注请求错误 + * + * @receiver IRemoteData + * @param block SuspendFunction0> 请求函数 + * @return State + */ +inline fun IRemoteData.processCallObj(block: () -> BaseResponse): R? { + val resp = kotlin.runCatching { + /*if (!NetUtils.isConnected()) { + throw AppException(HoloError.NO_NETWORK) + }*/ + block() + }.getOrElse { + KLog.e(it) + return null + } + return if (resp.isSuccess()) { + resp.getResponseData() + } else { + null + } +} + +/** + * 处理分页响应结果,转为ListState包裹对象 + * @receiver IRemoteData + * @param page Int 请求页码 + * @param pageSize Int 每页条数 + * @param block Function0>> 请求函数 + * @return ListState + */ +inline fun IRemoteData.processListCall(page: Int, block: () -> BaseResponse>): ListState { + val resp = kotlin.runCatching { + /*if (!NetUtils.isConnected()) { + return ListState(false, page == 0, err = AppException(HoloError.NO_NETWORK)) + }*/ + block() + }.getOrElse { + KLog.e(it) + val appError = ExceptionHandle.handleException(it) + return ListState(false, page == 0, err = appError) + } + + return if (resp.isSuccess()) { + return resp.getResponseData().run { + ListState(true, page == 0, !this.over, datas.isEmpty(), datas, total) + } + } else { + ListState(false, page == 0, err = AppException(resp.getResponseMsg(), resp.getResponseCode())) + } +} + +/** + * 处理分页响应结果,转为ListState包裹对象 + * @receiver IRemoteData + * @param refresh Int 是否刷新 + * @param block Function0>> 请求函数 + * @return ListState + */ +inline fun IRemoteData.processListCall(refresh: Boolean, block: () -> BaseResponse>): ListState { + val resp = kotlin.runCatching { + /*if (!NetUtils.isConnected()) { + return ListState(false, page == 0, err = AppException(HoloError.NO_NETWORK)) + }*/ + block() + }.getOrElse { + KLog.e(it) + val appError = ExceptionHandle.handleException(it) + return ListState(false, refresh, err = appError) + } + + return if (resp.isSuccess()) { + return resp.getResponseData().run { + ListState(true, refresh, !this.over, datas.isEmpty(), datas, total) + } + } else { + ListState(false, refresh, err = AppException(resp.getResponseMsg(), resp.getResponseCode())) + } +} + +/** + * Repository直接请求转Flow + * @receiver IRepository + * @param dispatcher CoroutineDispatcher + * @param action SuspendFunction0> + * @return Flow> + */ +fun IRepository.toFlow(dispatcher: CoroutineDispatcher, action: suspend () -> State): Flow> { + return flow { emit(action()) }.onStart { emit(State.loading()) }.flowOn(dispatcher) +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/initializer/StarInitializer.kt b/app/src/main/java/com/holo/wanandroid/initializer/StarInitializer.kt new file mode 100644 index 0000000..43624d9 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/initializer/StarInitializer.kt @@ -0,0 +1,28 @@ +package com.holo.wanandroid.initializer + +import android.content.Context +import androidx.startup.Initializer +import com.holo.architecture.ext.logD +import com.holo.architecture.logger.KLog +import com.holo.wanandroid.BuildConfig +import com.tencent.mmkv.MMKV + +/** + * Androidx Startup初始化组件 + * APP启动初始化三方依赖库 + * + * @Author holo + * @Date 2022/3/22 + */ +class StarInitializer : Initializer { + + override fun create(context: Context) { + //是否打印日志 + KLog.init(BuildConfig.DEBUG) + //初始化腾讯mmkv + val rootDir = MMKV.initialize(context) + "MMKV root: $rootDir".logD() + } + + override fun dependencies(): MutableList>> = mutableListOf() +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/network/ApiResponse.kt b/app/src/main/java/com/holo/wanandroid/network/ApiResponse.kt new file mode 100644 index 0000000..aa22d71 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/network/ApiResponse.kt @@ -0,0 +1,24 @@ +package com.holo.wanandroid.network + +import com.holo.architecture.network.BaseResponse + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +data class ApiResponse( + val errorCode: Int = 0, + val data: T, + val errorMsg: String? = "" +) : BaseResponse() { + + override fun isSuccess() = errorCode == 0 + + override fun getResponseData() = data + + override fun getResponseCode() = errorCode + + override fun getResponseMsg() = errorMsg ?: "" +} diff --git a/app/src/main/java/com/holo/wanandroid/network/CacheInterceptor.kt b/app/src/main/java/com/holo/wanandroid/network/CacheInterceptor.kt new file mode 100644 index 0000000..cc0ed40 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/network/CacheInterceptor.kt @@ -0,0 +1,22 @@ +package com.holo.wanandroid.network + +import okhttp3.Interceptor +import okhttp3.Response + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +class CacheInterceptor(var day: Int = 7) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val response = chain.proceed(chain.request()) + val maxStale = 60 * 60 * 24 * day + response.newBuilder() + .removeHeader("Pragma") + .header("Cache-Control", "public, only-if-cached, max-stale=$maxStale") + .build() + return response + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/network/PageBean.kt b/app/src/main/java/com/holo/wanandroid/network/PageBean.kt new file mode 100644 index 0000000..c91db67 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/network/PageBean.kt @@ -0,0 +1,17 @@ +package com.holo.wanandroid.network + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +data class PageBean( + val curPage: Int, + val datas: List, + val offset: Int, + val over: Boolean, + val pageCount: Int, + val size: Int, + val total: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/network/WanApi.kt b/app/src/main/java/com/holo/wanandroid/network/WanApi.kt new file mode 100644 index 0000000..eacb0d7 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/network/WanApi.kt @@ -0,0 +1,69 @@ +package com.holo.wanandroid.network + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +object WanApi { + + /** + * 默认请求超时时间 + */ + const val DEFAULT_TIMEOUT: Long = 10L + + const val API_BASE = "https://www.wanandroid.com" + + const val API_LOGIN = "user/login" + + const val API_LOGOUT = "user/logout/json" + + const val API_REGISTER = "user/register" + + const val API_GET_MY_INTEGRAL = "lg/coin/userinfo/json" + + const val API_GET_MY_INTEGRAL_LIST = "lg/coin/list/{page}/json" + + const val API_GET_RANK_LIST = "coin/rank/{page}/json" + + const val API_GET_BANNER = "banner/json" + + const val API_GET_TOP_ARTICLE_LIST = "article/top/json" + + const val API_GET_ARTICLE_LIST = "article/list/{page}/json" + + const val API_GET_PROJECT_CATEGORY = "project/tree/json" + + const val API_GET_PROJECT_LIST = "project/list/{page}/json" + + const val API_GET_LATEST_PROJECT_LIST = "article/listproject/{page}/json" + + const val API_GET_PLAZA_LIST = "user_article/list/{page}/json" + + const val API_GET_QA_LIST = "wenda/list/{page}/json" + + const val API_GET_TREE_LIST = "tree/json" + + const val API_GET_TREE_CHILD_LIST = "article/list/{page}/json" + + const val API_GET_NAVIGATION_LIST = "navi/json" + + const val API_GET_PUBLIC_NUM_LIST = "wxarticle/chapters/json" + + const val API_GET_PUBLIC_NUM_ARTICLE_LIST = "wxarticle/list/{id}/{page}/json" + + const val API_COLLECT = "lg/collect/{id}/json" + + const val API_UN_COLLECT = "lg/uncollect_originId/{id}/json" + + const val API_INFO_UN_COLLECT = "lg/uncollect/{id}/json" + + const val API_COLLECT_URL = "lg/collect/addtool/json" + + const val API_UN_COLLECT_URL = "lg/collect/deletetool/json" + + const val API_GET_COLLECT_LIST = "lg/collect/list/{page}/json" + + const val API_GET_COLLECT_URL_LIST = "lg/collect/usertools/json" +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/network/WanService.kt b/app/src/main/java/com/holo/wanandroid/network/WanService.kt new file mode 100644 index 0000000..c6ed279 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/network/WanService.kt @@ -0,0 +1,232 @@ +package com.holo.wanandroid.network + +import com.holo.wanandroid.data.dto.* +import retrofit2.http.* + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +interface WanService { + + /** + * 用户登录 + * @param username String 用户名 + * @param psw String 密码 + * @return ApiResponse + */ + @FormUrlEncoded + @POST(WanApi.API_LOGIN) + suspend fun login( + @Field("username") username: String, + @Field("password") psw: String + ): ApiResponse + + @GET(WanApi.API_LOGOUT) + suspend fun logout(): ApiResponse + + /** + * 注册 + * @param username String 用户名 + * @param psw String 密码 + * @param rePsw String 重复密码 + * @return ApiResponse + */ + @FormUrlEncoded + @POST(WanApi.API_REGISTER) + suspend fun register( + @Field("username") username: String, + @Field("password") psw: String, + @Field("repassword") rePsw: String + ): ApiResponse + + /** + * 获取我的积分 + * @return ApiResponse + */ + @GET(WanApi.API_GET_MY_INTEGRAL) + suspend fun getMyIntegral(): ApiResponse + + /** + * 获取我的积分获得详情 + * @param page Int 页码,开始位置:1 + * @return ApiResponse> + */ + @GET(WanApi.API_GET_MY_INTEGRAL_LIST) + suspend fun getMyIntegralList(@Path("page") page: Int): ApiResponse> + + /** + * 获取积分排行榜 + * @param page Int 页码,开始位置:1 + * @return ApiResponse> + */ + @GET(WanApi.API_GET_RANK_LIST) + suspend fun getIntegralRankList(@Path("page") page: Int): ApiResponse> + + /** + * 获取banner数据 + * @return ApiResponse> + */ + @GET(WanApi.API_GET_BANNER) + suspend fun getBanner(): ApiResponse> + + /** + * 获取置顶文章集合数据 + * @return ApiResponse> + */ + @GET(WanApi.API_GET_TOP_ARTICLE_LIST) + suspend fun getTopArticleList(): ApiResponse> + + /** + * 获取首页文章数据 + * @param page Int 页码,开始位置:0 + * @return ApiResponse> + */ + @GET(WanApi.API_GET_ARTICLE_LIST) + suspend fun getArticleList(@Path("page") page: Int): ApiResponse> + + + /** + * 项目分类标题 + * @return ApiResponse> + */ + @GET(WanApi.API_GET_PROJECT_CATEGORY) + suspend fun getProjectCategories(): ApiResponse> + + /** + * 根据分类id获取项目数据 + * @param page Int 页码,开始位置:0 + * @param categoryId Int 分类id + * @return ApiResponse> + */ + @GET(WanApi.API_GET_PROJECT_LIST) + suspend fun getProjectList(@Path("page") page: Int, @Query("cid") categoryId: Int): ApiResponse> + + /** + * 获取最新项目数据 + * @param page Int 页码,开始位置:0 + * @return ApiResponse> + */ + @GET(WanApi.API_GET_LATEST_PROJECT_LIST) + suspend fun getLatestProjectList(@Path("page") page: Int): ApiResponse> + + /** + * 广场列表数据 + * @param page Int 页码,开始位置:0 + * @return ApiResponse> + */ + @GET(WanApi.API_GET_PLAZA_LIST) + suspend fun getPlazaList(@Path("page") page: Int): ApiResponse> + + /** + * 问答列表数据 + * @param page Int 页码,开始位置:0 + * @return ApiResponse> + */ + @GET(WanApi.API_GET_QA_LIST) + suspend fun getQAList(@Path("page") page: Int): ApiResponse> + + /** + * 获取体系数据 + * @return ApiResponse> + */ + @GET(WanApi.API_GET_TREE_LIST) + suspend fun getTreeList(): ApiResponse> + + /** + * 知识体系下的文章数据 + * @param page Int 页码,开始位置:0 + * @param cid Int 体系id + * @return ApiResponse> + */ + @GET(WanApi.API_GET_TREE_CHILD_LIST) + suspend fun getTreeChildList(@Path("page") page: Int, @Query("cid") cid: Int): ApiResponse> + + /** + * 获取导航数据 + * @return ApiResponse> + */ + @GET(WanApi.API_GET_NAVIGATION_LIST) + suspend fun getNavigationList(): ApiResponse> + + + /** + * 公众号分类 + * @return ApiResponse> + */ + @GET(WanApi.API_GET_PUBLIC_NUM_LIST) + suspend fun getPublicNumList(): ApiResponse> + + /** + * 获取公众号数据 + * @param page Int 页码,开始位置:0 + * @param id Int 公众号id + * @return ApiResponse> + */ + @GET(WanApi.API_GET_PUBLIC_NUM_ARTICLE_LIST) + suspend fun getPublicNumArticleList(@Path("page") page: Int, @Path("id") id: Int): ApiResponse> + + /** + * 收藏站内文章 + * @param id Int 内容id + * @return ApiResponse + */ + @POST(WanApi.API_COLLECT) + suspend fun collect(@Path("id") id: Int): ApiResponse + + /** + * 取消收藏站内文章 + * @param id Int 收藏id + * @return ApiResponse + */ + @POST(WanApi.API_UN_COLLECT) + suspend fun unCollect(@Path("id") id: Int): ApiResponse + + /** + * 收藏页取消收藏 + * @param id Int 收藏id,拼接在链接上 + * @param originId Int 代表的是你收藏之前的那篇文章本身的id; 但是收藏支持主动添加,这种情况下,没有originId则为-1 + * @return ApiResponse + */ + @FormUrlEncoded + @POST(WanApi.API_INFO_UN_COLLECT) + suspend fun infoUnCollect(@Path("id") id: Int, @Field("originId") originId: Int): ApiResponse + + /** + * 收藏网址 + * @param name String 网址名称 + * @param link String 地址 + * @return ApiResponse + */ + @POST(WanApi.API_COLLECT_URL) + suspend fun collectUrl( + @Query("name") name: String, + @Query("link") link: String + ): ApiResponse + + /** + * 取消收藏网址 + * @param id Int 收藏id + * @return ApiResponse + */ + @POST(WanApi.API_UN_COLLECT_URL) + suspend fun unCollectUrl(@Query("id") id: Int): ApiResponse + + /** + * 获取收藏文章数据 + * @param page Int 页码,开始位置:0 + * @return ApiResponse> + */ + @GET(WanApi.API_GET_COLLECT_LIST) + suspend fun getCollectList(@Path("page") page: Int): ApiResponse> + + /** + * 获取收藏网址数据 + * @return ApiResponse> + */ + @GET(WanApi.API_GET_COLLECT_URL_LIST) + suspend fun getCollectUrlList(): ApiResponse> + +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/network/converter/MyGsonConverterFactory.java b/app/src/main/java/com/holo/wanandroid/network/converter/MyGsonConverterFactory.java new file mode 100644 index 0000000..2a94879 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/network/converter/MyGsonConverterFactory.java @@ -0,0 +1,64 @@ +package com.holo.wanandroid.network.converter; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Converter; +import retrofit2.Retrofit; + +/** + * A {@linkplain Converter.Factory converter} which uses Gson for JSON. + * + *

Because Gson is so flexible in the types it supports, this converter assumes that it can + * handle all types. If you are mixing JSON serialization with something else (such as protocol + * buffers), you must {@linkplain Retrofit.Builder#addConverterFactory(Converter.Factory) add this + * instance} last to allow the other converters a chance to see their types. + */ +public final class MyGsonConverterFactory extends Converter.Factory { + /** + * Create an instance using a default {@link Gson} instance for conversion. Encoding to JSON and + * decoding from JSON (when no charset is specified by a header) will use UTF-8. + */ + public static MyGsonConverterFactory create() { + return create(new Gson()); + } + + /** + * Create an instance using {@code gson} for conversion. Encoding to JSON and decoding from JSON + * (when no charset is specified by a header) will use UTF-8. + */ + @SuppressWarnings("ConstantConditions") // Guarding public API nullability. + public static MyGsonConverterFactory create(Gson gson) { + if (gson == null) throw new NullPointerException("gson == null"); + return new MyGsonConverterFactory(gson); + } + + private final Gson gson; + + private MyGsonConverterFactory(Gson gson) { + this.gson = gson; + } + + @Override + public Converter responseBodyConverter( + Type type, Annotation[] annotations, Retrofit retrofit) { + TypeAdapter adapter = gson.getAdapter(TypeToken.get(type)); + return new ResponseBodyConverter<>(gson, adapter); + } + + @Override + public Converter requestBodyConverter( + Type type, + Annotation[] parameterAnnotations, + Annotation[] methodAnnotations, + Retrofit retrofit) { + TypeAdapter adapter = gson.getAdapter(TypeToken.get(type)); + return new RequestBodyConverter<>(gson, adapter); + } +} diff --git a/app/src/main/java/com/holo/wanandroid/network/converter/RequestBodyConverter.java b/app/src/main/java/com/holo/wanandroid/network/converter/RequestBodyConverter.java new file mode 100644 index 0000000..7c31b49 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/network/converter/RequestBodyConverter.java @@ -0,0 +1,41 @@ +package com.holo.wanandroid.network.converter; + +import androidx.annotation.NonNull; + +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.Buffer; +import retrofit2.Converter; + +final class RequestBodyConverter implements Converter { + private static final MediaType MEDIA_TYPE = MediaType.get("application/json; charset=UTF-8"); + private static final Charset UTF_8 = StandardCharsets.UTF_8; + + private final Gson gson; + private final TypeAdapter adapter; + + RequestBodyConverter(Gson gson, TypeAdapter adapter) { + this.gson = gson; + this.adapter = adapter; + } + + @Override + public RequestBody convert(@NonNull T value) throws IOException { + Buffer buffer = new Buffer(); + Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8); + JsonWriter jsonWriter = gson.newJsonWriter(writer); + adapter.write(jsonWriter, value); + jsonWriter.close(); + return RequestBody.create(MEDIA_TYPE, buffer.readByteString()); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/network/converter/ResponseBodyConverter.java b/app/src/main/java/com/holo/wanandroid/network/converter/ResponseBodyConverter.java new file mode 100644 index 0000000..f595c86 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/network/converter/ResponseBodyConverter.java @@ -0,0 +1,71 @@ +package com.holo.wanandroid.network.converter; + + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.gson.Gson; +import com.google.gson.JsonIOException; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.holo.architecture.network.AppException; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; + +import okhttp3.MediaType; +import okhttp3.ResponseBody; +import retrofit2.Converter; + +/** + * @Author holo + * @Date 2022/1/27 + */ +public class ResponseBodyConverter implements Converter { + private final Gson gson; + private final TypeAdapter adapter; + + ResponseBodyConverter(Gson gson, TypeAdapter adapter) { + this.gson = gson; + this.adapter = adapter; + } + + @Override + public T convert(ResponseBody value) throws IOException { + String response = value.string(); + + JsonReader jsonReader = null; + MediaType mediaType = value.contentType(); + Charset charset = mediaType != null ? mediaType.charset(UTF_8) : UTF_8; + InputStream inputStream = new ByteArrayInputStream(response.getBytes()); + jsonReader = gson.newJsonReader(new InputStreamReader(inputStream, charset)); + + try { + T result = adapter.read(jsonReader); + if (jsonReader.peek() != JsonToken.END_DOCUMENT) { + throw new JsonIOException("JSON document was not fully consumed."); + } + return result; + } catch (Exception ex) { + JSONObject jsonObject = null; + try { + jsonObject = new JSONObject(response); + } catch (JSONException e) { + e.printStackTrace(); + } + int code = jsonObject != null ? jsonObject.optInt("errorCode") : 0; + String message = jsonObject != null ? jsonObject.optString("errorMsg") : ""; + + throw new AppException(message, code); + } finally { + value.close(); + } + } +} + diff --git a/app/src/main/java/com/holo/wanandroid/repository/UserRepository.kt b/app/src/main/java/com/holo/wanandroid/repository/UserRepository.kt new file mode 100644 index 0000000..926028e --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/repository/UserRepository.kt @@ -0,0 +1,33 @@ +package com.holo.wanandroid.repository + +import com.holo.architecture.base.repository.IRepository +import com.holo.wanandroid.data.remote.UserRemoteData +import com.holo.wanandroid.ext.toFlow +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import javax.inject.Inject + +/** + * + * + * @Author holo + * @Date 2022/4/17 + */ +class UserRepository @Inject constructor( + private val remote: UserRemoteData, + private val ioDispatcher: CoroutineDispatcher +) : IRepository { + + suspend fun doLogin(account: String, psw: String) = toFlow(ioDispatcher) { remote.doLogin(account, psw) } + + suspend fun doLogout() = toFlow(ioDispatcher) { remote.doLogout() } + + suspend fun doRegister(account: String, psw: String, rePsw: String) = toFlow(ioDispatcher) { remote.doRegister(account, psw, rePsw) } + + suspend fun getMyIntegral() = flow { emit(remote.getMyIntegral()) }.flowOn(ioDispatcher) + + suspend fun getMyIntegralList(page: Int, refresh: Boolean) = flow { emit(remote.getMyIntegralList(page, refresh)) }.flowOn(ioDispatcher) + + suspend fun getIntegralRankList(page: Int, refresh: Boolean) = flow { emit(remote.getIntegralRankList(page, refresh)) }.flowOn(ioDispatcher) +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/repository/WanRepository.kt b/app/src/main/java/com/holo/wanandroid/repository/WanRepository.kt new file mode 100644 index 0000000..63f7d0b --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/repository/WanRepository.kt @@ -0,0 +1,61 @@ +package com.holo.wanandroid.repository + +import com.holo.architecture.base.repository.IRepository +import com.holo.wanandroid.data.remote.WanRemoteData +import com.holo.wanandroid.ext.toFlow +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import javax.inject.Inject + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +class WanRepository @Inject constructor( + private val remote: WanRemoteData, + private val ioDispatcher: CoroutineDispatcher +) : IRepository { + + fun getBannerList() = toFlow(ioDispatcher) { remote.getBannerList() } + + fun getTopArticleList() = flow { emit(remote.getTopArticleList()) }.flowOn(ioDispatcher) + + fun getArticleList(page: Int) = flow { emit(remote.getArticleList(page)) }.flowOn(ioDispatcher) + + fun getProjectCategories() = flow { emit(remote.getProjectCategories()) }.flowOn(ioDispatcher) + + fun getProjectList(page: Int, categoryId: Int) = flow { emit(remote.getProjectList(page, categoryId)) }.flowOn(ioDispatcher) + + fun getLatestProjectList(page: Int) = flow { emit(remote.getLatestProjectList(page)) }.flowOn(ioDispatcher) + + suspend fun getPlazaList(page: Int) = flow { emit(remote.getPlazaList(page)) }.flowOn(ioDispatcher) + + suspend fun getQAList(page: Int) = flow { emit(remote.getQAList(page)) }.flowOn(ioDispatcher) + + suspend fun getTreeList() = flow { emit(remote.getTreeList()) }.flowOn(ioDispatcher) + + suspend fun getTreeChildList(page: Int, treeId: Int) = flow { emit(remote.getTreeChildList(page, treeId)) }.flowOn(ioDispatcher) + + suspend fun getNavigationList() = flow { emit(remote.getNavigationList()) }.flowOn(ioDispatcher) + + suspend fun getPublicNumList() = flow { emit(remote.getPublicNumList()) }.flowOn(ioDispatcher) + + suspend fun getPublicArticleList(page: Int, pnId: Int) = flow { emit(remote.getPublicArticleList(page, pnId)) }.flowOn(ioDispatcher) + + suspend fun collect(id: Int) = toFlow(ioDispatcher) { remote.collect(id) } + + suspend fun unCollect(id: Int) = toFlow(ioDispatcher) { remote.unCollect(id) } + + suspend fun infoUnCollect(id: Int, originId: Int) = toFlow(ioDispatcher) { remote.infoUnCollect(id, originId) } + + suspend fun collectUrl(name: String, link: String) = toFlow(ioDispatcher) { remote.collectUrl(name, link) } + + suspend fun unCollectUrl(id: Int) = toFlow(ioDispatcher) { remote.unCollectUrl(id) } + + suspend fun getCollectList(page: Int) = flow { emit(remote.getCollectList(page)) }.flowOn(ioDispatcher) + + suspend fun getCollectUrlList(page: Int) = flow { emit(remote.getCollectUrlList()) }.flowOn(ioDispatcher) +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/MainActivity.kt b/app/src/main/java/com/holo/wanandroid/ui/MainActivity.kt new file mode 100644 index 0000000..ab22b3e --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/MainActivity.kt @@ -0,0 +1,99 @@ +package com.holo.wanandroid.ui + +import android.os.Bundle +import android.view.KeyEvent +import android.view.View +import com.holo.architecture.base.activity.BaseActivity +import com.holo.architecture.ext.lifecycle.ActivityStack +import com.holo.architecture.ext.showErrToast +import com.holo.architecture.ext.showToast +import com.holo.architecture.logger.KLog +import com.holo.architecture.network.State +import com.holo.architecture.utils.flowbus.observeEvent +import com.holo.architecture.utils.netstatus.NetType +import com.holo.architecture.utils.netstatus.NetUtils +import com.holo.wanandroid.R +import com.holo.wanandroid.databinding.ActivityMainBinding +import com.holo.wanandroid.event.LoginEvent +import com.holo.wanandroid.event.LogoutEvent +import com.holo.wanandroid.ext.initMain +import dagger.hilt.android.AndroidEntryPoint +import kotlin.system.exitProcess + +@AndroidEntryPoint +class MainActivity : BaseActivity() { + + override fun bottomPaddingView(): View = binding.navMenu + + override fun initView(savedInstanceState: Bundle?) { + binding.vpMain.initMain(this) + binding.navMenu.setOnCheckedChangeListener { _, checkedId -> + when (checkedId) { + R.id.nav_home -> binding.vpMain.setCurrentItem(0, false) + R.id.nav_project -> binding.vpMain.setCurrentItem(1, false) + R.id.nav_discovery -> binding.vpMain.setCurrentItem(2, false) + R.id.nav_public -> binding.vpMain.setCurrentItem(3, false) + R.id.nav_mine -> binding.vpMain.setCurrentItem(4, false) + } + } + } + + override fun observeViewModel() { + observeEvent { event -> + when (event.state) { + is State.Loading -> showLoading() + is State.Success -> { + hideLoading() + showToast("登录成功") + } + is State.Error -> { + hideLoading() + showErrToast(event.state.err) + } + } + } + observeEvent { event -> + when (event.state) { + is State.Loading -> showLoading() + is State.Success -> { + hideLoading() + showToast("退出登录") + } + is State.Error -> { + hideLoading() + showErrToast(event.state.err) + } + } + } + } + + override fun onNetworkStateChanged(netType: String) { + // 网络状态改变 + KLog.e("测试", "Main网络状态改变:${netType}") + //binding.netType = netType + if (netType == NetType.WIFI) { + if (NetUtils.is5GWifiConnected(this)) { + KLog.e("测试", "这是5G WI-FI") + } else { + KLog.e("测试", "这是2.4G WI-FI") + } + KLog.e("测试", "WI-FI名:${NetUtils.getConnectedWifiSSID(this)}") + } + } + + private var exitTime = 0L + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if ((System.currentTimeMillis() - exitTime) > 2000) { + showToast("再次点击退出" + getString(R.string.app_show_name)) + exitTime = System.currentTimeMillis() + } else { + ActivityStack.finishAllActivity() + exitProcess(0) + } + return true + } + return super.onKeyDown(keyCode, event) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/WebActivity.kt b/app/src/main/java/com/holo/wanandroid/ui/WebActivity.kt new file mode 100644 index 0000000..f1eed14 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/WebActivity.kt @@ -0,0 +1,88 @@ +package com.holo.wanandroid.ui + +import android.os.Bundle +import android.webkit.ConsoleMessage +import android.webkit.WebView +import android.widget.LinearLayout +import com.holo.architecture.base.activity.BaseActivity +import com.holo.architecture.ext.toGone +import com.holo.architecture.ext.toVisible +import com.holo.architecture.logger.KLog +import com.holo.loadstate.LoadService +import com.holo.wanandroid.Constant +import com.holo.wanandroid.R +import com.holo.wanandroid.databinding.ActivityWebBinding +import com.holo.wanandroid.ext.loadServiceInit +import com.just.agentweb.AgentWeb +import com.just.agentweb.DefaultWebClient +import com.just.agentweb.WebChromeClient +import com.just.agentweb.WebViewClient + +class WebActivity : BaseActivity() { + + private lateinit var agentWeb: AgentWeb + private lateinit var preAgentWeb: AgentWeb.PreAgentWeb + + private var webTitle: String? = null + + private lateinit var loads: LoadService + + override fun initView(savedInstanceState: Bundle?) { + val webUrl = intent.getStringExtra(Constant.KEY_WEB_URL) + webTitle = intent.getStringExtra(Constant.KEY_WEB_TITLE) + initWeb(webUrl ?: "") + loads = loadServiceInit(binding.loadView, R.layout.layout_loadsir_loading) { + agentWeb = preAgentWeb.go(webUrl) + } + } + + private fun initWeb(url: String) { + KLog.d("Holo", "WebActivity go url:${url}") + preAgentWeb = AgentWeb.with(this) + .setAgentWebParent(binding.container, LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)) + .useDefaultIndicator(resources.getColor(R.color.colorPrimary)) + .setWebChromeClient(webChromeClient) + .setWebViewClient(webClient) + .setMainFrameErrorView(com.just.agentweb.R.layout.agentweb_error_page, -1) + .setOpenOtherPageWays(DefaultWebClient.OpenOtherPageWays.ASK) //打开其他应用时,弹窗咨询用户是否前往其他应用 + .createAgentWeb() + .ready() + agentWeb = preAgentWeb.go(url) + } + + private val webClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + loads.hide() + binding.loadView.toGone() + binding.container.toVisible() + super.onPageFinished(view, url) + } + + } + + private val webChromeClient = object : WebChromeClient() { + override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean { + return super.onConsoleMessage(consoleMessage) + } + } + + override fun observeViewModel() { + + } + + override fun onPause() { + agentWeb.webLifeCycle?.onPause() + super.onPause() + } + + override fun onResume() { + agentWeb.webLifeCycle?.onResume() + super.onResume() + } + + override fun onDestroy() { + agentWeb.webLifeCycle?.onDestroy() + super.onDestroy() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/discovery/DiscoveryViewModel.kt b/app/src/main/java/com/holo/wanandroid/ui/discovery/DiscoveryViewModel.kt new file mode 100644 index 0000000..0903b82 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/discovery/DiscoveryViewModel.kt @@ -0,0 +1,94 @@ +package com.holo.wanandroid.ui.discovery + +import androidx.lifecycle.viewModelScope +import com.holo.architecture.base.viewmodel.BaseViewModel +import com.holo.architecture.network.ListState +import com.holo.wanandroid.data.dto.ArticleBean +import com.holo.wanandroid.data.dto.CategoryBean +import com.holo.wanandroid.data.dto.NavigationBean +import com.holo.wanandroid.data.dto.TreeBean +import com.holo.wanandroid.repository.WanRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * + * + * @Author holo + * @Date 2022/4/16 + */ +@HiltViewModel +class DiscoveryViewModel @Inject constructor( + private val repo: WanRepository +) : BaseViewModel() { + + private val _plazaListResp = MutableSharedFlow>() + val plazaListResp = _plazaListResp.asSharedFlow() + + fun getPlazaList(refresh: Boolean = true) { + if (refresh) pageNo = 0 else pageNo++ + viewModelScope.launch { + repo.getPlazaList(pageNo).collect { _plazaListResp.emit(it) } + } + } + + private val _qaListResp = MutableSharedFlow>() + val qaListResp = _qaListResp.asSharedFlow() + + fun getQAList(refresh: Boolean = true) { + if (refresh) pageNo = 0 else pageNo++ + viewModelScope.launch { + repo.getQAList(pageNo).collect { _qaListResp.emit(it) } + } + } + + private val _treeListResp = MutableSharedFlow>() + val treeListResp = _treeListResp.asSharedFlow() + + fun getTreeList() { + viewModelScope.launch { + repo.getTreeList().collect { _treeListResp.emit(it ?: emptyList()) } + } + } + + private val _navigationListResp = MutableSharedFlow>() + val navigationListResp = _navigationListResp.asSharedFlow() + + fun getNavigationList() { + viewModelScope.launch { + repo.getNavigationList().collect { _navigationListResp.emit(it ?: emptyList()) } + } + } + + private val _publicNumListResp = MutableSharedFlow>() + val publicNumListResp = _publicNumListResp.asSharedFlow() + + fun getPublicNumList() { + viewModelScope.launch { + repo.getPublicNumList().collect { _publicNumListResp.emit(it ?: emptyList()) } + } + } + + private val _publicArticleListResp = MutableSharedFlow>() + val publicArticleListResp = _publicArticleListResp.asSharedFlow() + + fun getPublicArticleList(pnId: Int, refresh: Boolean = true) { + if (refresh) pageNo = 0 else pageNo++ + viewModelScope.launch { + repo.getPublicArticleList(pageNo, pnId).collect { _publicArticleListResp.emit(it) } + } + } + + private val _treeChildListResp = MutableSharedFlow>() + val treeChildListResp = _treeChildListResp.asSharedFlow() + + fun getTreeChildList(treeId: Int, refresh: Boolean = true) { + if (refresh) pageNo = 0 else pageNo++ + viewModelScope.launch { + repo.getTreeChildList(pageNo, treeId).collect { _treeChildListResp.emit(it) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/discovery/NavDiscoveryFragment.kt b/app/src/main/java/com/holo/wanandroid/ui/discovery/NavDiscoveryFragment.kt new file mode 100644 index 0000000..596b2b3 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/discovery/NavDiscoveryFragment.kt @@ -0,0 +1,53 @@ +package com.holo.wanandroid.ui.discovery + +import android.os.Bundle +import android.widget.LinearLayout +import com.google.android.material.tabs.TabLayoutMediator +import com.gyf.immersionbar.ktx.statusBarHeight +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.wanandroid.databinding.FragmentNavDiscoveryBinding +import com.holo.wanandroid.ext.init +import dagger.hilt.android.AndroidEntryPoint + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +@AndroidEntryPoint +class NavDiscoveryFragment : BaseFragment() { + + private val fragmentList: MutableList> = mutableListOf() + private val titleList: MutableList = mutableListOf() + + override fun initView(savedInstanceState: Bundle?) { + val params = binding.titleBar.layoutParams as LinearLayout.LayoutParams + params.height = statusBarHeight + binding.titleBar.layoutParams = params + + initTab() + } + + private fun initTab() { + activity?.run { + fragmentList.add(PlazaFragment()) + titleList.add("广场") + fragmentList.add(QAFragment()) + titleList.add("问答") + fragmentList.add(TreeFragment()) + titleList.add("体系") + fragmentList.add(NavigationFragment()) + titleList.add("导航") + + binding.viewPager.init(this, fragmentList) + TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position -> + tab.text = titleList[position] + }.attach() + } + } + + override fun observeViewModel() { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/discovery/NavigationAdapter.kt b/app/src/main/java/com/holo/wanandroid/ui/discovery/NavigationAdapter.kt new file mode 100644 index 0000000..d932b41 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/discovery/NavigationAdapter.kt @@ -0,0 +1,48 @@ +package com.holo.wanandroid.ui.discovery + +import android.annotation.SuppressLint +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.holo.architecture.ext.toHtml +import com.holo.wanandroid.R +import com.holo.wanandroid.data.dto.ArticleBean +import com.holo.wanandroid.data.dto.NavigationBean +import com.holo.wanandroid.databinding.ItemTreeBinding +import com.holo.wanandroid.databinding.ItemTreeInBinding +import com.holo.wanandroid.ext.getBinding + +/** + * + * + * @Author holo + * @Date 2022/4/16 + */ +class NavigationAdapter(private val childClick: (childItem: ArticleBean) -> Unit) : + BaseQuickAdapter(R.layout.item_tree, mutableListOf()) { + + private lateinit var adapter: NavigationInAdapter + + @SuppressLint("ClickableViewAccessibility") + override fun convert(holder: BaseViewHolder, item: NavigationBean) { + val binding = holder.getBinding(ItemTreeBinding::bind) + binding.tvName.text = item.name + + adapter = NavigationInAdapter(item.articles.toMutableList()) + binding.recyclerView.adapter = adapter + adapter.setOnItemClickListener { _, view, position -> + childClick.invoke(item.articles[position]) + } + binding.recyclerView.setOnTouchListener { v, event -> + binding.itemRoot.onTouchEvent(event) + false + } + } +} + +class NavigationInAdapter(list: MutableList) : BaseQuickAdapter(R.layout.item_tree_in, list) { + override fun convert(holder: BaseViewHolder, item: ArticleBean) { + val binding = holder.getBinding(ItemTreeInBinding::bind) + + binding.tvName.text = item.title.toHtml() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/discovery/NavigationFragment.kt b/app/src/main/java/com/holo/wanandroid/ui/discovery/NavigationFragment.kt new file mode 100644 index 0000000..61d32e7 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/discovery/NavigationFragment.kt @@ -0,0 +1,60 @@ +package com.holo.wanandroid.ui.discovery + +import android.os.Bundle +import androidx.fragment.app.viewModels +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.architecture.ext.startActivity +import com.holo.loadstate.LoadService +import com.holo.wanandroid.Constant +import com.holo.wanandroid.R +import com.holo.wanandroid.databinding.FragmentNavigationBinding +import com.holo.wanandroid.ext.loadServiceInit +import com.holo.wanandroid.ui.WebActivity +import dagger.hilt.android.AndroidEntryPoint + +/** + * + * + * @Author holo + * @Date 2022/4/16 + */ +@AndroidEntryPoint +class NavigationFragment : BaseFragment() { + + private val viewModel: DiscoveryViewModel by viewModels() + + private lateinit var loads: LoadService + + private lateinit var adapter: NavigationAdapter + + override fun initView(savedInstanceState: Bundle?) { + adapter = NavigationAdapter { childItem -> + startActivity( + Constant.KEY_WEB_TITLE to childItem.title, + Constant.KEY_WEB_URL to childItem.link + ) + } + binding.container.recyclerView.adapter = adapter + + binding.container.refreshLayout.setOnRefreshListener { viewModel.getNavigationList() } + loads = loadServiceInit(binding.container.recyclerView, adapter, R.layout.item_tree_shimmer) { + viewModel.getNavigationList() + } + } + + override fun lazyLoadData() { + viewModel.getNavigationList() + } + + override fun observeViewModel() { + observerObj(viewModel.navigationListResp) { list -> + binding.container.refreshLayout.finishRefresh() + if (list.isNullOrEmpty()) { + loads.showEmpty() + } else { + loads.hide() + adapter.setList(list) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/discovery/PlazaFragment.kt b/app/src/main/java/com/holo/wanandroid/ui/discovery/PlazaFragment.kt new file mode 100644 index 0000000..3a022a0 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/discovery/PlazaFragment.kt @@ -0,0 +1,90 @@ +package com.holo.wanandroid.ui.discovery + +import android.os.Bundle +import androidx.fragment.app.viewModels +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.architecture.ext.startActivity +import com.holo.architecture.ext.toHtml +import com.holo.loadstate.LoadService +import com.holo.wanandroid.Constant +import com.holo.wanandroid.R +import com.holo.wanandroid.databinding.FragmentPlazaBinding +import com.holo.wanandroid.ext.loadServiceInit +import com.holo.wanandroid.ext.parseListState +import com.holo.wanandroid.ext.showLoginDialog +import com.holo.wanandroid.ui.WebActivity +import com.holo.wanandroid.ui.home.ArticleAdapter +import com.holo.wanandroid.ui.mine.CollectViewModel +import com.holo.wanandroid.util.CacheUtil +import dagger.hilt.android.AndroidEntryPoint + +/** + * + * + * @Author holo + * @Date 2022/4/16 + */ +@AndroidEntryPoint +class PlazaFragment : BaseFragment() { + + private val viewModel: DiscoveryViewModel by viewModels() + private val collectViewModel: CollectViewModel by viewModels() + + private lateinit var loads: LoadService + + private lateinit var adapter: ArticleAdapter + + override fun initView(savedInstanceState: Bundle?) { + adapter = ArticleAdapter() + binding.container.recyclerView.adapter = adapter + adapter.setOnItemChildClickListener { _, view, position -> + val item = adapter.getItem(position) + when (view.id) { + R.id.tv_author -> { + } + R.id.like_view -> { + if (CacheUtil.isLogin()) { + if (item.collect) { + collectViewModel.unCollect(item.id, position) + } else { + collectViewModel.collect(item.id, position) + } + } else { + activity?.showLoginDialog() + } + } + } + } + adapter.setOnItemClickListener { _, view, position -> + val item = adapter.getItem(position) + startActivity( + Constant.KEY_WEB_TITLE to item.title.toHtml(), + Constant.KEY_WEB_URL to item.link + ) + } + + adapter.loadMoreModule.setOnLoadMoreListener { viewModel.getPlazaList(false) } + binding.container.refreshLayout.setOnRefreshListener { viewModel.getPlazaList() } + loads = loadServiceInit(binding.container.recyclerView, adapter, R.layout.item_article_shimmer) { + viewModel.getPlazaList() + } + } + + override fun lazyLoadData() { + viewModel.getPlazaList() + } + + override fun observeViewModel() { + observerList(viewModel.plazaListResp) { state -> + parseListState(state, loads, binding.container.refreshLayout, adapter) + } + observer(collectViewModel.collectResp, success = { + adapter.getItem(it).collect = true + adapter.notifyItemChanged(it) + }) + observer(collectViewModel.unCollectResp, success = { + adapter.getItem(it).collect = false + adapter.notifyItemChanged(it) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/discovery/QAFragment.kt b/app/src/main/java/com/holo/wanandroid/ui/discovery/QAFragment.kt new file mode 100644 index 0000000..b176f6d --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/discovery/QAFragment.kt @@ -0,0 +1,92 @@ +package com.holo.wanandroid.ui.discovery + +import android.os.Bundle +import androidx.fragment.app.viewModels +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.architecture.ext.showToast +import com.holo.architecture.ext.startActivity +import com.holo.architecture.ext.toHtml +import com.holo.loadstate.LoadService +import com.holo.wanandroid.Constant +import com.holo.wanandroid.R +import com.holo.wanandroid.databinding.FragmentQaBinding +import com.holo.wanandroid.ext.loadServiceInit +import com.holo.wanandroid.ext.parseListState +import com.holo.wanandroid.ext.showLoginDialog +import com.holo.wanandroid.ui.WebActivity +import com.holo.wanandroid.ui.home.ArticleAdapter +import com.holo.wanandroid.ui.mine.CollectViewModel +import com.holo.wanandroid.util.CacheUtil +import com.jaren.lib.view.LikeView +import dagger.hilt.android.AndroidEntryPoint + +/** + * + * + * @Author holo + * @Date 2022/4/16 + */ +@AndroidEntryPoint +class QAFragment : BaseFragment() { + + private val viewModel: DiscoveryViewModel by viewModels() + private val collectViewModel: CollectViewModel by viewModels() + + private lateinit var loads: LoadService + + private lateinit var adapter: ArticleAdapter + + override fun initView(savedInstanceState: Bundle?) { + adapter = ArticleAdapter() + binding.container.recyclerView.adapter = adapter + adapter.setOnItemChildClickListener { _, view, position -> + val item = adapter.getItem(position) + when (view.id) { + R.id.tv_author -> { + } + R.id.like_view -> { + if (CacheUtil.isLogin()) { + if (item.collect) { + collectViewModel.unCollect(item.id, position) + } else { + collectViewModel.collect(item.id, position) + } + } else { + activity?.showLoginDialog() + } + } + } + } + adapter.setOnItemClickListener { _, view, position -> + val item = adapter.getItem(position) + startActivity( + Constant.KEY_WEB_TITLE to item.title.toHtml(), + Constant.KEY_WEB_URL to item.link + ) + } + + adapter.loadMoreModule.setOnLoadMoreListener { viewModel.getQAList(false) } + binding.container.refreshLayout.setOnRefreshListener { viewModel.getQAList() } + loads = loadServiceInit(binding.container.recyclerView, adapter, R.layout.item_article_shimmer) { + viewModel.getQAList() + } + } + + override fun lazyLoadData() { + viewModel.getQAList() + } + + override fun observeViewModel() { + observerList(viewModel.qaListResp) { state -> + parseListState(state, loads, binding.container.refreshLayout, adapter) + } + observer(collectViewModel.collectResp, success = { + adapter.getItem(it).collect = true + adapter.notifyItemChanged(it) + }) + observer(collectViewModel.unCollectResp, success = { + adapter.getItem(it).collect = false + adapter.notifyItemChanged(it) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/discovery/TreeAdapter.kt b/app/src/main/java/com/holo/wanandroid/ui/discovery/TreeAdapter.kt new file mode 100644 index 0000000..a8cb5ca --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/discovery/TreeAdapter.kt @@ -0,0 +1,47 @@ +package com.holo.wanandroid.ui.discovery + +import android.annotation.SuppressLint +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.holo.wanandroid.R +import com.holo.wanandroid.data.dto.CategoryBean +import com.holo.wanandroid.data.dto.TreeBean +import com.holo.wanandroid.databinding.ItemTreeBinding +import com.holo.wanandroid.databinding.ItemTreeInBinding +import com.holo.wanandroid.ext.getBinding + +/** + * + * + * @Author holo + * @Date 2022/4/16 + */ +class TreeAdapter(private val childClick: (treeBean: TreeBean, category: CategoryBean) -> Unit) : + BaseQuickAdapter(R.layout.item_tree, mutableListOf()) { + + private lateinit var adapter: TreeInAdapter + + @SuppressLint("ClickableViewAccessibility") + override fun convert(holder: BaseViewHolder, item: TreeBean) { + val binding = holder.getBinding(ItemTreeBinding::bind) + binding.tvName.text = item.name + + adapter = TreeInAdapter(item.children.toMutableList()) + binding.recyclerView.adapter = adapter + adapter.setOnItemClickListener { _, view, position -> + childClick.invoke(item, item.children[position]) + } + binding.recyclerView.setOnTouchListener { v, event -> + binding.itemRoot.onTouchEvent(event) + false + } + } +} + +class TreeInAdapter(list: MutableList) : BaseQuickAdapter(R.layout.item_tree_in, list) { + override fun convert(holder: BaseViewHolder, item: CategoryBean) { + val binding = holder.getBinding(ItemTreeInBinding::bind) + + binding.tvName.text = item.name + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/discovery/TreeFragment.kt b/app/src/main/java/com/holo/wanandroid/ui/discovery/TreeFragment.kt new file mode 100644 index 0000000..e2eb7bb --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/discovery/TreeFragment.kt @@ -0,0 +1,64 @@ +package com.holo.wanandroid.ui.discovery + +import android.os.Bundle +import androidx.fragment.app.viewModels +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.architecture.ext.startActivity +import com.holo.loadstate.LoadService +import com.holo.wanandroid.Constant +import com.holo.wanandroid.R +import com.holo.wanandroid.databinding.FragmentTreeBinding +import com.holo.wanandroid.ext.loadServiceInit +import com.holo.wanandroid.ui.discovery.tree.TreeInfoActivity +import dagger.hilt.android.AndroidEntryPoint + +/** + * + * + * @Author holo + * @Date 2022/4/16 + */ +@AndroidEntryPoint +class TreeFragment : BaseFragment() { + + private val viewModel: DiscoveryViewModel by viewModels() + + private lateinit var loads: LoadService + + private lateinit var adapter: TreeAdapter + + override fun initView(savedInstanceState: Bundle?) { + adapter = TreeAdapter { tree, catetory -> + startActivity( + Constant.KEY_TREE_BEAN to tree, + Constant.KEY_ID to catetory.id + ) + } + binding.container.recyclerView.adapter = adapter + + adapter.setOnItemClickListener { _, view, position -> + startActivity(Constant.KEY_TREE_BEAN to adapter.getItem(position)) + } + + binding.container.refreshLayout.setOnRefreshListener { viewModel.getTreeList() } + loads = loadServiceInit(binding.container.recyclerView, adapter, R.layout.item_tree_shimmer) { + viewModel.getTreeList() + } + } + + override fun lazyLoadData() { + viewModel.getTreeList() + } + + override fun observeViewModel() { + observerObj(viewModel.treeListResp) { list -> + binding.container.refreshLayout.finishRefresh() + if (list.isNullOrEmpty()) { + loads.showEmpty() + } else { + loads.hide() + adapter.setList(list) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/discovery/tree/TreeInfoActivity.kt b/app/src/main/java/com/holo/wanandroid/ui/discovery/tree/TreeInfoActivity.kt new file mode 100644 index 0000000..b267223 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/discovery/tree/TreeInfoActivity.kt @@ -0,0 +1,52 @@ +package com.holo.wanandroid.ui.discovery.tree + +import android.os.Bundle +import com.google.android.material.tabs.TabLayoutMediator +import com.holo.architecture.base.activity.BaseActivity +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.architecture.ext.clickNoRepeat +import com.holo.wanandroid.Constant +import com.holo.wanandroid.data.dto.CategoryBean +import com.holo.wanandroid.data.dto.TreeBean +import com.holo.wanandroid.databinding.ActivityTreeInfoBinding +import com.holo.wanandroid.ext.init +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class TreeInfoActivity : BaseActivity() { + + override fun initView(savedInstanceState: Bundle?) { + val bean = intent.getParcelableExtra(Constant.KEY_TREE_BEAN) + val selectedId = intent.getIntExtra(Constant.KEY_ID, 0) + if (bean == null) { + finish() + return + } + binding.toolbar.tvToolbarTitle.text = bean.name + binding.toolbar.ivBack.clickNoRepeat { finish() } + initTab(bean.children, selectedId) + } + + private fun initTab(categoryList: List, selectedId: Int) { + val titleList = mutableListOf() + val fragments = mutableListOf>() + var selectedPosition = 0 + categoryList.forEachIndexed { index, bean -> + if (bean.id == selectedId) { + selectedPosition = index + } + titleList.add(bean.name) + fragments.add(TreeInfoFragment.newInstance(bean.id)) + } + + binding.viewPager.init(this, fragments) + binding.viewPager.setCurrentItem(selectedPosition, false) + TabLayoutMediator(binding.tabNews, binding.viewPager) { tab, position -> + tab.text = titleList[position] + }.attach() + } + + override fun observeViewModel() { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/discovery/tree/TreeInfoFragment.kt b/app/src/main/java/com/holo/wanandroid/ui/discovery/tree/TreeInfoFragment.kt new file mode 100644 index 0000000..21cb3c7 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/discovery/tree/TreeInfoFragment.kt @@ -0,0 +1,86 @@ +package com.holo.wanandroid.ui.discovery.tree + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.architecture.ext.startActivity +import com.holo.architecture.ext.toHtml +import com.holo.loadstate.LoadService +import com.holo.wanandroid.Constant +import com.holo.wanandroid.R +import com.holo.wanandroid.databinding.FragmentTreeInfoBinding +import com.holo.wanandroid.ext.loadServiceInit +import com.holo.wanandroid.ext.parseListState +import com.holo.wanandroid.ui.WebActivity +import com.holo.wanandroid.ui.discovery.DiscoveryViewModel +import com.holo.wanandroid.ui.home.ArticleAdapter +import com.jaren.lib.view.LikeView +import dagger.hilt.android.AndroidEntryPoint + +/** + * + * + * @Author holo + * @Date 2022/4/17 + */ +@AndroidEntryPoint +class TreeInfoFragment : BaseFragment() { + + companion object { + fun newInstance(treeId: Int): TreeInfoFragment { + val fragment = TreeInfoFragment() + fragment.arguments = bundleOf(Constant.KEY_ID to treeId) + return fragment + } + } + + private val viewModel: DiscoveryViewModel by viewModels() + + private var treeId: Int = 0 + private lateinit var loads: LoadService + private lateinit var adapter: ArticleAdapter + + override fun initView(savedInstanceState: Bundle?) { + adapter = ArticleAdapter() + binding.container.recyclerView.adapter = adapter + adapter.setOnItemChildClickListener { _, view, position -> + when (view.id) { + R.id.tv_author -> { + } + R.id.like_view -> { + if (view is LikeView) { + view.toggle() + } + } + } + } + adapter.setOnItemClickListener { _, view, position -> + val item = adapter.getItem(position) + startActivity( + Constant.KEY_WEB_TITLE to item.title.toHtml(), + Constant.KEY_WEB_URL to item.link + ) + } + + adapter.loadMoreModule.setOnLoadMoreListener { viewModel.getTreeChildList(treeId, false) } + binding.container.refreshLayout.setOnRefreshListener { viewModel.getTreeChildList(treeId) } + loads = loadServiceInit(binding.container.recyclerView, adapter, R.layout.item_article_shimmer) { + viewModel.getTreeChildList(treeId) + } + } + + override fun initData() { + treeId = arguments?.getInt(Constant.KEY_ID, 0) ?: 0 + } + + override fun lazyLoadData() { + viewModel.getTreeChildList(treeId) + } + + override fun observeViewModel() { + observerList(viewModel.treeChildListResp) { state -> + parseListState(state, loads, binding.container.refreshLayout, adapter) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/home/ArticleAdapter.kt b/app/src/main/java/com/holo/wanandroid/ui/home/ArticleAdapter.kt new file mode 100644 index 0000000..f69c502 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/home/ArticleAdapter.kt @@ -0,0 +1,49 @@ +package com.holo.wanandroid.ui.home + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.module.LoadMoreModule +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.holo.architecture.ext.toGone +import com.holo.architecture.ext.toHtml +import com.holo.architecture.ext.toVisible +import com.holo.wanandroid.R +import com.holo.wanandroid.data.dto.ArticleBean +import com.holo.wanandroid.databinding.ItemArticleBinding +import com.holo.wanandroid.ext.getBinding + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +class ArticleAdapter(private val inHome: Boolean = false) : BaseQuickAdapter(R.layout.item_article, mutableListOf()), + LoadMoreModule { + + init { + addChildClickViewIds(R.id.tv_author, R.id.like_view) + } + + override fun convert(holder: BaseViewHolder, item: ArticleBean) { + val binding = holder.getBinding(ItemArticleBinding::bind) + binding.tvAuthor.text = item.authorShow() + binding.slNewTag.toVisible(item.fresh) + if (inHome) { + binding.slTypeTag.toVisible(!item.tags.isNullOrEmpty()) + if (item.tags.isNotEmpty()) { + binding.tvTypeTag.text = item.tags[0].name + } + } else { + binding.slTypeTag.toGone() + } + binding.slTopTag.toVisible(item.type == 1) + + binding.tvCreateTime.text = item.niceDate + binding.tvTitle.text = item.title.toHtml() + + binding.tvCategory.text = "${item.superChapterName}・${item.chapterName}".toHtml() + + binding.likeView.isChecked = item.collect + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/home/BannerImgAdapter.kt b/app/src/main/java/com/holo/wanandroid/ui/home/BannerImgAdapter.kt new file mode 100644 index 0000000..38c6137 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/home/BannerImgAdapter.kt @@ -0,0 +1,20 @@ +package com.holo.wanandroid.ui.home + +import coil.load +import com.holo.wanandroid.data.dto.BannerBean +import com.youth.banner.adapter.BannerImageAdapter +import com.youth.banner.holder.BannerImageHolder + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +class BannerImgAdapter : BannerImageAdapter(mutableListOf()) { + + override fun onBindView(holder: BannerImageHolder, data: BannerBean, position: Int, size: Int) { + holder.imageView.load(data.imagePath) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/home/HomeViewModel.kt b/app/src/main/java/com/holo/wanandroid/ui/home/HomeViewModel.kt new file mode 100644 index 0000000..52bd822 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/home/HomeViewModel.kt @@ -0,0 +1,64 @@ +package com.holo.wanandroid.ui.home + +import androidx.lifecycle.viewModelScope +import com.holo.architecture.base.viewmodel.BaseViewModel +import com.holo.architecture.network.ListState +import com.holo.architecture.network.State +import com.holo.wanandroid.data.dto.ArticleBean +import com.holo.wanandroid.data.dto.BannerBean +import com.holo.wanandroid.repository.WanRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.zip +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +@HiltViewModel +class HomeViewModel @Inject constructor( + private val repo: WanRepository +) : BaseViewModel() { + + private val _bannerListResp = MutableSharedFlow>>() + val bannerListResp = _bannerListResp.asSharedFlow() + + fun getBannerList() { + viewModelScope.launch { + repo.getBannerList().collect { _bannerListResp.emit(it) } + } + } + + private val _articleListResp = MutableSharedFlow>() + val articleListResp = _articleListResp.asSharedFlow() + + fun getArticleList(refresh: Boolean = true) { + if (refresh) pageNo = 0 else pageNo++ + viewModelScope.launch { + val listFlow = repo.getArticleList(pageNo) + val topFlow = if (refresh) repo.getTopArticleList() else flow { emit(emptyList()) } + listFlow.zip(topFlow) { list, top -> + if (top.isNullOrEmpty()) { + list + } else { + if (refresh) { + val temp = mutableListOf() + temp.addAll(top) + temp.addAll(list.listData) + ListState(true, refresh, list.hasMore, temp.isEmpty(), temp) + } else { + list + } + } + }.collect { + _articleListResp.emit(it) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/home/NavHomeFragment.kt b/app/src/main/java/com/holo/wanandroid/ui/home/NavHomeFragment.kt new file mode 100644 index 0000000..27a1da1 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/home/NavHomeFragment.kt @@ -0,0 +1,124 @@ +package com.holo.wanandroid.ui.home + +import android.os.Bundle +import androidx.fragment.app.viewModels +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.appbar.CollapsingToolbarLayout +import com.gyf.immersionbar.ktx.statusBarHeight +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.architecture.ext.startActivity +import com.holo.architecture.ext.toHtml +import com.holo.architecture.ext.toVisible +import com.holo.loadstate.LoadService +import com.holo.wanandroid.Constant +import com.holo.wanandroid.R +import com.holo.wanandroid.databinding.FragmentNavHomeBinding +import com.holo.wanandroid.ext.loadServiceInit +import com.holo.wanandroid.ext.parseListState +import com.holo.wanandroid.ext.showLoginDialog +import com.holo.wanandroid.ui.WebActivity +import com.holo.wanandroid.ui.mine.CollectViewModel +import com.holo.wanandroid.util.CacheUtil +import com.youth.banner.indicator.CircleIndicator +import dagger.hilt.android.AndroidEntryPoint +import kotlin.math.abs + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +@AndroidEntryPoint +class NavHomeFragment : BaseFragment() { + + private val viewModel: HomeViewModel by viewModels() + private val collectViewModel: CollectViewModel by viewModels() + + private lateinit var loads: LoadService + + private lateinit var bannerAdapter: BannerImgAdapter + private lateinit var adapter: ArticleAdapter + + override fun initView(savedInstanceState: Bundle?) { + val params = binding.titleBar.layoutParams as CollapsingToolbarLayout.LayoutParams + val height = statusBarHeight + params.height = height + binding.titleBar.layoutParams = params + binding.appbar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> + val scrollHeight = binding.bannerHomeTop.height - height + val fraction: Float = abs(verticalOffset * 1.0f) / scrollHeight + binding.titleBar.alpha = if (fraction >= 1) fraction else 0f + binding.titleBar.toVisible(fraction > 0.8) + }) + + bannerAdapter = BannerImgAdapter() + bannerAdapter.setOnBannerListener { data, position -> + val item = adapter.getItem(position) + startActivity( + Constant.KEY_WEB_TITLE to item.title.toHtml(), + Constant.KEY_WEB_URL to item.link + ) + } + binding.bannerHomeTop + .addBannerLifecycleObserver(this) + .setIndicator(CircleIndicator(activity)) + .setAdapter(bannerAdapter) + + adapter = ArticleAdapter(true) + binding.container.recyclerView.adapter = adapter + adapter.setOnItemChildClickListener { _, view, position -> + val item = adapter.getItem(position) + when (view.id) { + R.id.tv_author -> { + } + R.id.like_view -> { + if (CacheUtil.isLogin()) { + if (item.collect) { + collectViewModel.unCollect(item.id, position) + } else { + collectViewModel.collect(item.id, position) + } + } else { + activity?.showLoginDialog() + } + } + } + } + adapter.setOnItemClickListener { _, view, position -> + val item = adapter.getItem(position) + startActivity( + Constant.KEY_WEB_TITLE to item.title.toHtml(), + Constant.KEY_WEB_URL to item.link + ) + } + + adapter.loadMoreModule.setOnLoadMoreListener { viewModel.getArticleList(false) } + binding.container.refreshLayout.setOnRefreshListener { viewModel.getArticleList() } + loads = loadServiceInit(binding.container.recyclerView, adapter, R.layout.item_article_shimmer) { + viewModel.getArticleList() + } + } + + override fun initData() { + viewModel.getBannerList() + viewModel.getArticleList() + } + + override fun observeViewModel() { + observer(viewModel.bannerListResp) { + bannerAdapter.setDatas(it) + } + observerList(viewModel.articleListResp) { state -> + parseListState(state, loads, binding.container.refreshLayout, adapter) + } + observer(collectViewModel.collectResp, success = { + adapter.getItem(it).collect = true + adapter.notifyItemChanged(it) + }) + observer(collectViewModel.unCollectResp, success = { + adapter.getItem(it).collect = false + adapter.notifyItemChanged(it) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/login/LoginActivity.kt b/app/src/main/java/com/holo/wanandroid/ui/login/LoginActivity.kt new file mode 100644 index 0000000..076afb5 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/login/LoginActivity.kt @@ -0,0 +1,59 @@ +package com.holo.wanandroid.ui.login + +import android.os.Bundle +import androidx.activity.viewModels +import androidx.navigation.NavController +import androidx.navigation.Navigation +import com.holo.architecture.base.activity.BaseActivity +import com.holo.architecture.ext.showErrToast +import com.holo.architecture.network.State +import com.holo.architecture.utils.flowbus.observeEvent +import com.holo.wanandroid.R +import com.holo.wanandroid.data.dto.UserInfoBean +import com.holo.wanandroid.databinding.ActivityLoginBinding +import com.holo.wanandroid.event.LoginEvent +import com.holo.wanandroid.ext.initClose +import com.holo.wanandroid.ui.mine.UserViewModel +import dagger.hilt.android.AndroidEntryPoint + +/** + * 登录 + */ +@AndroidEntryPoint +class LoginActivity : BaseActivity() { + + private val viewModel: UserViewModel by viewModels() + + private lateinit var controller: NavController + + override fun initView(savedInstanceState: Bundle?) { + binding.titleBar.toolbar.initClose { finish() } + } + + override fun onStart() { + super.onStart() + controller = Navigation.findNavController(binding.navHostLogin) + controller.addOnDestinationChangedListener { controller, destination, arguments -> + when (destination.id) { + R.id.loginFragment -> binding.titleBar.tvToolbarTitle.text = "登录" + else -> binding.titleBar.tvToolbarTitle.text = "注册" + } + } + } + + override fun observeViewModel() { + observeEvent { event -> + when (event.state) { + is State.Loading -> showLoading() + is State.Success -> { + hideLoading() + finish() + } + is State.Error -> { + hideLoading() + showErrToast(event.state.err) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/login/LoginFragment.kt b/app/src/main/java/com/holo/wanandroid/ui/login/LoginFragment.kt new file mode 100644 index 0000000..d49db1d --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/login/LoginFragment.kt @@ -0,0 +1,40 @@ +package com.holo.wanandroid.ui.login + +import android.os.Bundle +import androidx.fragment.app.activityViewModels +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.architecture.ext.* +import com.holo.wanandroid.R +import com.holo.wanandroid.databinding.FragmentLoginBinding +import com.holo.wanandroid.ui.mine.UserViewModel +import dagger.hilt.android.AndroidEntryPoint + +/** + * 登录 + * + * @Author holo + * @Date 2022/4/18 + */ +@AndroidEntryPoint +class LoginFragment : BaseFragment() { + + private val viewModel: UserViewModel by activityViewModels() + + override fun initView(savedInstanceState: Bundle?) { + binding.etAccount.afterTextChanged { switchBtnEnable() } + binding.etPsw.afterTextChanged { switchBtnEnable() } + + binding.tvRegisterNow.clickNoRepeat { nav().navigateAction(R.id.action_login_to_register) } + binding.btnConfirm.clickNoRepeat { + viewModel.doLogin(binding.etAccount.text.toString(), binding.etPsw.text.toString()) + } + } + + private fun switchBtnEnable() { + binding.btnConfirm.isEnabled = binding.etAccount.isNotBlank() && binding.etPsw.isNotBlank() + } + + override fun observeViewModel() { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/login/RegisterFragment.kt b/app/src/main/java/com/holo/wanandroid/ui/login/RegisterFragment.kt new file mode 100644 index 0000000..6674ba2 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/login/RegisterFragment.kt @@ -0,0 +1,48 @@ +package com.holo.wanandroid.ui.login + +import android.os.Bundle +import androidx.fragment.app.activityViewModels +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.architecture.ext.* +import com.holo.wanandroid.databinding.FragmentRegisterBinding +import com.holo.wanandroid.ui.mine.UserViewModel +import dagger.hilt.android.AndroidEntryPoint + +/** + * 注册 + * + * @Author holo + * @Date 2022/4/18 + */ +@AndroidEntryPoint +class RegisterFragment : BaseFragment() { + + private val viewModel: UserViewModel by activityViewModels() + + override fun initView(savedInstanceState: Bundle?) { + binding.etAccount.afterTextChanged { switchBtnEnable() } + binding.etPsw.afterTextChanged { switchBtnEnable() } + binding.etRePsw.afterTextChanged { switchBtnEnable() } + + binding.btnConfirm.clickNoRepeat { + val psw = binding.etPsw.text.toString() + val rePsw = binding.etRePsw.text.toString() + if (psw != rePsw) { + showToast("两次密码输入不一致") + return@clickNoRepeat + } + viewModel.doRegister(binding.etAccount.text.toString(), psw, rePsw) + } + } + + private fun switchBtnEnable() { + binding.btnConfirm.isEnabled = binding.etAccount.isNotBlank() && binding.etPsw.isNotBlank() && binding.etRePsw.isNotBlank() + } + + override fun observeViewModel() { + observer(viewModel.registerResp, success = { + showToast("注册成功,请登录") + nav().navigateUp() + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/mine/CollectViewModel.kt b/app/src/main/java/com/holo/wanandroid/ui/mine/CollectViewModel.kt new file mode 100644 index 0000000..432afba --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/mine/CollectViewModel.kt @@ -0,0 +1,88 @@ +package com.holo.wanandroid.ui.mine + +import androidx.lifecycle.viewModelScope +import com.holo.architecture.base.viewmodel.BaseViewModel +import com.holo.architecture.ext.vibrator +import com.holo.architecture.initializer.appContext +import com.holo.architecture.network.ListState +import com.holo.architecture.network.State +import com.holo.wanandroid.data.dto.CollectBean +import com.holo.wanandroid.repository.WanRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * + * + * @Author holo + * @Date 2022/4/17 + */ +@HiltViewModel +class CollectViewModel @Inject constructor( + private val repo: WanRepository +) : BaseViewModel() { + + private val _collectResp = MutableSharedFlow>() + val collectResp = _collectResp.asSharedFlow() + + fun collect(id: Int, position: Int) { + appContext.vibrator?.vibrate(20) + viewModelScope.launch { + repo.collect(id).collect { + _collectResp.emit( + when (it) { + is State.Loading -> State.loading() + is State.Error -> State.error(it.err) + else -> State.success(position) + } + ) + } + } + } + + private val _unCollectResp = MutableSharedFlow>() + val unCollectResp = _unCollectResp.asSharedFlow() + + fun unCollect(id: Int, position: Int) { + appContext.vibrator?.vibrate(20) + viewModelScope.launch { + repo.unCollect(id).collect { + _unCollectResp.emit( + when (it) { + is State.Loading -> State.loading() + is State.Error -> State.error(it.err) + else -> State.success(position) + } + ) + } + } + } + + fun infoUnCollect(id: Int, originId: Int, position: Int) { + appContext.vibrator?.vibrate(20) + viewModelScope.launch { + repo.infoUnCollect(id, if (originId == 0) -1 else originId).collect { + _unCollectResp.emit( + when (it) { + is State.Loading -> State.loading() + is State.Error -> State.error(it.err) + else -> State.success(position) + } + ) + } + } + } + + private val _collectListResp = MutableSharedFlow>() + val collectListResp = _collectListResp.asSharedFlow() + + fun getCollectList(refresh: Boolean = true) { + if (refresh) pageNo = 0 else pageNo++ + viewModelScope.launch { + repo.getCollectList(pageNo).collect { _collectListResp.emit(it) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/mine/NavMineFragment.kt b/app/src/main/java/com/holo/wanandroid/ui/mine/NavMineFragment.kt new file mode 100644 index 0000000..4a4ef11 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/mine/NavMineFragment.kt @@ -0,0 +1,103 @@ +package com.holo.wanandroid.ui.mine + +import android.os.Bundle +import androidx.fragment.app.viewModels +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.architecture.ext.clickNoRepeat +import com.holo.architecture.ext.startActivity +import com.holo.architecture.ext.toVisible +import com.holo.architecture.network.State +import com.holo.architecture.utils.flowbus.observeEvent +import com.holo.wanandroid.Constant +import com.holo.wanandroid.databinding.FragmentNavMineBinding +import com.holo.wanandroid.event.LoginEvent +import com.holo.wanandroid.event.LogoutEvent +import com.holo.wanandroid.ext.showLoginDialog +import com.holo.wanandroid.ui.WebActivity +import com.holo.wanandroid.ui.login.LoginActivity +import com.holo.wanandroid.ui.mine.collect.MyCollectActivity +import com.holo.wanandroid.ui.mine.integral.IntegralRankListActivity +import com.holo.wanandroid.ui.mine.integral.MyIntegralDetailsActivity +import com.holo.wanandroid.util.CacheUtil +import dagger.hilt.android.AndroidEntryPoint + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +@AndroidEntryPoint +class NavMineFragment : BaseFragment() { + + private val viewModel: UserViewModel by viewModels() + + override fun initView(savedInstanceState: Bundle?) { + binding.ivAvatar.clickNoRepeat { startActivity() } + binding.tvUserName.clickNoRepeat { startActivity() } + + binding.vgMyIntegral.clickNoRepeat { + if (CacheUtil.isLogin()) { + startActivity() + } else { + activity?.showLoginDialog() + } + } + binding.vgIntegralRankList.clickNoRepeat { + startActivity() + } + binding.vgMyCollect.clickNoRepeat { + if (CacheUtil.isLogin()) { + startActivity() + } else { + activity?.showLoginDialog() + } + } + binding.vgGithub.clickNoRepeat { + startActivity( + Constant.KEY_WEB_TITLE to "Github", + Constant.KEY_WEB_URL to "https://github.com/holoXia" + ) + } + binding.tvLogout.clickNoRepeat { viewModel.doLogout() } + refreshUI() + } + + override fun initData() { + viewModel.getMyIntegral() + } + + private fun refreshUI() { + val userInfo = CacheUtil.getUserInfo() + binding.tvUserName.text = userInfo?.getShowName() ?: "请登录" + binding.tvLogout.toVisible(userInfo != null) + if (userInfo == null) { + binding.tvLevel.text = "等级:-" + binding.tvRank.text = "排名:-" + binding.tvIntegral.text = "" + } + } + + override fun observeViewModel() { + observerObj(viewModel.myIntegralResp) { bean -> + bean?.let { + binding.tvLevel.text = "等级:${bean.level}" + binding.tvRank.text = "排名:${bean.rank}" + binding.tvIntegral.text = bean.coinCount.toString() + } + } + observeEvent { event -> + if (event.state is State.Success) { + CacheUtil.saveUserInfo(event.state.data) + refreshUI() + viewModel.getMyIntegral() + } + } + observeEvent { event -> + if (event.state is State.Success) { + CacheUtil.saveUserInfo(null) + refreshUI() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/mine/UserViewModel.kt b/app/src/main/java/com/holo/wanandroid/ui/mine/UserViewModel.kt new file mode 100644 index 0000000..a5adc9e --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/mine/UserViewModel.kt @@ -0,0 +1,84 @@ +package com.holo.wanandroid.ui.mine + +import androidx.lifecycle.viewModelScope +import com.holo.architecture.base.viewmodel.BaseViewModel +import com.holo.architecture.network.ListState +import com.holo.architecture.network.State +import com.holo.architecture.utils.flowbus.postEvent +import com.holo.wanandroid.data.dto.IntegralBean +import com.holo.wanandroid.data.dto.IntegralInfoBean +import com.holo.wanandroid.data.dto.UserInfoBean +import com.holo.wanandroid.event.LoginEvent +import com.holo.wanandroid.event.LogoutEvent +import com.holo.wanandroid.repository.UserRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * + * + * @Author holo + * @Date 2022/4/17 + */ +@HiltViewModel +class UserViewModel @Inject constructor( + private val repo: UserRepository +) : BaseViewModel() { + + fun doLogin(account: String, psw: String) { + viewModelScope.launch { + repo.doLogin(account, psw).collect { + postEvent(LoginEvent(it)) + } + } + } + + fun doLogout() { + viewModelScope.launch { + repo.doLogout().collect { + postEvent(LogoutEvent(it)) + } + } + } + + private val _registerResp = MutableSharedFlow>() + val registerResp = _registerResp.asSharedFlow() + + fun doRegister(account: String, psw: String, rePsw: String) { + viewModelScope.launch { + repo.doRegister(account, psw, rePsw).collect { _registerResp.emit(it) } + } + } + + private val _myIntegralResp = MutableSharedFlow() + val myIntegralResp = _myIntegralResp.asSharedFlow() + + fun getMyIntegral() { + viewModelScope.launch { + repo.getMyIntegral().collect { _myIntegralResp.emit(it ?: IntegralBean()) } + } + } + + private val _myIntegralListResp = MutableSharedFlow>() + val myIntegralListResp = _myIntegralListResp.asSharedFlow() + + fun getMyIntegralList(refresh: Boolean = true) { + if (refresh) pageNo = 1 else pageNo++ + viewModelScope.launch { + repo.getMyIntegralList(pageNo, refresh).collect { _myIntegralListResp.emit(it) } + } + } + + private val _integralRankListResp = MutableSharedFlow>() + val integralRankListResp = _integralRankListResp.asSharedFlow() + + fun getIntegralRankList(refresh: Boolean = true) { + if (refresh) pageNo = 1 else pageNo++ + viewModelScope.launch { + repo.getIntegralRankList(pageNo, refresh).collect { _integralRankListResp.emit(it) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/mine/collect/CollectAdapter.kt b/app/src/main/java/com/holo/wanandroid/ui/mine/collect/CollectAdapter.kt new file mode 100644 index 0000000..5671218 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/mine/collect/CollectAdapter.kt @@ -0,0 +1,62 @@ +package com.holo.wanandroid.ui.mine.collect + +import coil.load +import com.chad.library.adapter.base.BaseMultiItemQuickAdapter +import com.chad.library.adapter.base.module.LoadMoreModule +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.holo.architecture.ext.toHtml +import com.holo.wanandroid.R +import com.holo.wanandroid.data.dto.CollectBean +import com.holo.wanandroid.data.dto.CollectType +import com.holo.wanandroid.databinding.ItemArticleBinding +import com.holo.wanandroid.databinding.ItemProjectsBinding +import com.holo.wanandroid.ext.getBinding + +/** + * + * + * @Author holo + * @Date 2022/4/18 + */ +class CollectAdapter : BaseMultiItemQuickAdapter(mutableListOf()), LoadMoreModule { + + init { + addItemType(CollectType.COLLECT_ARTICLE, R.layout.item_article) + addItemType(CollectType.COLLECT_PROJECTS, R.layout.item_projects) + addChildClickViewIds(R.id.like_view) + } + + override fun convert(holder: BaseViewHolder, item: CollectBean) { + when (holder.itemViewType) { + CollectType.COLLECT_ARTICLE -> convertArticle(holder, item) + CollectType.COLLECT_PROJECTS -> convertProjects(holder, item) + } + } + + private fun convertArticle(holder: BaseViewHolder, item: CollectBean) { + val binding = holder.getBinding(ItemArticleBinding::bind) + + binding.tvAuthor.text = item.author + + binding.tvCreateTime.text = item.niceDate + binding.tvTitle.text = item.title.toHtml() + + binding.tvCategory.text = "${item.chapterName}".toHtml() + + binding.likeView.isChecked = true + } + + private fun convertProjects(holder: BaseViewHolder, item: CollectBean) { + val binding = holder.getBinding(ItemProjectsBinding::bind) + + binding.ivCover.load(item.envelopePic) { + error(R.mipmap.ic_img_default) + } + binding.tvTitle.text = item.title.toHtml() + binding.tvContent.text = item.desc.toHtml() + binding.tvAuthor.text = item.author + binding.tvCreateTime.text = item.niceDate + + binding.likeView.isChecked = true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/mine/collect/MyCollectActivity.kt b/app/src/main/java/com/holo/wanandroid/ui/mine/collect/MyCollectActivity.kt new file mode 100644 index 0000000..488bce7 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/mine/collect/MyCollectActivity.kt @@ -0,0 +1,68 @@ +package com.holo.wanandroid.ui.mine.collect + +import android.os.Bundle +import androidx.activity.viewModels +import com.holo.architecture.base.activity.BaseActivity +import com.holo.architecture.ext.startActivity +import com.holo.architecture.ext.toHtml +import com.holo.loadstate.LoadService +import com.holo.wanandroid.Constant +import com.holo.wanandroid.R +import com.holo.wanandroid.databinding.ActivityMyCollectBinding +import com.holo.wanandroid.ext.initClose +import com.holo.wanandroid.ext.loadServiceInit +import com.holo.wanandroid.ext.parseListState +import com.holo.wanandroid.ui.WebActivity +import com.holo.wanandroid.ui.mine.CollectViewModel +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MyCollectActivity : BaseActivity() { + + private val viewModel: CollectViewModel by viewModels() + + private lateinit var loads: LoadService + private lateinit var adapter: CollectAdapter + + override fun initView(savedInstanceState: Bundle?) { + binding.titleBar.toolbar.initClose("我的收藏") { finish() } + + adapter = CollectAdapter() + binding.container.recyclerView.adapter = adapter + adapter.loadMoreModule.setOnLoadMoreListener { viewModel.getCollectList(false) } + loads = loadServiceInit(binding.container.recyclerView, adapter, R.layout.item_projects_shimmer) { + viewModel.getCollectList() + } + binding.container.refreshLayout.setOnRefreshListener { viewModel.getCollectList() } + + adapter.setOnItemClickListener { _, view, position -> + val item = adapter.getItem(position) + startActivity( + Constant.KEY_WEB_TITLE to item.title.toHtml(), + Constant.KEY_WEB_URL to item.link + ) + } + adapter.setOnItemChildClickListener { _, view, position -> + val item = adapter.getItem(position) + when (view.id) { + R.id.like_view -> { + viewModel.infoUnCollect(item.id, item.originId, position) + } + } + } + } + + override fun initData() { + viewModel.getCollectList() + } + + override fun observeViewModel() { + observerList(viewModel.collectListResp) { state -> + parseListState(state, loads, binding.container.refreshLayout, adapter) + } + observer(viewModel.unCollectResp, success = { position -> + adapter.data.removeAt(position) + adapter.notifyItemRemoved(position) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/mine/integral/IntegralAdapter.kt b/app/src/main/java/com/holo/wanandroid/ui/mine/integral/IntegralAdapter.kt new file mode 100644 index 0000000..9426bc6 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/mine/integral/IntegralAdapter.kt @@ -0,0 +1,39 @@ +package com.holo.wanandroid.ui.mine.integral + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.module.LoadMoreModule +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.holo.architecture.ext.toInvisible +import com.holo.architecture.ext.toVisible +import com.holo.wanandroid.R +import com.holo.wanandroid.data.dto.IntegralBean +import com.holo.wanandroid.databinding.ItemIntegralBinding +import com.holo.wanandroid.ext.getBinding + +/** + * + * + * @Author holo + * @Date 2022/4/18 + */ +class IntegralAdapter : BaseQuickAdapter(R.layout.item_integral, mutableListOf()), LoadMoreModule { + + override fun convert(holder: BaseViewHolder, item: IntegralBean) { + val binding = holder.getBinding(ItemIntegralBinding::bind) + + binding.vDivider.toVisible(holder.bindingAdapterPosition < (data.size - 1)) + + binding.ivRankTop.toInvisible(holder.bindingAdapterPosition < 3) + binding.tvRank.toVisible(holder.bindingAdapterPosition >= 3) + when (holder.bindingAdapterPosition) { + 0 -> binding.ivRankTop.setImageResource(R.mipmap.ic_rank_no1) + 1 -> binding.ivRankTop.setImageResource(R.mipmap.ic_rank_no2) + 2 -> binding.ivRankTop.setImageResource(R.mipmap.ic_rank_no3) + else -> binding.tvRank.text = "${holder.bindingAdapterPosition + 1}" + } + + binding.tvUserName.text = item.getShowName() + binding.tvIntegral.text = item.coinCount.toString() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/mine/integral/IntegralDetailsAdapter.kt b/app/src/main/java/com/holo/wanandroid/ui/mine/integral/IntegralDetailsAdapter.kt new file mode 100644 index 0000000..cd40b14 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/mine/integral/IntegralDetailsAdapter.kt @@ -0,0 +1,26 @@ +package com.holo.wanandroid.ui.mine.integral + +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.module.LoadMoreModule +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.holo.wanandroid.R +import com.holo.wanandroid.data.dto.IntegralInfoBean +import com.holo.wanandroid.databinding.ItemIntegralDetailsBinding +import com.holo.wanandroid.ext.getBinding + +/** + * + * + * @Author holo + * @Date 2022/4/18 + */ +class IntegralDetailsAdapter : BaseQuickAdapter(R.layout.item_integral_details, mutableListOf()), LoadMoreModule { + + override fun convert(holder: BaseViewHolder, item: IntegralInfoBean) { + val binding = holder.getBinding(ItemIntegralDetailsBinding::bind) + + binding.tvTitle.text = item.reason + binding.tvCreateTime.text = item.desc + binding.tvIntegral.text = "+${item.coinCount}" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/mine/integral/IntegralRankListActivity.kt b/app/src/main/java/com/holo/wanandroid/ui/mine/integral/IntegralRankListActivity.kt new file mode 100644 index 0000000..133e024 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/mine/integral/IntegralRankListActivity.kt @@ -0,0 +1,45 @@ +package com.holo.wanandroid.ui.mine.integral + +import android.os.Bundle +import androidx.activity.viewModels +import com.holo.architecture.base.activity.BaseActivity +import com.holo.loadstate.LoadService +import com.holo.wanandroid.R +import com.holo.wanandroid.databinding.ActivityIntegralRankListBinding +import com.holo.wanandroid.ext.initClose +import com.holo.wanandroid.ext.loadServiceInit +import com.holo.wanandroid.ext.parseListState +import com.holo.wanandroid.ui.mine.UserViewModel +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class IntegralRankListActivity : BaseActivity() { + + private val viewModel: UserViewModel by viewModels() + + private lateinit var loads: LoadService + private lateinit var adapter: IntegralAdapter + + override fun initView(savedInstanceState: Bundle?) { + binding.titleBar.toolbar.initClose("积分排行") { finish() } + + adapter = IntegralAdapter() + binding.container.recyclerView.adapter = adapter + adapter.loadMoreModule.setOnLoadMoreListener { viewModel.getIntegralRankList(false) } + loads = loadServiceInit(binding.container.recyclerView, adapter, R.layout.item_integral_shimmer) { + viewModel.getIntegralRankList() + } + loads.config { setItemCount(15) } + binding.container.refreshLayout.setOnRefreshListener { viewModel.getIntegralRankList() } + } + + override fun initData() { + viewModel.getIntegralRankList() + } + + override fun observeViewModel() { + observerList(viewModel.integralRankListResp) { state -> + parseListState(state, loads, binding.container.refreshLayout, adapter) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/mine/integral/MyIntegralDetailsActivity.kt b/app/src/main/java/com/holo/wanandroid/ui/mine/integral/MyIntegralDetailsActivity.kt new file mode 100644 index 0000000..287158c --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/mine/integral/MyIntegralDetailsActivity.kt @@ -0,0 +1,45 @@ +package com.holo.wanandroid.ui.mine.integral + +import android.os.Bundle +import androidx.activity.viewModels +import com.holo.architecture.base.activity.BaseActivity +import com.holo.loadstate.LoadService +import com.holo.wanandroid.R +import com.holo.wanandroid.databinding.ActivityMyIntegralDetailsBinding +import com.holo.wanandroid.ext.initClose +import com.holo.wanandroid.ext.loadServiceInit +import com.holo.wanandroid.ext.parseListState +import com.holo.wanandroid.ui.mine.UserViewModel +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MyIntegralDetailsActivity : BaseActivity() { + + private val viewModel: UserViewModel by viewModels() + + private lateinit var loads: LoadService + private lateinit var adapter: IntegralDetailsAdapter + + override fun initView(savedInstanceState: Bundle?) { + binding.titleBar.toolbar.initClose("我的积分") { finish() } + + adapter = IntegralDetailsAdapter() + binding.container.recyclerView.adapter = adapter + adapter.loadMoreModule.setOnLoadMoreListener { viewModel.getMyIntegralList(false) } + loads = loadServiceInit(binding.container.recyclerView, adapter, R.layout.item_integral_details_shimmer) { + viewModel.getMyIntegralList() + } + loads.config { setItemCount(15) } + binding.container.refreshLayout.setOnRefreshListener { viewModel.getMyIntegralList() } + } + + override fun initData() { + viewModel.getMyIntegralList() + } + + override fun observeViewModel() { + observerList(viewModel.myIntegralListResp) { state -> + parseListState(state, loads, binding.container.refreshLayout, adapter) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/projects/NavProjectsFragment.kt b/app/src/main/java/com/holo/wanandroid/ui/projects/NavProjectsFragment.kt new file mode 100644 index 0000000..bccd4cb --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/projects/NavProjectsFragment.kt @@ -0,0 +1,53 @@ +package com.holo.wanandroid.ui.projects + +import android.os.Bundle +import android.widget.LinearLayout +import androidx.fragment.app.viewModels +import com.google.android.material.tabs.TabLayoutMediator +import com.gyf.immersionbar.ktx.statusBarHeight +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.wanandroid.databinding.FragmentNavProjectsBinding +import com.holo.wanandroid.ext.init +import dagger.hilt.android.AndroidEntryPoint + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +@AndroidEntryPoint +class NavProjectsFragment : BaseFragment() { + + private val viewModel: ProjectsViewModel by viewModels() + + private val fragmentList: MutableList> = mutableListOf() + private val titleList: MutableList = mutableListOf() + + override fun initView(savedInstanceState: Bundle?) { + val params = binding.titleBar.layoutParams as LinearLayout.LayoutParams + params.height = statusBarHeight + binding.titleBar.layoutParams = params + } + + override fun initData() { + viewModel.getProjectCategories() + } + + override fun observeViewModel() { + observerObj(viewModel.categoryListResp) { list -> + activity?.let { act -> + fragmentList.add(ProjectsFragment.newInstance(-1)) + titleList.add("最新项目") + list?.forEach { bean -> + fragmentList.add(ProjectsFragment.newInstance(bean.id)) + titleList.add(bean.name) + } + binding.viewPager.init(act, fragmentList) + TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position -> + tab.text = titleList[position] + }.attach() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/projects/ProjectsAdapter.kt b/app/src/main/java/com/holo/wanandroid/ui/projects/ProjectsAdapter.kt new file mode 100644 index 0000000..6aee576 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/projects/ProjectsAdapter.kt @@ -0,0 +1,39 @@ +package com.holo.wanandroid.ui.projects + +import coil.load +import com.chad.library.adapter.base.BaseQuickAdapter +import com.chad.library.adapter.base.module.LoadMoreModule +import com.chad.library.adapter.base.viewholder.BaseViewHolder +import com.holo.architecture.ext.toHtml +import com.holo.wanandroid.R +import com.holo.wanandroid.data.dto.ProjectBean +import com.holo.wanandroid.databinding.ItemProjectsBinding +import com.holo.wanandroid.ext.getBinding + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +class ProjectsAdapter : BaseQuickAdapter(R.layout.item_projects, mutableListOf()), LoadMoreModule { + + init { + addChildClickViewIds(R.id.like_view) + } + + override fun convert(holder: BaseViewHolder, item: ProjectBean) { + val binding = holder.getBinding(ItemProjectsBinding::bind) + + binding.ivCover.load(item.envelopePic) { + error(R.mipmap.ic_img_default) + } + binding.tvTitle.text = item.title.toHtml() + binding.tvContent.text = item.desc.toHtml() + binding.tvAuthor.text = item.authorShow() + binding.tvCreateTime.text = item.niceDate + + binding.likeView.isChecked = item.collect + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/projects/ProjectsFragment.kt b/app/src/main/java/com/holo/wanandroid/ui/projects/ProjectsFragment.kt new file mode 100644 index 0000000..ca2e69a --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/projects/ProjectsFragment.kt @@ -0,0 +1,101 @@ +package com.holo.wanandroid.ui.projects + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.architecture.ext.startActivity +import com.holo.architecture.ext.toHtml +import com.holo.loadstate.LoadService +import com.holo.wanandroid.Constant +import com.holo.wanandroid.R +import com.holo.wanandroid.databinding.FragmentProjectsBinding +import com.holo.wanandroid.ext.loadServiceInit +import com.holo.wanandroid.ext.parseListState +import com.holo.wanandroid.ext.showLoginDialog +import com.holo.wanandroid.ui.WebActivity +import com.holo.wanandroid.ui.mine.CollectViewModel +import com.holo.wanandroid.util.CacheUtil +import dagger.hilt.android.AndroidEntryPoint + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +@AndroidEntryPoint +class ProjectsFragment : BaseFragment() { + + companion object { + fun newInstance(categoryId: Int = -1): ProjectsFragment { + val fragment = ProjectsFragment() + fragment.arguments = bundleOf(Constant.KEY_ID to categoryId) + return fragment + } + } + + private val viewModel: ProjectsViewModel by viewModels() + private val collectViewModel: CollectViewModel by viewModels() + + private var categoryId: Int = -1 + private lateinit var loads: LoadService + private lateinit var adapter: ProjectsAdapter + + override fun initView(savedInstanceState: Bundle?) { + adapter = ProjectsAdapter() + binding.container.recyclerView.adapter = adapter + adapter.loadMoreModule.setOnLoadMoreListener { viewModel.getProjectList(categoryId, false) } + loads = loadServiceInit(binding.container.recyclerView, adapter, R.layout.item_projects_shimmer) { + viewModel.getProjectList(categoryId) + } + binding.container.refreshLayout.setOnRefreshListener { viewModel.getProjectList(categoryId) } + + adapter.setOnItemClickListener { _, view, position -> + val item = adapter.getItem(position) + startActivity( + Constant.KEY_WEB_TITLE to item.title.toHtml(), + Constant.KEY_WEB_URL to item.link + ) + } + adapter.setOnItemChildClickListener { _, view, position -> + val item = adapter.getItem(position) + when (view.id) { + R.id.like_view -> { + if (CacheUtil.isLogin()) { + if (item.collect) { + collectViewModel.unCollect(item.id, position) + } else { + collectViewModel.collect(item.id, position) + } + } else { + activity?.showLoginDialog() + } + } + } + } + } + + + override fun initData() { + categoryId = arguments?.getInt(Constant.KEY_ID, -1) ?: -1 + } + + override fun lazyLoadData() { + viewModel.getProjectList(categoryId) + } + + override fun observeViewModel() { + observerList(viewModel.projectListResp) { state -> + parseListState(state, loads, binding.container.refreshLayout, adapter) + } + observer(collectViewModel.collectResp, success = { + adapter.getItem(it).collect = true + adapter.notifyItemChanged(it) + }) + observer(collectViewModel.unCollectResp, success = { + adapter.getItem(it).collect = false + adapter.notifyItemChanged(it) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/projects/ProjectsViewModel.kt b/app/src/main/java/com/holo/wanandroid/ui/projects/ProjectsViewModel.kt new file mode 100644 index 0000000..83d4e75 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/projects/ProjectsViewModel.kt @@ -0,0 +1,48 @@ +package com.holo.wanandroid.ui.projects + +import androidx.lifecycle.viewModelScope +import com.holo.architecture.base.viewmodel.BaseViewModel +import com.holo.architecture.network.ListState +import com.holo.wanandroid.data.dto.ProjectBean +import com.holo.wanandroid.data.dto.CategoryBean +import com.holo.wanandroid.repository.WanRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +@HiltViewModel +data class ProjectsViewModel @Inject constructor( + private val repo: WanRepository +) : BaseViewModel() { + + private val _categoryListResp = MutableSharedFlow>() + val categoryListResp = _categoryListResp.asSharedFlow() + + fun getProjectCategories() { + viewModelScope.launch { + repo.getProjectCategories().collect { _categoryListResp.emit(it ?: emptyList()) } + } + } + + private val _projectListResp = MutableSharedFlow>() + val projectListResp = _projectListResp.asSharedFlow() + + fun getProjectList(categoryId: Int, refresh: Boolean = true) { + if (refresh) pageNo = 0 else pageNo++ + viewModelScope.launch { + if (categoryId == -1) { + repo.getLatestProjectList(pageNo) + } else { + repo.getProjectList(pageNo, categoryId) + }.collect { _projectListResp.emit(it) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/publicnum/NavPublicNumFragment.kt b/app/src/main/java/com/holo/wanandroid/ui/publicnum/NavPublicNumFragment.kt new file mode 100644 index 0000000..78896be --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/publicnum/NavPublicNumFragment.kt @@ -0,0 +1,53 @@ +package com.holo.wanandroid.ui.publicnum + +import android.os.Bundle +import android.widget.LinearLayout +import androidx.fragment.app.viewModels +import com.google.android.material.tabs.TabLayoutMediator +import com.gyf.immersionbar.ktx.statusBarHeight +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.wanandroid.databinding.FragmentNavPublicnumBinding +import com.holo.wanandroid.ext.init +import com.holo.wanandroid.ui.discovery.DiscoveryViewModel +import com.holo.wanandroid.ui.projects.ProjectsFragment +import dagger.hilt.android.AndroidEntryPoint + +/** + * + * + * @Author holo + * @Date 2022/4/15 + */ +@AndroidEntryPoint +class NavPublicNumFragment : BaseFragment() { + + private val viewModel: DiscoveryViewModel by viewModels() + + private val fragmentList: MutableList> = mutableListOf() + private val titleList: MutableList = mutableListOf() + + override fun initView(savedInstanceState: Bundle?) { + val params = binding.titleBar.layoutParams as LinearLayout.LayoutParams + params.height = statusBarHeight + binding.titleBar.layoutParams = params + } + + override fun initData() { + viewModel.getPublicNumList() + } + + override fun observeViewModel() { + observerObj(viewModel.publicNumListResp) { list -> + activity?.let { act -> + list?.forEach { bean -> + fragmentList.add(PublicArticleFragment.newInstance(bean.id)) + titleList.add(bean.name) + } + binding.viewPager.init(act, fragmentList) + TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position -> + tab.text = titleList[position] + }.attach() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/ui/publicnum/PublicArticleFragment.kt b/app/src/main/java/com/holo/wanandroid/ui/publicnum/PublicArticleFragment.kt new file mode 100644 index 0000000..6ffff72 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/ui/publicnum/PublicArticleFragment.kt @@ -0,0 +1,105 @@ +package com.holo.wanandroid.ui.publicnum + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.viewModels +import com.holo.architecture.base.fragment.BaseFragment +import com.holo.architecture.ext.startActivity +import com.holo.architecture.ext.toHtml +import com.holo.loadstate.LoadService +import com.holo.wanandroid.Constant +import com.holo.wanandroid.R +import com.holo.wanandroid.databinding.FragmentPublicArticleBinding +import com.holo.wanandroid.ext.loadServiceInit +import com.holo.wanandroid.ext.parseListState +import com.holo.wanandroid.ext.showLoginDialog +import com.holo.wanandroid.ui.WebActivity +import com.holo.wanandroid.ui.discovery.DiscoveryViewModel +import com.holo.wanandroid.ui.home.ArticleAdapter +import com.holo.wanandroid.ui.mine.CollectViewModel +import com.holo.wanandroid.util.CacheUtil +import dagger.hilt.android.AndroidEntryPoint + +/** + * + * + * @Author holo + * @Date 2022/4/16 + */ +@AndroidEntryPoint +class PublicArticleFragment : BaseFragment() { + + companion object { + fun newInstance(categoryId: Int = -1): PublicArticleFragment { + val fragment = PublicArticleFragment() + fragment.arguments = bundleOf(Constant.KEY_ID to categoryId) + return fragment + } + } + + private val viewModel: DiscoveryViewModel by viewModels() + private val collectViewModel: CollectViewModel by viewModels() + + private var publicNumId: Int = 0 + private lateinit var loads: LoadService + private lateinit var adapter: ArticleAdapter + + override fun initView(savedInstanceState: Bundle?) { + adapter = ArticleAdapter() + binding.container.recyclerView.adapter = adapter + adapter.setOnItemChildClickListener { _, view, position -> + val item = adapter.getItem(position) + when (view.id) { + R.id.tv_author -> { + } + R.id.like_view -> { + if (CacheUtil.isLogin()) { + if (item.collect) { + collectViewModel.unCollect(item.id, position) + } else { + collectViewModel.collect(item.id, position) + } + } else { + activity?.showLoginDialog() + } + } + } + } + adapter.setOnItemClickListener { _, view, position -> + val item = adapter.getItem(position) + startActivity( + Constant.KEY_WEB_TITLE to item.title.toHtml(), + Constant.KEY_WEB_URL to item.link + ) + } + + adapter.loadMoreModule.setOnLoadMoreListener { viewModel.getPublicArticleList(publicNumId, false) } + binding.container.refreshLayout.setOnRefreshListener { viewModel.getPublicArticleList(publicNumId) } + loads = loadServiceInit(binding.container.recyclerView, adapter, R.layout.item_article_shimmer) { + viewModel.getPublicArticleList(publicNumId) + } + } + + override fun initData() { + publicNumId = arguments?.getInt(Constant.KEY_ID, 0) ?: 0 + } + + override fun lazyLoadData() { + viewModel.getPublicArticleList(publicNumId) + } + + override fun observeViewModel() { + observerList(viewModel.publicArticleListResp) { state -> + parseListState(state, loads, binding.container.refreshLayout, adapter) + } + observer(collectViewModel.collectResp, success = { + adapter.getItem(it).collect = true + adapter.notifyItemChanged(it) + }) + observer(collectViewModel.unCollectResp, success = { + adapter.getItem(it).collect = false + adapter.notifyItemChanged(it) + }) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/holo/wanandroid/util/CacheUtil.kt b/app/src/main/java/com/holo/wanandroid/util/CacheUtil.kt new file mode 100644 index 0000000..a7e8515 --- /dev/null +++ b/app/src/main/java/com/holo/wanandroid/util/CacheUtil.kt @@ -0,0 +1,21 @@ +package com.holo.wanandroid.util + +import com.holo.wanandroid.data.dto.UserInfoBean +import com.tencent.mmkv.MMKV + +/** + * + * + * @Author holo + * @Date 2022/4/17 + */ +object CacheUtil { + + private const val SP_USER_INFO = "user_info" + + fun saveUserInfo(user: UserInfoBean?) = MMKV.defaultMMKV().encode(SP_USER_INFO, user) + + fun getUserInfo() = MMKV.defaultMMKV().decodeParcelable(SP_USER_INFO, UserInfoBean::class.java) + + fun isLogin() = getUserInfo() != null +} \ No newline at end of file diff --git a/app/src/main/res/color/nav_color_text.xml b/app/src/main/res/color/nav_color_text.xml new file mode 100644 index 0000000..98259f6 --- /dev/null +++ b/app/src/main/res/color/nav_color_text.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_click_button.xml b/app/src/main/res/drawable/bg_click_button.xml new file mode 100644 index 0000000..bd43f06 --- /dev/null +++ b/app/src/main/res/drawable/bg_click_button.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/shape_gradient.xml b/app/src/main/res/drawable/shape_gradient.xml new file mode 100644 index 0000000..f8dee9c --- /dev/null +++ b/app/src/main/res/drawable/shape_gradient.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/tab_indicator.xml b/app/src/main/res/drawable/tab_indicator.xml new file mode 100644 index 0000000..bf1d157 --- /dev/null +++ b/app/src/main/res/drawable/tab_indicator.xml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/activity_integral_rank_list.xml b/app/src/main/res/layout/activity_integral_rank_list.xml new file mode 100644 index 0000000..3ed6bca --- /dev/null +++ b/app/src/main/res/layout/activity_integral_rank_list.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..7c03f84 --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b982d31 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_my_collect.xml b/app/src/main/res/layout/activity_my_collect.xml new file mode 100644 index 0000000..0e51cbc --- /dev/null +++ b/app/src/main/res/layout/activity_my_collect.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_my_integral_details.xml b/app/src/main/res/layout/activity_my_integral_details.xml new file mode 100644 index 0000000..3ed6bca --- /dev/null +++ b/app/src/main/res/layout/activity_my_integral_details.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_tree_info.xml b/app/src/main/res/layout/activity_tree_info.xml new file mode 100644 index 0000000..da82d84 --- /dev/null +++ b/app/src/main/res/layout/activity_tree_info.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_web.xml b/app/src/main/res/layout/activity_web.xml new file mode 100644 index 0000000..817b1a4 --- /dev/null +++ b/app/src/main/res/layout/activity_web.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_login.xml b/app/src/main/res/layout/dialog_login.xml new file mode 100644 index 0000000..e05e96d --- /dev/null +++ b/app/src/main/res/layout/dialog_login.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml new file mode 100644 index 0000000..6fc9617 --- /dev/null +++ b/app/src/main/res/layout/fragment_login.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_nav_discovery.xml b/app/src/main/res/layout/fragment_nav_discovery.xml new file mode 100644 index 0000000..b29b09d --- /dev/null +++ b/app/src/main/res/layout/fragment_nav_discovery.xml @@ -0,0 +1,27 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_nav_home.xml b/app/src/main/res/layout/fragment_nav_home.xml new file mode 100644 index 0000000..15b70ec --- /dev/null +++ b/app/src/main/res/layout/fragment_nav_home.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_nav_mine.xml b/app/src/main/res/layout/fragment_nav_mine.xml new file mode 100644 index 0000000..2de335c --- /dev/null +++ b/app/src/main/res/layout/fragment_nav_mine.xml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_nav_projects.xml b/app/src/main/res/layout/fragment_nav_projects.xml new file mode 100644 index 0000000..56ce0c1 --- /dev/null +++ b/app/src/main/res/layout/fragment_nav_projects.xml @@ -0,0 +1,26 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_nav_publicnum.xml b/app/src/main/res/layout/fragment_nav_publicnum.xml new file mode 100644 index 0000000..6b58653 --- /dev/null +++ b/app/src/main/res/layout/fragment_nav_publicnum.xml @@ -0,0 +1,27 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_navigation.xml b/app/src/main/res/layout/fragment_navigation.xml new file mode 100644 index 0000000..1b30997 --- /dev/null +++ b/app/src/main/res/layout/fragment_navigation.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_plaza.xml b/app/src/main/res/layout/fragment_plaza.xml new file mode 100644 index 0000000..1b30997 --- /dev/null +++ b/app/src/main/res/layout/fragment_plaza.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_projects.xml b/app/src/main/res/layout/fragment_projects.xml new file mode 100644 index 0000000..e85fb66 --- /dev/null +++ b/app/src/main/res/layout/fragment_projects.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_public_article.xml b/app/src/main/res/layout/fragment_public_article.xml new file mode 100644 index 0000000..d0a0486 --- /dev/null +++ b/app/src/main/res/layout/fragment_public_article.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_qa.xml b/app/src/main/res/layout/fragment_qa.xml new file mode 100644 index 0000000..1b30997 --- /dev/null +++ b/app/src/main/res/layout/fragment_qa.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_register.xml b/app/src/main/res/layout/fragment_register.xml new file mode 100644 index 0000000..f1ef7c5 --- /dev/null +++ b/app/src/main/res/layout/fragment_register.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_tree.xml b/app/src/main/res/layout/fragment_tree.xml new file mode 100644 index 0000000..1b30997 --- /dev/null +++ b/app/src/main/res/layout/fragment_tree.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_tree_info.xml b/app/src/main/res/layout/fragment_tree_info.xml new file mode 100644 index 0000000..d0a0486 --- /dev/null +++ b/app/src/main/res/layout/fragment_tree_info.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_article.xml b/app/src/main/res/layout/item_article.xml new file mode 100644 index 0000000..6266bbe --- /dev/null +++ b/app/src/main/res/layout/item_article.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_article_shimmer.xml b/app/src/main/res/layout/item_article_shimmer.xml new file mode 100644 index 0000000..31daf88 --- /dev/null +++ b/app/src/main/res/layout/item_article_shimmer.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_integral.xml b/app/src/main/res/layout/item_integral.xml new file mode 100644 index 0000000..7e0378a --- /dev/null +++ b/app/src/main/res/layout/item_integral.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_integral_details.xml b/app/src/main/res/layout/item_integral_details.xml new file mode 100644 index 0000000..720164d --- /dev/null +++ b/app/src/main/res/layout/item_integral_details.xml @@ -0,0 +1,43 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_integral_details_shimmer.xml b/app/src/main/res/layout/item_integral_details_shimmer.xml new file mode 100644 index 0000000..3a8a948 --- /dev/null +++ b/app/src/main/res/layout/item_integral_details_shimmer.xml @@ -0,0 +1,39 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_integral_shimmer.xml b/app/src/main/res/layout/item_integral_shimmer.xml new file mode 100644 index 0000000..b37da62 --- /dev/null +++ b/app/src/main/res/layout/item_integral_shimmer.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_projects.xml b/app/src/main/res/layout/item_projects.xml new file mode 100644 index 0000000..6876369 --- /dev/null +++ b/app/src/main/res/layout/item_projects.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_projects_shimmer.xml b/app/src/main/res/layout/item_projects_shimmer.xml new file mode 100644 index 0000000..a11fa68 --- /dev/null +++ b/app/src/main/res/layout/item_projects_shimmer.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_tree.xml b/app/src/main/res/layout/item_tree.xml new file mode 100644 index 0000000..f666f1e --- /dev/null +++ b/app/src/main/res/layout/item_tree.xml @@ -0,0 +1,38 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_tree_in.xml b/app/src/main/res/layout/item_tree_in.xml new file mode 100644 index 0000000..37c0f53 --- /dev/null +++ b/app/src/main/res/layout/item_tree_in.xml @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_tree_shimmer.xml b/app/src/main/res/layout/item_tree_shimmer.xml new file mode 100644 index 0000000..ed431e0 --- /dev/null +++ b/app/src/main/res/layout/item_tree_shimmer.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_loadsir_empty.xml b/app/src/main/res/layout/layout_loadsir_empty.xml new file mode 100644 index 0000000..81a2a10 --- /dev/null +++ b/app/src/main/res/layout/layout_loadsir_empty.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_loadsir_failed.xml b/app/src/main/res/layout/layout_loadsir_failed.xml new file mode 100644 index 0000000..ffc6ef0 --- /dev/null +++ b/app/src/main/res/layout/layout_loadsir_failed.xml @@ -0,0 +1,37 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_loadsir_loading.xml b/app/src/main/res/layout/layout_loadsir_loading.xml new file mode 100644 index 0000000..7eb6cac --- /dev/null +++ b/app/src/main/res/layout/layout_loadsir_loading.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_recycler_view.xml b/app/src/main/res/layout/layout_recycler_view.xml new file mode 100644 index 0000000..72a92a3 --- /dev/null +++ b/app/src/main/res/layout/layout_recycler_view.xml @@ -0,0 +1,23 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_custom_toolbar.xml b/app/src/main/res/layout/view_custom_toolbar.xml new file mode 100644 index 0000000..93b0308 --- /dev/null +++ b/app/src/main/res/layout/view_custom_toolbar.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/app/src/main/res/layout/view_toolbar.xml b/app/src/main/res/layout/view_toolbar.xml new file mode 100644 index 0000000..463dce4 --- /dev/null +++ b/app/src/main/res/layout/view_toolbar.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/layout/view_toolbar_trans.xml b/app/src/main/res/layout/view_toolbar_trans.xml new file mode 100644 index 0000000..10015cc --- /dev/null +++ b/app/src/main/res/layout/view_toolbar_trans.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/layout/view_toolbar_white.xml b/app/src/main/res/layout/view_toolbar_white.xml new file mode 100644 index 0000000..663b55f --- /dev/null +++ b/app/src/main/res/layout/view_toolbar_white.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..2ac6246 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_collect.png b/app/src/main/res/mipmap-xxhdpi/ic_collect.png new file mode 100644 index 0000000..35d9758 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_collect.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_default_avatar.png b/app/src/main/res/mipmap-xxhdpi/ic_default_avatar.png new file mode 100644 index 0000000..d6f63b7 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_default_avatar.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_github.png b/app/src/main/res/mipmap-xxhdpi/ic_github.png new file mode 100644 index 0000000..b25181b Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_github.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_img_default.jpg b/app/src/main/res/mipmap-xxhdpi/ic_img_default.jpg new file mode 100644 index 0000000..a81cab3 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_img_default.jpg differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_integral.png b/app/src/main/res/mipmap-xxhdpi/ic_integral.png new file mode 100644 index 0000000..d67a63a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_integral.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..a3fd90d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_logo.png b/app/src/main/res/mipmap-xxhdpi/ic_logo.png new file mode 100644 index 0000000..cb203f4 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_logo.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_rank_list.png b/app/src/main/res/mipmap-xxhdpi/ic_rank_list.png new file mode 100644 index 0000000..4470177 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_rank_list.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_rank_no1.png b/app/src/main/res/mipmap-xxhdpi/ic_rank_no1.png new file mode 100644 index 0000000..58139dc Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_rank_no1.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_rank_no2.png b/app/src/main/res/mipmap-xxhdpi/ic_rank_no2.png new file mode 100644 index 0000000..3a5cbae Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_rank_no2.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_rank_no3.png b/app/src/main/res/mipmap-xxhdpi/ic_rank_no3.png new file mode 100644 index 0000000..11f30c9 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_rank_no3.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_reg_arrow.png b/app/src/main/res/mipmap-xxhdpi/ic_reg_arrow.png new file mode 100644 index 0000000..a174e81 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_reg_arrow.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_right_arrow.png b/app/src/main/res/mipmap-xxhdpi/ic_right_arrow.png new file mode 100644 index 0000000..61f3df5 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_right_arrow.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/icon_back.png b/app/src/main/res/mipmap-xxhdpi/icon_back.png new file mode 100644 index 0000000..0e26468 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/icon_back.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/nav_icon_discovery.png b/app/src/main/res/mipmap-xxhdpi/nav_icon_discovery.png new file mode 100644 index 0000000..3e9b87c Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/nav_icon_discovery.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/nav_icon_home.png b/app/src/main/res/mipmap-xxhdpi/nav_icon_home.png new file mode 100644 index 0000000..89a68fa Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/nav_icon_home.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/nav_icon_mine.png b/app/src/main/res/mipmap-xxhdpi/nav_icon_mine.png new file mode 100644 index 0000000..fdc0ba3 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/nav_icon_mine.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/nav_icon_project.png b/app/src/main/res/mipmap-xxhdpi/nav_icon_project.png new file mode 100644 index 0000000..8c35db0 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/nav_icon_project.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/nav_icon_public.png b/app/src/main/res/mipmap-xxhdpi/nav_icon_public.png new file mode 100644 index 0000000..15178cc Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/nav_icon_public.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..402352f Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/navigation/nav_login.xml b/app/src/main/res/navigation/nav_login.xml new file mode 100644 index 0000000..1d44570 --- /dev/null +++ b/app/src/main/res/navigation/nav_login.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..0298831 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,36 @@ + + + #E57815 + #BA7815 + #F4B02F + #F3F6FF + + #F3F3F7 + + #FFFFFF + #000000 + #E96D6B + #66DFAA + #D7DBDC + #00000000 + #36000000 + #55000000 + #99000000 + + #EED7AD + #272727 + #6D7E82 + #999999 + #55333333 + #E6E6E6 + #BEBEBE + #CDCDCD + #EEEEEE + #DFDFDF + + #D4D4D4 + #F5F5F5 + #2a000000 + #0F000000 + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..a1efa86 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,20 @@ + + + 18sp + 16sp + 15sp + 14sp + 12sp + 10sp + + 5dp + 4dp + + 5dp + + 45dp + + 40dp + 5dp + 15dp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..59d46f7 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,11 @@ + + WanAndroid + 玩安卓 + + 首页 + 项目 + 发现 + 公众号 + 我的 + 请登录 + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..23f25c4 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 2 + 3 + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..b5b765b --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,25 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..dca93c0 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/test/java/com/holo/wanandroid/ExampleUnitTest.kt b/app/src/test/java/com/holo/wanandroid/ExampleUnitTest.kt new file mode 100644 index 0000000..ebe9ed3 --- /dev/null +++ b/app/src/test/java/com/holo/wanandroid/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.holo.wanandroid + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..c5d9c82 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,38 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + google() + mavenCentral() + maven ("https://jitpack.io") + //阿里云jcenter仓库 + maven("https://maven.aliyun.com/repository/jcenter") + maven("https://maven.aliyun.com/repository/public") + maven("https://s01.oss.sonatype.org/content/groups/public") + } + dependencies { + classpath(ModulePlugin.android) + classpath(ModulePlugin.kotlin) + classpath(ModulePlugin.hilt) + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10") + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle.kts.kts files + } +} + +allprojects { + repositories { + google() + mavenCentral() + maven ("https://jitpack.io") + //阿里云jcenter仓库 + maven("https://maven.aliyun.com/repository/jcenter") + maven("https://maven.aliyun.com/repository/public") + maven("https://s01.oss.sonatype.org/content/groups/public") + } +} + +tasks.register("clean", Delete::class) { + delete(rootProject.buildDir) +} \ No newline at end of file diff --git a/buildSrc/.gitignore b/buildSrc/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/buildSrc/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..5bd96b5 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,9 @@ +//import org.gradle.kotlin.dsl.`kotlin-dsl` +plugins { + `kotlin-dsl` +} + +repositories { + google() + mavenCentral() +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/AndroidX.kt b/buildSrc/src/main/kotlin/AndroidX.kt new file mode 100644 index 0000000..95f2fc6 --- /dev/null +++ b/buildSrc/src/main/kotlin/AndroidX.kt @@ -0,0 +1,100 @@ +/** + * androidx 相关依赖库 + * + * @Author holo + * @Date 2022/2/14 + */ +object Coroutines { + const val core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2" + const val android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2" +} + +object Android { + /** + * appcompat中默认引入了很多库(比如activity库、fragment库、core库、annotation库、drawerlayout库、appcompat-resources) + * 如果想使用其中某个库的更新版本,可以单独引用,比如下面的vectordrawable + * 提示:对于声明式依赖,同一个库的不同版本,gradle会自动使用最新版本来进行依赖替换、编译 + */ + const val appcompat = "androidx.appcompat:appcompat:1.4.1" + + const val recyclerView = "androidx.recyclerview:recyclerview:1.2.1" + + // startup + const val startup = "androidx.startup:startup-runtime:1.1.0" + + // activity+ktx扩展函数 + const val activityKtx = "androidx.activity:activity-ktx:1.4.0" + + // fragment+ktx扩展函数 + const val fragmentKtx = "androidx.fragment:fragment-ktx:1.4.0" + + // core包+ktx扩展函数 + const val coreKtx = "androidx.core:core-ktx:1.7.0" + + // 约束布局 + const val constraintLayout = "androidx.constraintlayout:constraintlayout:2.1.2" + + // swipeRefreshLayout + const val swipeRefreshLayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01" + + //material包 + const val material = "com.google.android.material:material:1.5.0-alpha01" + + const val webkit = "androidx.webkit:webkit:1.4.0" + + const val flexBox = "com.google.android.flexbox:flexbox:3.0.0" +} + +object ViewPager { + //viewpager + const val viewpager = "androidx.viewpager:viewpager:1.0.0" + + //viewpager2 + const val viewpager2 = "androidx.viewpager2:viewpager2:1.1.0-beta01" +} + +object Room { + private const val version = "2.4.0" + const val compiler = "androidx.room:room-compiler:${version}" + const val ktx = "androidx.room:room-ktx:${version}" + const val runtime = "androidx.room:room-runtime:${version}" +} + +object Hilt { + private const val version = "2.40.5" + const val hiltAndroid = "com.google.dagger:hilt-android:${version}" + const val daggerCompiler = "com.google.dagger:hilt-android-compiler:${version}" + + private const val xversion = "1.0.0-alpha03" + const val hiltCompiler = "androidx.hilt:hilt-compiler:${xversion}" + const val hiltViewModel = "androidx.hilt:hilt-lifecycle-viewmodel:${xversion}" +} + +object Lifecycle { + private const val version = "2.4.0" + const val viewModel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${version}" + const val liveData = "androidx.lifecycle:lifecycle-livedata-ktx:${version}" + const val runtimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:${version}" +} + +object Navigation { + //这个版本支持多返回栈了 + private const val version = "2.4.1" + + const val fragmentKtx = "androidx.navigation:navigation-fragment-ktx:$version" + const val uiKtx = "androidx.navigation:navigation-ui-ktx:$version" +} + +object Testing { + const val jUnit = "junit:junit:4.13.2" + const val extJUnit = "androidx.test.ext:junit:1.1.3" + const val testRunner = "androidx.test:runner:1.4.0" + const val espresso = "androidx.test.espresso:espresso-core:3.4.0" + + const val coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.1" + const val room = "androidx.room:room-testing:2.2.6" + const val okHttp = "com.squareup.okhttp3:mockwebserver:4.9.0" + const val core = "androidx.arch.core:core-testing:2.1.0" + const val truth = "com.google.truth:truth:1.1.3" +} + diff --git a/buildSrc/src/main/kotlin/Depends.kt b/buildSrc/src/main/kotlin/Depends.kt new file mode 100644 index 0000000..0d1a8fa --- /dev/null +++ b/buildSrc/src/main/kotlin/Depends.kt @@ -0,0 +1,114 @@ +/** + * + * + * @Author holo + * @Date 2022/2/14 + */ + +object Moshi { + const val moshi = "com.squareup.moshi:moshi-kotlin:1.13.0" + const val codeGen = "com.squareup.moshi:moshi-kotlin-codegen:1.13.0" +} + +object Retrofit { + //网路请求库retrofit + private const val version = "2.9.0" + const val retrofit = "com.squareup.retrofit2:retrofit:${version}" + + //gson序列化转换 + const val convertGson = "com.squareup.retrofit2:converter-gson:${version}" + + //moshi序列化转换 + const val convertMoshi = "com.squareup.retrofit2:converter-moshi:${version}" + + //日志拦截打印 + const val loggingInterceptor = "com.squareup.okhttp3:logging-interceptor:4.9.3" +} + +object Depends { + //gson解析 + const val gson = "com.google.code.gson:gson:2.8.7" + + // Kotlin 协程方式的加载图片库 https://github.com/coil-kt/coil/ + const val coil = "io.coil-kt:coil:2.0.0-rc01" + + // 腾讯内存映射组件,替代SP https://github.com/Tencent/MMKV/blob/master/README_CN.md + const val mmkv = "com.tencent:mmkv:1.2.11" + + // android 4.4以上沉浸式实现 https://github.com/gyf-dev/ImmersionBar + const val immersionBar = "com.geyifeng.immersionbar:immersionbar:3.2.0" + const val immersionBarKtx = "com.geyifeng.immersionbar:immersionbar-ktx:3.2.0" + + // UnPeekLiveData-解决数据倒灌困扰 https://github.com/KunMinX/UnPeek-LiveData + const val unPeekLiveData = "com.kunminx.arch:unpeek-livedata:7.2.0-beta1" + + //lottie用来加载json动画 + // 你可以使用转换工具将mp4转换成json【https://isotropic.co/video-to-lottie/】 + // 去这里下载素材【https://lottiefiles.com/】 + const val lottie = "com.airbnb.android:lottie:4.2.2" + + const val shadowLayout = "com.github.lihangleo2:ShadowLayout:3.2.4" + + // Android WebView ,极度容易使用以及功能强大的库 https://github.com/Justson/AgentWeb + const val agentWeb = "com.github.Justson.AgentWeb:agentweb-core:v5.0.0-alpha.1-androidx" + + // BRVAH 强大而灵活的RecyclerView Adapter https://github.com/CymChad/BaseRecyclerViewAdapterHelper + const val BRVAH = "com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.7" + + // 加载反馈页管理框架 https://github.com/KingJA/LoadSir + const val loadSir = "com.kingja.loadsir:loadsir:1.3.8" + + // FaceBook shimmer闪烁动画库 https://github.com/facebook/shimmer-android + const val shimmer = "com.facebook.shimmer:shimmer:0.5.0" + + // 指示器框架 https://github.com/hackware1993/MagicIndicator + const val magicIndicator = "com.github.hackware1993:MagicIndicator:1.7.0" + + // 基于ViewPager2的Banner控件 https://github.com/youth5201314/banner + const val banner = "io.github.youth5201314:banner:2.2.2" + + // 权限申请库 https://github.com/guolindev/PermissionX + const val permissionX = "com.guolindev.permissionx:permissionx:1.6.1" + + // 点赞View https://github.com/qkxyjren/LikeView + const val likeView = "com.jaren:likeview:1.2.2" + + // 缺省页管理 https://github.com/HoloXia/LoadState + const val loadState = "com.github.HoloXia:LoadState:1.0" + + // Cookie持久化 https://github.com/franmontiel/PersistentCookieJar + const val persistentCookieJar = "com.github.franmontiel:PersistentCookieJar:v1.0.1" +} + +object SmartRefreshLayout { + // 智能下拉刷新框架 https://github.com/scwang90/SmartRefreshLayout + const val core = "io.github.scwang90:refresh-layout-kernel:2.0.5" + const val headerMaterial = "io.github.scwang90:refresh-header-material:2.0.5" + const val footerClassics = "io.github.scwang90:refresh-footer-classics:2.0.5" +} + +object DKVideoPlayer { + // 视频播放器 https://github.com/Doikki/DKVideoPlayer + private const val version = "3.3.5" + const val core = "xyz.doikki.android.dkplayer:dkplayer-java:$version" + const val ui = "xyz.doikki.android.dkplayer:dkplayer-ui:$version" + const val exo = "xyz.doikki.android.dkplayer:player-exo:$version" + const val ijk = "xyz.doikki.android.dkplayer:player-ijk:$version" + const val cache = "xyz.doikki.android.dkplayer:videocache:$version" +} + +/** + * 插入即用的dialog + */ +object MaterialDialogs { + // 项目地址 https://github.com/afollestad/material-dialogs + + private const val version = "3.3.0" + const val core = "com.afollestad.material-dialogs:core:$version" + const val input = "com.afollestad.material-dialogs:input:$version" + const val color = "com.afollestad.material-dialogs:color:$version" + const val files = "com.afollestad.material-dialogs:files:$version" + const val datetime = "com.afollestad.material-dialogs:datetime:$version" + const val bottomSheets = "com.afollestad.material-dialogs:bottomsheets:$version" + const val lifecycle = "com.afollestad.material-dialogs:lifecycle:$version" +} diff --git a/buildSrc/src/main/kotlin/ModuleConfig.kt b/buildSrc/src/main/kotlin/ModuleConfig.kt new file mode 100644 index 0000000..e09de6a --- /dev/null +++ b/buildSrc/src/main/kotlin/ModuleConfig.kt @@ -0,0 +1,15 @@ +/** + * @Desc 编译配置信息 + * + * @Author holo + * @Date 2022/1/11 + */ +object ModuleConfig { + const val compileSdkVersion = 31 + const val buildToolsVersion = "30.0.3" + const val minSdkVersion = 23 + const val targetSdkVersion = 31 + + const val versionCode = 1 + const val versionName = "1.0" +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/ModulePlugin.kt b/buildSrc/src/main/kotlin/ModulePlugin.kt new file mode 100644 index 0000000..dca0eaa --- /dev/null +++ b/buildSrc/src/main/kotlin/ModulePlugin.kt @@ -0,0 +1,16 @@ +/** + * Gradle插件库 + * + * @Author holo + * @Date 2022/2/14 + */ +object ModulePlugin { + private const val gradle_version = "7.0.4" + const val android = "com.android.tools.build:gradle:$gradle_version" + + private const val kotlin_version = "1.6.10" + const val kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + + private const val hilt_version = "2.40.5" + const val hilt = "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..ad372da --- /dev/null +++ b/gradle.properties @@ -0,0 +1,25 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..78a613f --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Apr 15 12:05:42 CST 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..5ad9a85 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,3 @@ +rootProject.name = "WanAndroid" +include(":app") +include(":BaseMvvm")