diff --git a/.github/workflows/autopublish.yml b/.github/workflows/autopublish.yml index d93ddaf9..f7bf8590 100644 --- a/.github/workflows/autopublish.yml +++ b/.github/workflows/autopublish.yml @@ -8,6 +8,15 @@ jobs: - uses: actions/setup-java@v1 with: java-version: 1.8 + - name: Fix android 31.0.0 dx + continue-on-error: true + run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar + - name: Rewrite version + run: | + branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" + cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp + rm gradle.properties + mv gradle.properties.tmp gradle.properties - name: prebuild run: ./gradlew clean build - name: Publish package @@ -15,4 +24,3 @@ jobs: env: GITHUBPACKAGES_USER: ${{ secrets.GITHUBPACKAGES_USER }} GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - additional_version: "-build${{ github.run_number }}" diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 261eeb9e..00000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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 - - http://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. diff --git a/README.md b/README.md new file mode 100644 index 00000000..a632cd67 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +## Структура проекта + +* Features - набор **законченных** фич проекта. Считается, что любая фича, находящаяся в мастере может быть добавлена в + клиент и использована в нем. Исключением является `common` - это набор вещей, используемых везде. В подпунктах представлены + части, на которые *обычно* разделяется фича + * Common - общая для фичи часть. Тут, как правило, хранятся конвенции путей для сетевых соединений, общие типы и пр. + * Server - часть, включаемая в сервер для подключения фичи. Обычно содержит работу с бд, определение модулей сервера и пр. + * Client - часть с клиентским кодом. В большинстве своём включает работу с сервером, MVVM часть (View при этом должны + находиться в платформенной части, если их нельзя вынести в сommon часть клиента) +* Client - итоговый клиент. На момент написания этой доки (`Пн окт 25 12:56:41 +06 2021`) предполагается два варианта: + * Мультиплатформенный проект со сборкой каждого таргета. Скорее всего, не будет использован в силу сложности настройки + части клиентов (например, андроид) + * Мультимодульный проект +* Server - пока что JVM-only модуль, включающий все необходимые для сервера фичи diff --git a/build.gradle b/build.gradle index 07759c6e..c334c733 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,5 @@ buildscript { repositories { - jcenter() google() mavenCentral() mavenLocal() @@ -8,11 +7,10 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.0.2' + classpath 'com.android.tools.build:gradle:4.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version" - classpath "com.github.breadmoirai:github-release:$github_release_plugin_version" classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" } } @@ -20,12 +18,10 @@ buildscript { allprojects { repositories { mavenLocal() - jcenter() mavenCentral() google() - maven { url "https://kotlin.bintray.com/kotlinx" } } } apply from: "./extensions.gradle" -apply from: "./github_release.gradle" +// apply from: "./github_release.gradle" diff --git a/business_cases/post_creating/client/build.gradle b/business_cases/post_creating/client/build.gradle deleted file mode 100644 index fbeae368..00000000 --- a/business_cases/post_creating/client/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id "org.jetbrains.kotlin.multiplatform" - id "org.jetbrains.kotlin.plugin.serialization" - id "com.android.library" -} - -apply from: "$mppProjectWithSerializationPresetPath" - -kotlin { - sourceSets { - commonMain { - dependencies { - implementation kotlin('stdlib') - - api project(":postssystem.business_cases.post_creating.common") - api project(":postssystem.core.ktor.client") - } - } - } -} diff --git a/business_cases/post_creating/client/src/commonMain/kotlin/dev/inmo/postssystem/business_cases/post_creating/client/PostCreatingClientCase.kt b/business_cases/post_creating/client/src/commonMain/kotlin/dev/inmo/postssystem/business_cases/post_creating/client/PostCreatingClientCase.kt deleted file mode 100644 index e76d5950..00000000 --- a/business_cases/post_creating/client/src/commonMain/kotlin/dev/inmo/postssystem/business_cases/post_creating/client/PostCreatingClientCase.kt +++ /dev/null @@ -1,26 +0,0 @@ -package dev.inmo.postssystem.business_cases.post_creating.client - -import dev.inmo.micro_utils.ktor.client.* -import dev.inmo.postssystem.business_cases.post_creating.server.* -import dev.inmo.postssystem.core.content.Content -import dev.inmo.postssystem.core.post.RegisteredPost -import dev.inmo.postssystem.core.publishing.TriggerId -import dev.inmo.micro_utils.ktor.common.buildStandardUrl -import io.ktor.client.HttpClient -import kotlinx.serialization.builtins.nullable - -class PostCreatingClientCase( - private val baseUrl: String, - private val unifiedRequester: UnifiedRequester, - private val rootRoute: String? = postCreatingRootRoute -) : PostCreatingCase { - private val realBaseUrl = rootRoute ?.let { "$baseUrl/$rootRoute" } ?: baseUrl - override suspend fun createPost( - postContent: List, - triggerId: TriggerId? - ): RegisteredPost? = unifiedRequester.unipost( - buildStandardUrl(realBaseUrl, postCreatingCreatePostRoute), - BodyPair(PostCreatingCreatePostModel.serializer(), PostCreatingCreatePostModel(postContent, triggerId)), - RegisteredPost.serializer().nullable - ) -} diff --git a/business_cases/post_creating/client/src/main/AndroidManifest.xml b/business_cases/post_creating/client/src/main/AndroidManifest.xml deleted file mode 100644 index ead9e0fd..00000000 --- a/business_cases/post_creating/client/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/business_cases/post_creating/common/src/commonMain/kotlin/dev/inmo/postssystem/business_cases/post_creating/server/BusinessPostCreatingCase.kt b/business_cases/post_creating/common/src/commonMain/kotlin/dev/inmo/postssystem/business_cases/post_creating/server/BusinessPostCreatingCase.kt deleted file mode 100644 index e993efaa..00000000 --- a/business_cases/post_creating/common/src/commonMain/kotlin/dev/inmo/postssystem/business_cases/post_creating/server/BusinessPostCreatingCase.kt +++ /dev/null @@ -1,26 +0,0 @@ -package dev.inmo.postssystem.business_cases.post_creating.server - -import dev.inmo.micro_utils.repos.set -import dev.inmo.postssystem.core.content.Content -import dev.inmo.postssystem.core.content.api.ContentRepo -import dev.inmo.postssystem.core.post.* -import dev.inmo.postssystem.core.post.repo.PostsRepo -import dev.inmo.postssystem.core.publishing.* -import dev.inmo.postssystem.core.publishing.repos.WriteTriggersToPostsRepo - -class BusinessPostCreatingCase( - private val postsRepo: PostsRepo, - private val contentRepo: ContentRepo, - private val postsTriggersToPostsRepo: WriteTriggersToPostsRepo -) : PostCreatingCase { - override suspend fun createPost(postContent: List, triggerId: TriggerId?): RegisteredPost? { - val content = contentRepo.create(postContent) - val post = postsRepo.createPost(SimplePost(content.map { it.id })) ?: return null - - triggerId ?.let { - postsTriggersToPostsRepo.set(post.id, triggerId) - } - - return post - } -} diff --git a/business_cases/post_creating/common/src/commonMain/kotlin/dev/inmo/postssystem/business_cases/post_creating/server/PostCreatingCase.kt b/business_cases/post_creating/common/src/commonMain/kotlin/dev/inmo/postssystem/business_cases/post_creating/server/PostCreatingCase.kt deleted file mode 100644 index 2d2a231b..00000000 --- a/business_cases/post_creating/common/src/commonMain/kotlin/dev/inmo/postssystem/business_cases/post_creating/server/PostCreatingCase.kt +++ /dev/null @@ -1,19 +0,0 @@ -package dev.inmo.postssystem.business_cases.post_creating.server - -import dev.inmo.postssystem.core.content.Content -import dev.inmo.postssystem.core.post.RegisteredPost -import dev.inmo.postssystem.core.publishing.TriggerId -import kotlinx.serialization.Serializable - -@Serializable -data class PostCreatingCreatePostModel( - val postContent: List, - val triggerId: TriggerId? -) - -interface PostCreatingCase { - suspend fun createPost( - postContent: List, - triggerId: TriggerId? = null - ): RegisteredPost? -} \ No newline at end of file diff --git a/business_cases/post_creating/common/src/commonMain/kotlin/dev/inmo/postssystem/business_cases/post_creating/server/Routings.kt b/business_cases/post_creating/common/src/commonMain/kotlin/dev/inmo/postssystem/business_cases/post_creating/server/Routings.kt deleted file mode 100644 index f8831f72..00000000 --- a/business_cases/post_creating/common/src/commonMain/kotlin/dev/inmo/postssystem/business_cases/post_creating/server/Routings.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.inmo.postssystem.business_cases.post_creating.server - -const val postCreatingRootRoute = "postCreating" - -const val postCreatingCreatePostRoute = "createPost" diff --git a/business_cases/post_creating/common/src/main/AndroidManifest.xml b/business_cases/post_creating/common/src/main/AndroidManifest.xml deleted file mode 100644 index 42d81edc..00000000 --- a/business_cases/post_creating/common/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/business_cases/post_creating/server/build.gradle b/business_cases/post_creating/server/build.gradle deleted file mode 100644 index c8a26a1c..00000000 --- a/business_cases/post_creating/server/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -plugins { - id "org.jetbrains.kotlin.multiplatform" - id "org.jetbrains.kotlin.plugin.serialization" -} - -apply from: "$mppJavaProjectPresetPath" - -kotlin { - sourceSets { - commonMain { - dependencies { - api project(":postssystem.business_cases.post_creating.common") - api project(":postssystem.core.ktor.server") - } - } - jvmTest { - dependencies { - implementation "org.xerial:sqlite-jdbc:$test_sqlite_version" - implementation "org.jetbrains.kotlin:kotlin-test" - implementation "org.jetbrains.kotlin:kotlin-test-junit" - } - } - } -} diff --git a/business_cases/post_creating/server/src/jvmMain/kotlin/dev/inmo/postssystem/business_cases/post_creating/server/PostCreatingRoutingCase.kt b/business_cases/post_creating/server/src/jvmMain/kotlin/dev/inmo/postssystem/business_cases/post_creating/server/PostCreatingRoutingCase.kt deleted file mode 100644 index d952671e..00000000 --- a/business_cases/post_creating/server/src/jvmMain/kotlin/dev/inmo/postssystem/business_cases/post_creating/server/PostCreatingRoutingCase.kt +++ /dev/null @@ -1,33 +0,0 @@ -package dev.inmo.postssystem.business_cases.post_creating.server - -import dev.inmo.micro_utils.ktor.server.* -import dev.inmo.postssystem.core.post.RegisteredPost -import io.ktor.application.call -import io.ktor.routing.* -import kotlinx.serialization.builtins.nullable - -private inline fun Route.configurePostCreatingRoutes( - origin: PostCreatingCase, - unifiedRouter: UnifiedRouter -) { - post(postCreatingCreatePostRoute) { - unifiedRouter.apply { - val model = uniload(PostCreatingCreatePostModel.serializer()) - - unianswer( - RegisteredPost.serializer().nullable, - origin.createPost(model.postContent, model.triggerId) - ) - } - } -} - -fun Route.configurePostCreatingRoutes( - origin: PostCreatingCase, - unifiedRouter: UnifiedRouter, - subroute: String? = postCreatingRootRoute -) { - subroute ?.also { - route(subroute) { configurePostCreatingRoutes(origin, unifiedRouter) } - } ?: configurePostCreatingRoutes(origin, unifiedRouter) -} diff --git a/client/build.gradle b/client/build.gradle new file mode 100644 index 00000000..b6313f8d --- /dev/null +++ b/client/build.gradle @@ -0,0 +1,41 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppProjectWithSerializationPresetPath" + +kotlin { + js(IR) { + binaries.executable() + } + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.common.client") + api project(":postssystem.features.status.client") + api project(":postssystem.features.files.client") + api project(":postssystem.features.users.client") + api project(":postssystem.features.auth.client") + api project(":postssystem.features.roles.client") + api project(":postssystem.features.roles.manager.client") + api "dev.inmo:micro_utils.fsm.common:$microutils_version" + api "dev.inmo:micro_utils.fsm.repos.common:$microutils_version" + api "dev.inmo:micro_utils.crypto:$microutils_version" + } + } + + jvmMain { + dependencies { + api "io.ktor:ktor-client-apache:$ktor_version" + } + } + + jsMain { + dependencies { + implementation "org.jetbrains.kotlinx:kotlinx-html:$kotlinx_html_version" + } + } + } +} diff --git a/client/src/commonMain/kotlin/dev/inmo/postssystem/client/DBDropper.kt b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/DBDropper.kt new file mode 100644 index 00000000..66496163 --- /dev/null +++ b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/DBDropper.kt @@ -0,0 +1,12 @@ +package dev.inmo.postssystem.client + +import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging +import dev.inmo.micro_utils.repos.KeyValueRepo + +class DBDropper( + private val repo: KeyValueRepo +) { + suspend operator fun invoke() { + repo.unset(repo.getAllByWithNextPaging { keys(it) }) + } +} diff --git a/client/src/commonMain/kotlin/dev/inmo/postssystem/client/DI.kt b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/DI.kt new file mode 100644 index 00000000..88094e35 --- /dev/null +++ b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/DI.kt @@ -0,0 +1,105 @@ +package dev.inmo.postssystem.client + +import dev.inmo.postssystem.client.ui.fsm.* +import dev.inmo.postssystem.features.auth.client.installClientAuthenticator +import dev.inmo.postssystem.features.auth.common.* +import dev.inmo.postssystem.features.files.client.ClientFilesStorage +import dev.inmo.postssystem.features.files.common.storage.FilesStorage +import dev.inmo.postssystem.features.roles.common.UserRole +import dev.inmo.postssystem.features.roles.common.UsersRolesStorage +import dev.inmo.postssystem.features.roles.client.ClientUsersRolesStorage +import dev.inmo.postssystem.features.roles.manager.common.RolesManagerRoleSerializer +import dev.inmo.postssystem.features.users.client.UsersStorageKtorClient +import dev.inmo.postssystem.features.users.common.ReadUsersStorage +import dev.inmo.postssystem.features.users.common.User +import dev.inmo.micro_utils.common.Either +import dev.inmo.micro_utils.coroutines.LinkedSupervisorScope +import dev.inmo.micro_utils.fsm.common.StatesMachine +import dev.inmo.micro_utils.fsm.common.dsl.FSMBuilder +import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo +import dev.inmo.micro_utils.ktor.client.UnifiedRequester +import dev.inmo.micro_utils.ktor.common.standardKtorSerialFormat +import dev.inmo.micro_utils.repos.KeyValueRepo +import io.ktor.client.HttpClient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.serialization.BinaryFormat +import kotlinx.serialization.StringFormat +import kotlinx.serialization.json.Json +import org.koin.core.Koin +import org.koin.core.context.startKoin +import org.koin.core.module.Module +import org.koin.core.qualifier.* +import org.koin.core.scope.Scope +import org.koin.dsl.module + +val UIScopeQualifier = StringQualifier("CoroutineScopeUI") +val SettingsQualifier = StringQualifier("Settings") +val UserRolesQualifier = StringQualifier("UserRoles") +private val DBDropperQualifier = StringQualifier("DBDropper") +private val FSMHandlersBuilderQualifier = StringQualifier("FSMHandlersBuilder") + +val defaultSerialFormat = Json { + ignoreUnknownKeys = true +} + +/** + * Entrypoint for getting [org.koin.core.Koin] DI for the client + * + * @param repoFactory Factory for creating of [DefaultStatesManagerRepo] for [dev.inmo.postssystem.client.ui.fsm.UIFSM] + */ +fun baseKoin( + defaultScope: CoroutineScope, + settingsFactory: Scope.() -> KeyValueRepo, + repoFactory: Scope.() -> DefaultStatesManagerRepo, + handlersSetter: Pair>.() -> Unit +): Koin = startKoin { + modules( + module { + single { defaultSerialFormat } + single(SettingsQualifier) { settingsFactory() } + single(DBDropperQualifier) { DBDropper(get(SettingsQualifier)) } + single(FSMHandlersBuilderQualifier) { handlersSetter } + single { repoFactory() } + single { defaultScope } + single(UIScopeQualifier) { get().LinkedSupervisorScope(Dispatchers.Main) } + single>(UIFSMQualifier) { UIFSM(get()) { (this@single to this@UIFSM).apply(get( + FSMHandlersBuilderQualifier + )) } } + } + ) +}.koin.apply { + loadModules( + listOf( + module { single { this@apply } } + ) + ) + RolesManagerRoleSerializer // Just to activate it in JS client +} + +fun getAuthorizedFeaturesDIModule( + serverUrl: String, + initialAuthKey: Either, + onAuthKeyUpdated: suspend (AuthTokenInfo) -> Unit, + onUserRetrieved: suspend (User?) -> Unit, + onAuthKeyInvalidated: suspend () -> Unit +): Module { + val serverUrlQualifier = StringQualifier("serverUrl") + val credsQualifier = StringQualifier("creds") + + return module { + single(createdAtStart = true) { + HttpClient { + installClientAuthenticator(serverUrl, get(), get(credsQualifier), onAuthKeyUpdated, onUserRetrieved, onAuthKeyInvalidated) + } + } + single(credsQualifier) { initialAuthKey } + single(serverUrlQualifier) { serverUrl } + single { standardKtorSerialFormat } + single { UnifiedRequester(get(), get()) } + + single { ClientFilesStorage(get(serverUrlQualifier), get(), get()) } + single { UsersStorageKtorClient(get(serverUrlQualifier), get()) } + single> { ClientUsersRolesStorage(get(serverUrlQualifier), get(), UserRole.serializer()) } + } +} diff --git a/client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/DefaultSettings.kt b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/DefaultSettings.kt new file mode 100644 index 00000000..af3afffc --- /dev/null +++ b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/DefaultSettings.kt @@ -0,0 +1,8 @@ +package dev.inmo.postssystem.client.settings + +import dev.inmo.postssystem.client.settings.auth.AuthSettings + + +data class DefaultSettings( + override val authSettings: AuthSettings +) : Settings diff --git a/client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/Settings.kt b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/Settings.kt new file mode 100644 index 00000000..a8b21f08 --- /dev/null +++ b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/Settings.kt @@ -0,0 +1,12 @@ +package dev.inmo.postssystem.client.settings + +import dev.inmo.postssystem.client.settings.auth.AuthSettings +import kotlinx.coroutines.flow.StateFlow +import org.koin.core.module.Module + +interface Settings { + val authSettings: AuthSettings + + val authorizedDIModule: StateFlow + get() = authSettings.authorizedDIModule +} diff --git a/client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/auth/AuthSettings.kt b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/auth/AuthSettings.kt new file mode 100644 index 00000000..01b73d85 --- /dev/null +++ b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/auth/AuthSettings.kt @@ -0,0 +1,18 @@ +package dev.inmo.postssystem.client.settings.auth + +import dev.inmo.postssystem.features.auth.client.ui.AuthUIError +import dev.inmo.postssystem.features.auth.common.AuthCreds +import dev.inmo.postssystem.features.roles.common.UserRole +import dev.inmo.postssystem.features.users.common.User +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.StateFlow +import org.koin.core.module.Module + +interface AuthSettings { + val authorizedDIModule: StateFlow + val user: StateFlow + val userRoles: StateFlow> + val loadingJob: Job + + suspend fun auth(serverUrl: String, creds: AuthCreds): AuthUIError? +} diff --git a/client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/auth/DefaultAuthSettings.kt b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/auth/DefaultAuthSettings.kt new file mode 100644 index 00000000..04c9b8d9 --- /dev/null +++ b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/settings/auth/DefaultAuthSettings.kt @@ -0,0 +1,118 @@ +package dev.inmo.postssystem.client.settings.auth + +import dev.inmo.postssystem.client.DBDropper +import dev.inmo.postssystem.client.getAuthorizedFeaturesDIModule +import dev.inmo.postssystem.features.auth.client.AuthUnavailableException +import dev.inmo.postssystem.features.auth.client.ui.* +import dev.inmo.postssystem.features.auth.common.* +import dev.inmo.postssystem.features.roles.common.UserRole +import dev.inmo.postssystem.features.roles.common.UsersRolesStorage +import dev.inmo.postssystem.features.status.client.StatusFeatureClient +import dev.inmo.postssystem.features.users.common.User +import dev.inmo.micro_utils.common.Either +import dev.inmo.micro_utils.common.either +import dev.inmo.micro_utils.coroutines.plus +import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.micro_utils.repos.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import org.koin.core.Koin +import org.koin.core.module.Module + +data class DefaultAuthSettings( + private val repo: KeyValueRepo, + private val scope: CoroutineScope, + private val koin: Koin, + private val dbDropper: DBDropper +) : AuthSettings { + private val _authorizedDIModule = MutableStateFlow(null) + override val authorizedDIModule: StateFlow = _authorizedDIModule.asStateFlow() + private val _user = MutableStateFlow(null) + override val user: StateFlow = _user.asStateFlow() + private val _userRoles = MutableStateFlow>(emptyList()) + override val userRoles: StateFlow> = _userRoles.asStateFlow() + + private suspend fun getCurrentServerURL() = repo.get(SERVER_URL_FIELD) as? String + private suspend fun getCurrentUsername() = repo.get(USERNAME_FIELD) as? String + private suspend fun getCurrentToken() = repo.get(TOKEN_FIELD) as? AuthTokenInfo + + override val loadingJob: Job = scope.launch { + val serverUrl = getCurrentServerURL() ?: return@launch + val token = getCurrentToken() ?: return@launch + updateModule(serverUrl, token.either()) + } + + val rolesUpdatingJob = (user + authorizedDIModule).subscribeSafelyWithoutExceptions(scope) { + val user = user.value + + if (user == null || authorizedDIModule.value == null) { + _userRoles.value = emptyList() + } else { + _userRoles.value = koin.get>().getRoles(user.id) + } + println(user) + println(userRoles.value) + } + + override suspend fun auth(serverUrl: String, creds: AuthCreds): AuthUIError? { + return runCatching { + if (getCurrentServerURL() != serverUrl || getCurrentUsername() != creds.username.string) { + dbDropper() + } + repo.set(SERVER_URL_FIELD, serverUrl) + repo.set(USERNAME_FIELD, creds.username.string) + repo.unset(TOKEN_FIELD) + updateModule(serverUrl, creds.either()) + }.onFailure { + it.printStackTrace() + }.getOrThrow() + } + + private suspend fun updateModule( + serverUrl: String, + initialAuthKey: Either, + ): AuthUIError? { + val currentModule = authorizedDIModule.value + val newModule = getAuthorizedFeaturesDIModule( + serverUrl, + initialAuthKey, + { + repo.set(TOKEN_FIELD, it) + }, + { + _user.value = it + } + ) { + repo.unset(SERVER_URL_FIELD, USERNAME_FIELD, TOKEN_FIELD) + _authorizedDIModule.value = null + _user.value = null + throw AuthUnavailableException + } + currentModule ?.let { koin.unloadModules(listOf(currentModule)) } + koin.loadModules(listOf(newModule)) + val statusFeature = koin.get() + + val serverAvailable = statusFeature.checkServerStatus() + val authCorrect = serverAvailable && runCatching { + statusFeature.checkServerStatusWithAuth() + }.getOrElse { false } + if (!serverAvailable && !authCorrect) { + koin.unloadModules(listOf(newModule)) + currentModule ?.let { koin.loadModules(listOf(currentModule)) } + } + return when { + !serverAvailable -> ServerUnavailableAuthUIError + !authCorrect -> AuthIncorrectAuthUIError + else -> { + _authorizedDIModule.value = newModule + null + } + } + } + + companion object { + private const val SERVER_URL_FIELD = "AuthServerURL" + private const val USERNAME_FIELD = "AuthUsername" + private const val TOKEN_FIELD = "AuthToken" + } +} diff --git a/client/src/commonMain/kotlin/dev/inmo/postssystem/client/ui/DefaultAuthUIModel.kt b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/ui/DefaultAuthUIModel.kt new file mode 100644 index 00000000..959bb8c5 --- /dev/null +++ b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/ui/DefaultAuthUIModel.kt @@ -0,0 +1,36 @@ +package dev.inmo.postssystem.client.ui + +import dev.inmo.postssystem.client.settings.auth.AuthSettings +import dev.inmo.postssystem.features.auth.client.ui.* +import dev.inmo.postssystem.features.auth.common.AuthCreds +import dev.inmo.postssystem.features.common.common.AbstractUIModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class DefaultAuthUIModel( + private val scope: CoroutineScope, + private val authSettings: AuthSettings +) : AbstractUIModel(LoadingAuthUIState), AuthUIModel { + init { + scope.launch { + _currentState.value = LoadingAuthUIState + authSettings.loadingJob.join() + if (authSettings.authorizedDIModule.value == null) { + _currentState.value = DefaultInitAuthUIState + } else { + _currentState.value = AuthorizedAuthUIState + } + } + } + + override suspend fun initAuth(serverUrl: String, creds: AuthCreds) { + _currentState.value = LoadingAuthUIState + val authError = authSettings.auth(serverUrl, creds) + if (authError == null) { + _currentState.value = AuthorizedAuthUIState + } else { + _currentState.value = InitAuthUIState(authError) + } + } + +} diff --git a/client/src/commonMain/kotlin/dev/inmo/postssystem/client/ui/fsm/UIFSM.kt b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/ui/fsm/UIFSM.kt new file mode 100644 index 00000000..0255095b --- /dev/null +++ b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/ui/fsm/UIFSM.kt @@ -0,0 +1,17 @@ +package dev.inmo.postssystem.client.ui.fsm + +import dev.inmo.micro_utils.fsm.common.dsl.FSMBuilder +import dev.inmo.micro_utils.fsm.common.dsl.buildFSM +import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager +import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo +import org.koin.core.qualifier.StringQualifier + +val UIFSMQualifier = StringQualifier("UIFSM") + +fun UIFSM( + repo: DefaultStatesManagerRepo, + handlersSetter: FSMBuilder.() -> Unit +) = buildFSM { + statesManager = DefaultStatesManager(repo) + handlersSetter() +} diff --git a/client/src/commonMain/kotlin/dev/inmo/postssystem/client/ui/fsm/UIFSMHandler.kt b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/ui/fsm/UIFSMHandler.kt new file mode 100644 index 00000000..c4d287fe --- /dev/null +++ b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/ui/fsm/UIFSMHandler.kt @@ -0,0 +1,27 @@ +package dev.inmo.postssystem.client.ui.fsm + +import dev.inmo.postssystem.features.auth.client.AuthUnavailableException +import dev.inmo.micro_utils.fsm.common.* + +interface UIFSMHandler : StatesHandler { + suspend fun StatesMachine.safeHandleState(state: T): UIFSMState? + override suspend fun StatesMachine.handleState(state: T): UIFSMState? { + return runCatching { + safeHandleState(state).also(::println) + }.getOrElse { + errorToNextStep(state, it) ?.let { return it } ?: throw it + }.also(::println) + } + + suspend fun errorToNextStep( + currentState: T, + e: Throwable + ): UIFSMState? = when (e) { + is AuthUnavailableException -> if (currentState is AuthUIFSMState) { + currentState + } else { + AuthUIFSMState(currentState) + } + else -> null + } +} diff --git a/client/src/commonMain/kotlin/dev/inmo/postssystem/client/ui/fsm/UIFSMState.kt b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/ui/fsm/UIFSMState.kt new file mode 100644 index 00000000..d42c363e --- /dev/null +++ b/client/src/commonMain/kotlin/dev/inmo/postssystem/client/ui/fsm/UIFSMState.kt @@ -0,0 +1,24 @@ +package dev.inmo.postssystem.client.ui.fsm + +import dev.inmo.micro_utils.fsm.common.State +import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer +import kotlinx.serialization.* + +@Serializable(UIFSMStateSerializer::class) +sealed interface UIFSMState : State { + val from: UIFSMState? + get() = null + override val context: String + get() = "main" +} + +object UIFSMStateSerializer : KSerializer by TypedSerializer( + "auth" to AuthUIFSMState.serializer(), +) + +@Serializable +data class AuthUIFSMState( + override val from: UIFSMState? = null, + override val context: String = "main" +) : UIFSMState +val DefaultAuthUIFSMState = AuthUIFSMState() diff --git a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/JSDI.kt b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/JSDI.kt new file mode 100644 index 00000000..e60fcb30 --- /dev/null +++ b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/JSDI.kt @@ -0,0 +1,92 @@ +package dev.inmo.postssystem.client + +import dev.inmo.postssystem.client.fsm.ui.* +import dev.inmo.postssystem.client.ui.* +import dev.inmo.postssystem.client.ui.fsm.* +import dev.inmo.postssystem.client.ui.fsm.UIFSMStateSerializer +import dev.inmo.postssystem.features.auth.client.ui.AuthUIModel +import dev.inmo.postssystem.features.auth.client.ui.AuthUIViewModel +import dev.inmo.postssystem.features.auth.common.AuthTokenInfo +import dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler +import dev.inmo.micro_utils.repos.mappers.withMapper +import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer +import kotlinx.browser.document +import kotlinx.browser.localStorage +import kotlinx.coroutines.* +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.serializer +import org.koin.core.Koin +import org.koin.core.context.loadKoinModules +import org.koin.dsl.module +import org.w3c.dom.HTMLElement + +val defaultTypedSerializer = TypedSerializer( + "AuthTokenInfo" to AuthTokenInfo.serializer(), + "String" to String.serializer(), + "Int" to Int.serializer(), + "Long" to Long.serializer(), + "Short" to Short.serializer(), + "Byte" to Byte.serializer(), + "Float" to Float.serializer(), + "Double" to Double.serializer(), + "UIFSMState" to UIFSMStateSerializer +) + +fun baseKoin(): Koin { + val anyToString: suspend Any.() -> String = { + defaultSerialFormat.encodeToString( + defaultTypedSerializer, + this + ) + } + return baseKoin( + CoroutineScope( + Dispatchers.Default + + ContextSafelyExceptionHandler { it.printStackTrace() } + + CoroutineExceptionHandler { _, it -> it.printStackTrace() } + ), + { + CookiesKeyValueRepo.withMapper( + { this }, + { + runCatching { + anyToString() + }.getOrElse { + if (it is NoSuchElementException) { + val name = this::class.simpleName!! + defaultTypedSerializer.include(name, serializer()) + anyToString() + } else { + throw it + } + } + }, + { this }, + { + defaultSerialFormat.decodeFromString( + defaultTypedSerializer, + this + ) + } + ) + }, + { + OneStateUIFSMStatesRepo(get(), localStorage) + } + ) { + first.apply { + second.apply { + loadKoinModules( + module { + factory { document.getElementById("main") as HTMLElement } + + factory { DefaultAuthUIModel(get(), get()) } + factory { AuthUIViewModel(get()) } + factory { AuthView(get(), get(UIScopeQualifier)) } + } + ) + strictlyOn(get()) + } + } + } +} diff --git a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/Main.kt b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/Main.kt new file mode 100644 index 00000000..afd3c3e2 --- /dev/null +++ b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/Main.kt @@ -0,0 +1,14 @@ +package dev.inmo.postssystem.client + +import dev.inmo.postssystem.client.ui.fsm.UIFSMQualifier +import dev.inmo.postssystem.client.ui.fsm.UIFSMState +import dev.inmo.micro_utils.fsm.common.StatesMachine +import kotlinx.browser.window + +fun main() { + window.addEventListener("load", { + val koin = baseKoin() + val uiStatesMachine = koin.get>(UIFSMQualifier) + uiStatesMachine.start(koin.get()) + }) +} diff --git a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/OneStateUIFSMStatesRepo.kt b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/OneStateUIFSMStatesRepo.kt new file mode 100644 index 00000000..e634902f --- /dev/null +++ b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/OneStateUIFSMStatesRepo.kt @@ -0,0 +1,63 @@ +package dev.inmo.postssystem.client + +import dev.inmo.postssystem.client.ui.fsm.* +import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManagerRepo +import kotlinx.serialization.StringFormat +import org.w3c.dom.* + +class OneStateUIFSMStatesRepo( + private val serialFormat: StringFormat, + private val storage: Storage, + private val initialState: UIFSMState = DefaultAuthUIFSMState +) : DefaultStatesManagerRepo { + private val String.storageKey + get() = "${FSMStateSettingsFieldPrefix}$this" + private val String.UIFSMState + get() = runCatching { + serialFormat.decodeFromString(UIFSMStateSerializer, this) + }.onFailure { it.printStackTrace() }.getOrNull() + + init { + if (states().isEmpty()) { + setState(initialState) + } + } + + private fun setState(state: UIFSMState) { + storage[state.context.storageKey] = serialFormat.encodeToString(UIFSMStateSerializer, state) + } + + override suspend fun getContextState(context: Any): UIFSMState? { + return when (context) { + is String -> storage[context.storageKey] ?.UIFSMState ?: return DefaultAuthUIFSMState + else -> null + } + } + + override suspend fun contains(context: Any): Boolean = when (context) { + is String -> storage.get(context) ?.UIFSMState != null + else -> super.contains(context) + } + + private fun states(): List = storage.iterator().asSequence().mapNotNull { (k, v) -> + if (k.startsWith(FSMStateSettingsFieldPrefix)) { + v.UIFSMState + } else { + null + } + }.toList() + + override suspend fun getStates(): List = states() + + override suspend fun removeState(state: UIFSMState) { + storage.removeItem((state.context as? String) ?.storageKey ?: return) + } + + override suspend fun set(state: UIFSMState) { + setState(state) + } + + companion object { + private const val FSMStateSettingsFieldPrefix = "UIFSMState_" + } +} diff --git a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/Settings.kt b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/Settings.kt new file mode 100644 index 00000000..d5174372 --- /dev/null +++ b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/Settings.kt @@ -0,0 +1,82 @@ +package dev.inmo.postssystem.client + +import dev.inmo.micro_utils.pagination.Pagination +import dev.inmo.micro_utils.pagination.PaginationResult +import dev.inmo.micro_utils.pagination.utils.paginate +import dev.inmo.micro_utils.pagination.utils.reverse +import dev.inmo.micro_utils.repos.KeyValueRepo +import kotlinx.browser.localStorage +import kotlinx.coroutines.flow.* +import org.w3c.dom.get +import org.w3c.dom.set + +object CookiesKeyValueRepo : KeyValueRepo { + private val _onNewValue = MutableSharedFlow>() + private val _onValueRemoved = MutableSharedFlow() + override val onNewValue: Flow> = _onNewValue.asSharedFlow() + override val onValueRemoved: Flow = _onValueRemoved.asSharedFlow() + + override suspend fun contains(key: String): Boolean = localStorage.iterator().asSequence().any { it.first == key } + + override suspend fun count(): Long = localStorage.length.toLong() + + override suspend fun get(k: String): String? = localStorage[k] + + override suspend fun keys( + v: String, + pagination: Pagination, + reversed: Boolean + ): PaginationResult = localStorage.iterator().asSequence().mapNotNull { + if (it.second == v) it.first else null + }.toList().let { + it.paginate( + if (reversed) { + pagination.reverse(it.count()) + } else { + pagination + } + ) + } + + override suspend fun keys( + pagination: Pagination, + reversed: Boolean + ): PaginationResult = localStorage.iterator().asSequence().map { it.first }.toList().let { + it.paginate( + if (reversed) { + pagination.reverse(it.count()) + } else { + pagination + } + ) + } + + override suspend fun values( + pagination: Pagination, + reversed: Boolean + ): PaginationResult = localStorage.iterator().asSequence().map { it.second }.toList().let { + it.paginate( + if (reversed) { + pagination.reverse(it.count()) + } else { + pagination + } + ) + } + + override suspend fun set(toSet: Map) { + toSet.forEach { (k, v) -> + localStorage[k] = v + _onNewValue.emit(k to v) + } + } + + override suspend fun unset(toUnset: List) { + toUnset.forEach { + localStorage[it] ?.let { _ -> + localStorage.removeItem(it) + _onValueRemoved.emit(it) + } + } + } +} diff --git a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/StorageIterator.kt b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/StorageIterator.kt new file mode 100644 index 00000000..d46e8693 --- /dev/null +++ b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/StorageIterator.kt @@ -0,0 +1,19 @@ +package dev.inmo.postssystem.client + +import org.w3c.dom.Storage +import org.w3c.dom.get + +class StorageIterator(private val storage: Storage) : Iterator> { + private var index = 0 + + override fun hasNext(): Boolean = index < storage.length + + override fun next(): Pair { + val k = storage.key(index) ?: error("Key for index $index was not found") + val v = storage[k] ?: error("Key for index $index was not found") + index++ + return k to v + } +} + +operator fun Storage.iterator() = StorageIterator(this) diff --git a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/AuthView.kt b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/AuthView.kt new file mode 100644 index 00000000..71580870 --- /dev/null +++ b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/AuthView.kt @@ -0,0 +1,120 @@ +package dev.inmo.postssystem.client.fsm.ui + +import dev.inmo.postssystem.client.ui.fsm.* +import dev.inmo.postssystem.client.utils.HTMLViewContainer +import dev.inmo.postssystem.features.auth.client.ui.* +import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions +import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.micro_utils.fsm.common.StatesMachine +import kotlinx.browser.document +import kotlinx.coroutines.* +import kotlinx.dom.clear +import kotlinx.html.* +import kotlinx.html.dom.append +import kotlinx.html.js.form +import kotlinx.html.js.onClickFunction +import org.w3c.dom.* + +class AuthView( + private val viewModel: AuthUIViewModel, + private val uiScope: CoroutineScope +) : JSView() { + private val usernameInput + get() = document.getElementById("authUsername") as? HTMLInputElement + private val passwordInput + get() = document.getElementById("authPassword") as? HTMLInputElement + private val authButton + get() = document.getElementById("authButton") + private val errorBadge + get() = document.getElementById("errorBadge") as? HTMLElement + private val progressBarDiv + get() = document.getElementById("progressBar") as? HTMLDivElement + + override suspend fun StatesMachine.safeHandleState( + htmlElement: HTMLElement, + container: HTMLViewContainer, + state: AuthUIFSMState + ): UIFSMState? { + val completion = CompletableDeferred() + htmlElement.clear() + + htmlElement.append { + form(classes = "vertical_container") { + div(classes = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label") { + input(type = InputType.text, classes = "mdl-textfield__input") { + id = "authUsername" + } + label(classes = "mdl-textfield__label") { + +"Имя пользователя" + } + } + div(classes = "mdl-textfield mdl-js-textfield mdl-textfield--floating-label") { + input(type = InputType.password, classes = "mdl-textfield__input") { + id = "authPassword" + } + label(classes = "mdl-textfield__label") { + +"Пароль" + } + } + div(classes = "mdl-progress mdl-js-progress") { + id = "progressBar" + } + span(classes = "material-icons mdl-badge mdl-badge--overlap gone") { + id = "errorBadge" + attributes["data-badge"] = "!" + } + button(classes = "mdl-button mdl-js-button mdl-button--raised") { + +"Авторизоваться" + id = "authButton" + onClickFunction = { + it.preventDefault() + val serverUrl = document.location ?.run { "$hostname:$port" } + val username = usernameInput ?.value + val password = passwordInput ?.value + if (serverUrl != null && username != null && password != null) { + uiScope.launchSafelyWithoutExceptions { viewModel.initAuth(serverUrl, username, password) } + } + } + } + } + } + + val viewJob = viewModel.currentState.subscribeSafelyWithoutExceptions(uiScope) { + when (it) { + is InitAuthUIState -> { + usernameInput ?.removeAttribute("disabled") + passwordInput ?.removeAttribute("disabled") + authButton ?.removeAttribute("disabled") + errorBadge ?.apply { + when (it.showError) { + ServerUnavailableAuthUIError -> { + classList.remove("gone") + innerText = "Сервер недоступен" + } + AuthIncorrectAuthUIError -> { + classList.remove("gone") + innerText = "Данные некорректны" + } + null -> classList.add("gone") + } + } + progressBarDiv ?.classList ?.add("gone") + } + LoadingAuthUIState -> { + usernameInput ?.setAttribute("disabled", "") + passwordInput ?.setAttribute("disabled", "") + authButton ?.setAttribute("disabled", "") + errorBadge ?.classList ?.add("gone") + progressBarDiv ?.classList ?.remove("gone") + } + AuthorizedAuthUIState -> { + htmlElement.clear() + completion.complete(state.from) + } + } + } + return completion.await().also { + viewJob.cancel() + } + } +} diff --git a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/Container.kt b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/Container.kt new file mode 100644 index 00000000..6ccbc6ab --- /dev/null +++ b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/Container.kt @@ -0,0 +1,7 @@ +package dev.inmo.postssystem.client.fsm.ui + +import kotlinx.browser.document +import org.w3c.dom.Element + +val mainContainer: Element + get() = document.getElementById("main")!! diff --git a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/JSView.kt b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/JSView.kt new file mode 100644 index 00000000..390e9ef3 --- /dev/null +++ b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/JSView.kt @@ -0,0 +1,21 @@ +package dev.inmo.postssystem.client.fsm.ui + +import dev.inmo.postssystem.client.ui.fsm.UIFSMHandler +import dev.inmo.postssystem.client.ui.fsm.UIFSMState +import dev.inmo.postssystem.client.utils.HTMLViewContainer +import dev.inmo.micro_utils.fsm.common.StatesMachine +import org.w3c.dom.HTMLElement + +abstract class JSView : UIFSMHandler { + open suspend fun StatesMachine.safeHandleState( + htmlElement: HTMLElement, + container: HTMLViewContainer, + state: T + ): UIFSMState? = null + + override suspend fun StatesMachine.safeHandleState(state: T): UIFSMState? { + return HTMLViewContainer.from(state.context) ?.let { + safeHandleState(it.htmlElement ?: return null, it, state) + } + } +} diff --git a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/defaults/BackButton.kt b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/defaults/BackButton.kt new file mode 100644 index 00000000..0d32700d --- /dev/null +++ b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/fsm/ui/defaults/BackButton.kt @@ -0,0 +1,20 @@ +package dev.inmo.postssystem.client.fsm.ui.defaults + +import dev.inmo.postssystem.client.ui.fsm.UIFSMState +import kotlinx.coroutines.CompletableDeferred +import kotlinx.html.TagConsumer +import kotlinx.html.js.button +import kotlinx.html.js.onClickFunction +import org.w3c.dom.HTMLElement + +fun TagConsumer.addBackButton( + completableDeferred: CompletableDeferred, + stateToBack: UIFSMState +) { + button { + +"Назад" + onClickFunction = { + completableDeferred.complete(stateToBack) + } + } +} diff --git a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/utils/DialogHelper.kt b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/utils/DialogHelper.kt new file mode 100644 index 00000000..0395129e --- /dev/null +++ b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/utils/DialogHelper.kt @@ -0,0 +1,64 @@ +package dev.inmo.postssystem.client.utils + +import com.benasher44.uuid.uuid4 +import kotlinx.browser.document +import kotlinx.html.* +import kotlinx.html.dom.append +import kotlinx.html.js.* +import org.w3c.dom.* + +object DialogHelper { + fun createOneFieldDialog( + title: String, + hint: String, + doneButtonText: String, + closeButtonText: String, + onClose: () -> Unit, + onSubmit: (String) -> Unit + ): HTMLDialogElement { + lateinit var dialogElement: HTMLDialogElement + (document.getElementsByTagName("body").item(0) as? HTMLBodyElement) ?.append { + dialogElement = dialog("mdl-dialog") { + h4("mdl-dialog__title") { + +title + } + val id = "form_${uuid4()}_text" + div(classes = "mdl-dialog__content") { + form("#") { + div("mdl-textfield mdl-js-textfield mdl-textfield--floating-label") { + input(InputType.text, classes = "mdl-textfield__input") { + this.id = id + } + label(classes = "mdl-textfield__label") { + +hint + attributes["for"] = id + } + } + } + } + div(classes = "mdl-dialog__actions mdl-dialog__actions--full-width") { + button(classes = "mdl-button", type = ButtonType.button) { + +doneButtonText + onClickFunction = { + it.preventDefault() + + val input = document.getElementById(id) as? HTMLInputElement + input ?.value ?.let { + onSubmit(it) + } + } + } + button(classes = "mdl-button", type = ButtonType.button) { + +closeButtonText + onClickFunction = { + it.preventDefault() + + onClose() + } + } + } + } + } + return dialogElement + } +} diff --git a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/utils/DownloadFile.kt b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/utils/DownloadFile.kt new file mode 100644 index 00000000..f1a852d5 --- /dev/null +++ b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/utils/DownloadFile.kt @@ -0,0 +1,18 @@ +package dev.inmo.postssystem.client.utils + +import dev.inmo.postssystem.features.files.common.FullFileInfo +import dev.inmo.micro_utils.common.toArrayBuffer +import kotlinx.browser.document +import org.w3c.dom.HTMLAnchorElement +import org.w3c.dom.url.URL +import org.w3c.files.Blob + +fun triggerDownloadFile(fullFileInfo: FullFileInfo) { + val hiddenElement = document.createElement("a") as HTMLAnchorElement + + val url = URL.createObjectURL(Blob(arrayOf(fullFileInfo.byteArrayAllocator().toArrayBuffer()))) + hiddenElement.href = url + hiddenElement.target = "_blank" + hiddenElement.download = fullFileInfo.name.name + hiddenElement.click() +} diff --git a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/utils/HTMLViewContainer.kt b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/utils/HTMLViewContainer.kt new file mode 100644 index 00000000..60ca486b --- /dev/null +++ b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/utils/HTMLViewContainer.kt @@ -0,0 +1,134 @@ +package dev.inmo.postssystem.client.utils + +import kotlinx.browser.document +import kotlinx.dom.clear +import kotlinx.html.dom.append +import kotlinx.html.id +import kotlinx.html.js.* +import org.w3c.dom.HTMLElement +import org.w3c.dom.events.Event + +object HTMLViewsConstants { + val mainContainer: MainHTMLViewContainer = MainHTMLViewContainer + val processesContainer: DrawerHTMLViewContainer = DrawerHTMLViewContainer + val toolsContainerId: ToolsHTMLViewContainer = ToolsHTMLViewContainer +} + +sealed interface HTMLViewContainer { + val htmlElement: HTMLElement? + get() = document.getElementById(id) as? HTMLElement + val id: String + + fun setIsLoading() { + htmlElement ?.apply { + clear() + append { + div("mdl-spinner mdl-js-spinner is-active") + } + } + } + + companion object { + fun from(elementId: String) = when (elementId) { + MainHTMLViewContainer.id -> MainHTMLViewContainer + DrawerHTMLViewContainer.id -> DrawerHTMLViewContainer + ToolsHTMLViewContainer.id -> ToolsHTMLViewContainer + else -> null + } + } +} + +object MainHTMLViewContainer : HTMLViewContainer { + override val id: String + get() = "main" +} + +object DrawerHTMLViewContainer : HTMLViewContainer { + data class DrawerAddButtonInfo( + val text: String, + val onAddButtonClick: (Event) -> Unit, + ) + + override val id: String + get() = "drawer" + + private val titleElement: HTMLElement? + get() = (document.getElementById("drawerTitle") ?:let { + htmlElement ?.append { + span("mdl-layout-title") { + id = "drawerTitle" + } + } ?.first() + }) as? HTMLElement + var title: String? + get() = titleElement ?.textContent + set(value) { + if (value == null) { + titleElement ?.classList ?.add("gone") + } else { + val element = titleElement ?: return + element.textContent = value + element.classList.remove("gone") + } + } + private val contentElement + get() = (document.getElementById("drawerContent") ?:let { + htmlElement ?.append { + nav("mdl-navigation") { + id = "drawerContent" + } + } ?.first() + }) as? HTMLElement + + fun setListContent( + title: String?, + data: Iterable, + getText: (T) -> String?, + addButtonInfo: DrawerAddButtonInfo? = null, + onClick: (T) -> Unit + ) { + this.title = title + val contentElement = contentElement ?: return + + contentElement.clear() + contentElement.append { + fun hideDrawer() { + // Emulate clicking for hiding of drawer + (document.getElementsByClassName("mdl-layout__obfuscator").item(0) as? HTMLElement) ?.click() + } + data.forEach { + val elementTitle = getText(it) ?: return@forEach + div("mdl-navigation__link") { + +elementTitle + onClickFunction = { _ -> + onClick(it) + hideDrawer() + } + } + } + if (addButtonInfo != null) { + button(classes = "mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent") { + +addButtonInfo.text + onClickFunction = { + addButtonInfo.onAddButtonClick(it) + hideDrawer() + } + } + } + } + } + + override fun setIsLoading() { + contentElement ?.apply { + clear() + append { + div("mdl-spinner mdl-js-spinner is-active") + } + } + } +} + +object ToolsHTMLViewContainer : HTMLViewContainer { + override val id: String + get() = "tools" +} diff --git a/client/src/jsMain/kotlin/dev/inmo/postssystem/client/utils/UploadFile.kt b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/utils/UploadFile.kt new file mode 100644 index 00000000..50a8b599 --- /dev/null +++ b/client/src/jsMain/kotlin/dev/inmo/postssystem/client/utils/UploadFile.kt @@ -0,0 +1,41 @@ +package dev.inmo.postssystem.client.utils + +import dev.inmo.postssystem.features.files.common.FullFileInfo +import dev.inmo.micro_utils.common.* +import dev.inmo.micro_utils.mime_types.KnownMimeTypes +import dev.inmo.micro_utils.mime_types.findBuiltinMimeType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import org.khronos.webgl.ArrayBuffer +import org.w3c.dom.HTMLInputElement +import org.w3c.dom.events.Event +import org.w3c.files.FileReader +import org.w3c.files.get + +fun uploadFileCallbackForHTMLInputChange( + output: MutableStateFlow, + scope: CoroutineScope +): (Event) -> Unit = { + (it.target as? HTMLInputElement) ?.apply { + files ?.also { files -> + files[0] ?.also { file -> + scope.launch { + val reader: FileReader = FileReader() + + reader.onload = { + val bytes = ((it.target.asDynamic()).result as ArrayBuffer).toByteArray() + output.value = FullFileInfo( + FileName(file.name), + findBuiltinMimeType(file.type) ?: KnownMimeTypes.Any, + bytes.asAllocator + ) + Unit + } + + reader.readAsArrayBuffer(file) + } + } + } + } +} diff --git a/client/src/jsMain/resources/index.html b/client/src/jsMain/resources/index.html new file mode 100644 index 00000000..bae6fb10 --- /dev/null +++ b/client/src/jsMain/resources/index.html @@ -0,0 +1,34 @@ + + + + + PostsSystem + + + + + + + + + +
+
+
+ + Posts System + +
+ + +
+
+
+
+
+
+ + + + + diff --git a/client/src/jsMain/resources/js/material.min.js b/client/src/jsMain/resources/js/material.min.js new file mode 100644 index 00000000..46524fbc --- /dev/null +++ b/client/src/jsMain/resources/js/material.min.js @@ -0,0 +1,10 @@ +/** + * material-design-lite - Material Design Components in CSS, JS and HTML + * @version v1.3.0 + * @license Apache-2.0 + * @copyright 2015 Google, Inc. + * @link https://github.com/google/material-design-lite + */ +!function(){"use strict";function e(e,t){if(e){if(t.element_.classList.contains(t.CssClasses_.MDL_JS_RIPPLE_EFFECT)){var s=document.createElement("span");s.classList.add(t.CssClasses_.MDL_RIPPLE_CONTAINER),s.classList.add(t.CssClasses_.MDL_JS_RIPPLE_EFFECT);var i=document.createElement("span");i.classList.add(t.CssClasses_.MDL_RIPPLE),s.appendChild(i),e.appendChild(s)}e.addEventListener("click",function(s){if("#"===e.getAttribute("href").charAt(0)){s.preventDefault();var i=e.href.split("#")[1],n=t.element_.querySelector("#"+i);t.resetTabState_(),t.resetPanelState_(),e.classList.add(t.CssClasses_.ACTIVE_CLASS),n.classList.add(t.CssClasses_.ACTIVE_CLASS)}})}}function t(e,t,s,i){function n(){var n=e.href.split("#")[1],a=i.content_.querySelector("#"+n);i.resetTabState_(t),i.resetPanelState_(s),e.classList.add(i.CssClasses_.IS_ACTIVE),a.classList.add(i.CssClasses_.IS_ACTIVE)}if(i.tabBar_.classList.contains(i.CssClasses_.JS_RIPPLE_EFFECT)){var a=document.createElement("span");a.classList.add(i.CssClasses_.RIPPLE_CONTAINER),a.classList.add(i.CssClasses_.JS_RIPPLE_EFFECT);var l=document.createElement("span");l.classList.add(i.CssClasses_.RIPPLE),a.appendChild(l),e.appendChild(a)}i.tabBar_.classList.contains(i.CssClasses_.TAB_MANUAL_SWITCH)||e.addEventListener("click",function(t){"#"===e.getAttribute("href").charAt(0)&&(t.preventDefault(),n())}),e.show=n}var s={upgradeDom:function(e,t){},upgradeElement:function(e,t){},upgradeElements:function(e){},upgradeAllRegistered:function(){},registerUpgradedCallback:function(e,t){},register:function(e){},downgradeElements:function(e){}};s=function(){function e(e,t){for(var s=0;s0&&l(t.children))}function o(t){var s="undefined"==typeof t.widget&&"undefined"==typeof t.widget,i=!0;s||(i=t.widget||t.widget);var n={classConstructor:t.constructor||t.constructor,className:t.classAsString||t.classAsString,cssClass:t.cssClass||t.cssClass,widget:i,callbacks:[]};if(c.forEach(function(e){if(e.cssClass===n.cssClass)throw new Error("The provided cssClass has already been registered: "+e.cssClass);if(e.className===n.className)throw new Error("The provided className has already been registered")}),t.constructor.prototype.hasOwnProperty(C))throw new Error("MDL component classes must not have "+C+" defined as a property.");var a=e(t.classAsString,n);a||c.push(n)}function r(t,s){var i=e(t);i&&i.callbacks.push(s)}function _(){for(var e=0;e0&&this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)&&(e.keyCode===this.Keycodes_.UP_ARROW?(e.preventDefault(),t[t.length-1].focus()):e.keyCode===this.Keycodes_.DOWN_ARROW&&(e.preventDefault(),t[0].focus()))}},d.prototype.handleItemKeyboardEvent_=function(e){if(this.element_&&this.container_){var t=this.element_.querySelectorAll("."+this.CssClasses_.ITEM+":not([disabled])");if(t&&t.length>0&&this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)){var s=Array.prototype.slice.call(t).indexOf(e.target);if(e.keyCode===this.Keycodes_.UP_ARROW)e.preventDefault(),s>0?t[s-1].focus():t[t.length-1].focus();else if(e.keyCode===this.Keycodes_.DOWN_ARROW)e.preventDefault(),t.length>s+1?t[s+1].focus():t[0].focus();else if(e.keyCode===this.Keycodes_.SPACE||e.keyCode===this.Keycodes_.ENTER){e.preventDefault();var i=new MouseEvent("mousedown");e.target.dispatchEvent(i),i=new MouseEvent("mouseup"),e.target.dispatchEvent(i),e.target.click()}else e.keyCode===this.Keycodes_.ESCAPE&&(e.preventDefault(),this.hide())}}},d.prototype.handleItemClick_=function(e){e.target.hasAttribute("disabled")?e.stopPropagation():(this.closing_=!0,window.setTimeout(function(e){this.hide(),this.closing_=!1}.bind(this),this.Constant_.CLOSE_TIMEOUT))},d.prototype.applyClip_=function(e,t){this.element_.classList.contains(this.CssClasses_.UNALIGNED)?this.element_.style.clip="":this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)?this.element_.style.clip="rect(0 "+t+"px 0 "+t+"px)":this.element_.classList.contains(this.CssClasses_.TOP_LEFT)?this.element_.style.clip="rect("+e+"px 0 "+e+"px 0)":this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)?this.element_.style.clip="rect("+e+"px "+t+"px "+e+"px "+t+"px)":this.element_.style.clip=""},d.prototype.removeAnimationEndListener_=function(e){e.target.classList.remove(d.prototype.CssClasses_.IS_ANIMATING)},d.prototype.addAnimationEndListener_=function(){this.element_.addEventListener("transitionend",this.removeAnimationEndListener_),this.element_.addEventListener("webkitTransitionEnd",this.removeAnimationEndListener_)},d.prototype.show=function(e){if(this.element_&&this.container_&&this.outline_){var t=this.element_.getBoundingClientRect().height,s=this.element_.getBoundingClientRect().width;this.container_.style.width=s+"px",this.container_.style.height=t+"px",this.outline_.style.width=s+"px",this.outline_.style.height=t+"px";for(var i=this.Constant_.TRANSITION_DURATION_SECONDS*this.Constant_.TRANSITION_DURATION_FRACTION,n=this.element_.querySelectorAll("."+this.CssClasses_.ITEM),a=0;a0&&this.showSnackbar(this.queuedNotifications_.shift())},C.prototype.cleanup_=function(){this.element_.classList.remove(this.cssClasses_.ACTIVE),setTimeout(function(){this.element_.setAttribute("aria-hidden","true"),this.textElement_.textContent="",Boolean(this.actionElement_.getAttribute("aria-hidden"))||(this.setActionHidden_(!0),this.actionElement_.textContent="",this.actionElement_.removeEventListener("click",this.actionHandler_)),this.actionHandler_=void 0,this.message_=void 0,this.actionText_=void 0,this.active=!1,this.checkQueue_()}.bind(this),this.Constant_.ANIMATION_LENGTH)},C.prototype.setActionHidden_=function(e){e?this.actionElement_.setAttribute("aria-hidden","true"):this.actionElement_.removeAttribute("aria-hidden")},s.register({constructor:C,classAsString:"MaterialSnackbar",cssClass:"mdl-js-snackbar",widget:!0});var u=function(e){this.element_=e,this.init()};window.MaterialSpinner=u,u.prototype.Constant_={MDL_SPINNER_LAYER_COUNT:4},u.prototype.CssClasses_={MDL_SPINNER_LAYER:"mdl-spinner__layer",MDL_SPINNER_CIRCLE_CLIPPER:"mdl-spinner__circle-clipper",MDL_SPINNER_CIRCLE:"mdl-spinner__circle",MDL_SPINNER_GAP_PATCH:"mdl-spinner__gap-patch",MDL_SPINNER_LEFT:"mdl-spinner__left",MDL_SPINNER_RIGHT:"mdl-spinner__right"},u.prototype.createLayer=function(e){var t=document.createElement("div");t.classList.add(this.CssClasses_.MDL_SPINNER_LAYER),t.classList.add(this.CssClasses_.MDL_SPINNER_LAYER+"-"+e);var s=document.createElement("div");s.classList.add(this.CssClasses_.MDL_SPINNER_CIRCLE_CLIPPER),s.classList.add(this.CssClasses_.MDL_SPINNER_LEFT);var i=document.createElement("div");i.classList.add(this.CssClasses_.MDL_SPINNER_GAP_PATCH);var n=document.createElement("div");n.classList.add(this.CssClasses_.MDL_SPINNER_CIRCLE_CLIPPER),n.classList.add(this.CssClasses_.MDL_SPINNER_RIGHT);for(var a=[s,i,n],l=0;l=this.maxRows&&e.preventDefault()},L.prototype.onFocus_=function(e){this.element_.classList.add(this.CssClasses_.IS_FOCUSED)},L.prototype.onBlur_=function(e){this.element_.classList.remove(this.CssClasses_.IS_FOCUSED)},L.prototype.onReset_=function(e){this.updateClasses_()},L.prototype.updateClasses_=function(){this.checkDisabled(),this.checkValidity(),this.checkDirty(),this.checkFocus()},L.prototype.checkDisabled=function(){this.input_.disabled?this.element_.classList.add(this.CssClasses_.IS_DISABLED):this.element_.classList.remove(this.CssClasses_.IS_DISABLED)},L.prototype.checkDisabled=L.prototype.checkDisabled,L.prototype.checkFocus=function(){Boolean(this.element_.querySelector(":focus"))?this.element_.classList.add(this.CssClasses_.IS_FOCUSED):this.element_.classList.remove(this.CssClasses_.IS_FOCUSED)},L.prototype.checkFocus=L.prototype.checkFocus,L.prototype.checkValidity=function(){this.input_.validity&&(this.input_.validity.valid?this.element_.classList.remove(this.CssClasses_.IS_INVALID):this.element_.classList.add(this.CssClasses_.IS_INVALID))},L.prototype.checkValidity=L.prototype.checkValidity,L.prototype.checkDirty=function(){this.input_.value&&this.input_.value.length>0?this.element_.classList.add(this.CssClasses_.IS_DIRTY):this.element_.classList.remove(this.CssClasses_.IS_DIRTY)},L.prototype.checkDirty=L.prototype.checkDirty,L.prototype.disable=function(){this.input_.disabled=!0,this.updateClasses_()},L.prototype.disable=L.prototype.disable,L.prototype.enable=function(){this.input_.disabled=!1,this.updateClasses_()},L.prototype.enable=L.prototype.enable,L.prototype.change=function(e){this.input_.value=e||"",this.updateClasses_()},L.prototype.change=L.prototype.change,L.prototype.init=function(){if(this.element_&&(this.label_=this.element_.querySelector("."+this.CssClasses_.LABEL),this.input_=this.element_.querySelector("."+this.CssClasses_.INPUT),this.input_)){this.input_.hasAttribute(this.Constant_.MAX_ROWS_ATTRIBUTE)&&(this.maxRows=parseInt(this.input_.getAttribute(this.Constant_.MAX_ROWS_ATTRIBUTE),10),isNaN(this.maxRows)&&(this.maxRows=this.Constant_.NO_MAX_ROWS)),this.input_.hasAttribute("placeholder")&&this.element_.classList.add(this.CssClasses_.HAS_PLACEHOLDER),this.boundUpdateClassesHandler=this.updateClasses_.bind(this),this.boundFocusHandler=this.onFocus_.bind(this),this.boundBlurHandler=this.onBlur_.bind(this),this.boundResetHandler=this.onReset_.bind(this),this.input_.addEventListener("input",this.boundUpdateClassesHandler),this.input_.addEventListener("focus",this.boundFocusHandler),this.input_.addEventListener("blur",this.boundBlurHandler),this.input_.addEventListener("reset",this.boundResetHandler),this.maxRows!==this.Constant_.NO_MAX_ROWS&&(this.boundKeyDownHandler=this.onKeyDown_.bind(this),this.input_.addEventListener("keydown",this.boundKeyDownHandler));var e=this.element_.classList.contains(this.CssClasses_.IS_INVALID);this.updateClasses_(),this.element_.classList.add(this.CssClasses_.IS_UPGRADED),e&&this.element_.classList.add(this.CssClasses_.IS_INVALID),this.input_.hasAttribute("autofocus")&&(this.element_.focus(),this.checkFocus())}},s.register({constructor:L,classAsString:"MaterialTextfield",cssClass:"mdl-js-textfield",widget:!0});var I=function(e){this.element_=e,this.init()};window.MaterialTooltip=I,I.prototype.Constant_={},I.prototype.CssClasses_={IS_ACTIVE:"is-active",BOTTOM:"mdl-tooltip--bottom",LEFT:"mdl-tooltip--left",RIGHT:"mdl-tooltip--right",TOP:"mdl-tooltip--top"},I.prototype.handleMouseEnter_=function(e){var t=e.target.getBoundingClientRect(),s=t.left+t.width/2,i=t.top+t.height/2,n=-1*(this.element_.offsetWidth/2),a=-1*(this.element_.offsetHeight/2);this.element_.classList.contains(this.CssClasses_.LEFT)||this.element_.classList.contains(this.CssClasses_.RIGHT)?(s=t.width/2,i+a<0?(this.element_.style.top="0",this.element_.style.marginTop="0"):(this.element_.style.top=i+"px",this.element_.style.marginTop=a+"px")):s+n<0?(this.element_.style.left="0",this.element_.style.marginLeft="0"):(this.element_.style.left=s+"px",this.element_.style.marginLeft=n+"px"),this.element_.classList.contains(this.CssClasses_.TOP)?this.element_.style.top=t.top-this.element_.offsetHeight-10+"px":this.element_.classList.contains(this.CssClasses_.RIGHT)?this.element_.style.left=t.left+t.width+10+"px":this.element_.classList.contains(this.CssClasses_.LEFT)?this.element_.style.left=t.left-this.element_.offsetWidth-10+"px":this.element_.style.top=t.top+t.height+10+"px",this.element_.classList.add(this.CssClasses_.IS_ACTIVE)},I.prototype.hideTooltip_=function(){this.element_.classList.remove(this.CssClasses_.IS_ACTIVE)},I.prototype.init=function(){if(this.element_){var e=this.element_.getAttribute("for")||this.element_.getAttribute("data-mdl-for");e&&(this.forElement_=document.getElementById(e)),this.forElement_&&(this.forElement_.hasAttribute("tabindex")||this.forElement_.setAttribute("tabindex","0"),this.boundMouseEnterHandler=this.handleMouseEnter_.bind(this),this.boundMouseLeaveAndScrollHandler=this.hideTooltip_.bind(this),this.forElement_.addEventListener("mouseenter",this.boundMouseEnterHandler,!1),this.forElement_.addEventListener("touchend",this.boundMouseEnterHandler,!1),this.forElement_.addEventListener("mouseleave",this.boundMouseLeaveAndScrollHandler,!1),window.addEventListener("scroll",this.boundMouseLeaveAndScrollHandler,!0),window.addEventListener("touchstart",this.boundMouseLeaveAndScrollHandler))}},s.register({constructor:I,classAsString:"MaterialTooltip",cssClass:"mdl-tooltip"});var f=function(e){this.element_=e,this.init()};window.MaterialLayout=f,f.prototype.Constant_={MAX_WIDTH:"(max-width: 1024px)",TAB_SCROLL_PIXELS:100,RESIZE_TIMEOUT:100,MENU_ICON:"",CHEVRON_LEFT:"chevron_left",CHEVRON_RIGHT:"chevron_right"},f.prototype.Keycodes_={ENTER:13,ESCAPE:27,SPACE:32},f.prototype.Mode_={STANDARD:0,SEAMED:1,WATERFALL:2,SCROLL:3},f.prototype.CssClasses_={CONTAINER:"mdl-layout__container",HEADER:"mdl-layout__header",DRAWER:"mdl-layout__drawer",CONTENT:"mdl-layout__content",DRAWER_BTN:"mdl-layout__drawer-button",ICON:"material-icons",JS_RIPPLE_EFFECT:"mdl-js-ripple-effect",RIPPLE_CONTAINER:"mdl-layout__tab-ripple-container",RIPPLE:"mdl-ripple",RIPPLE_IGNORE_EVENTS:"mdl-js-ripple-effect--ignore-events",HEADER_SEAMED:"mdl-layout__header--seamed",HEADER_WATERFALL:"mdl-layout__header--waterfall",HEADER_SCROLL:"mdl-layout__header--scroll",FIXED_HEADER:"mdl-layout--fixed-header",OBFUSCATOR:"mdl-layout__obfuscator",TAB_BAR:"mdl-layout__tab-bar",TAB_CONTAINER:"mdl-layout__tab-bar-container",TAB:"mdl-layout__tab",TAB_BAR_BUTTON:"mdl-layout__tab-bar-button",TAB_BAR_LEFT_BUTTON:"mdl-layout__tab-bar-left-button",TAB_BAR_RIGHT_BUTTON:"mdl-layout__tab-bar-right-button",TAB_MANUAL_SWITCH:"mdl-layout__tab-manual-switch",PANEL:"mdl-layout__tab-panel",HAS_DRAWER:"has-drawer",HAS_TABS:"has-tabs",HAS_SCROLLING_HEADER:"has-scrolling-header",CASTING_SHADOW:"is-casting-shadow",IS_COMPACT:"is-compact",IS_SMALL_SCREEN:"is-small-screen",IS_DRAWER_OPEN:"is-visible",IS_ACTIVE:"is-active",IS_UPGRADED:"is-upgraded",IS_ANIMATING:"is-animating",ON_LARGE_SCREEN:"mdl-layout--large-screen-only",ON_SMALL_SCREEN:"mdl-layout--small-screen-only"},f.prototype.contentScrollHandler_=function(){if(!this.header_.classList.contains(this.CssClasses_.IS_ANIMATING)){var e=!this.element_.classList.contains(this.CssClasses_.IS_SMALL_SCREEN)||this.element_.classList.contains(this.CssClasses_.FIXED_HEADER);this.content_.scrollTop>0&&!this.header_.classList.contains(this.CssClasses_.IS_COMPACT)?(this.header_.classList.add(this.CssClasses_.CASTING_SHADOW),this.header_.classList.add(this.CssClasses_.IS_COMPACT),e&&this.header_.classList.add(this.CssClasses_.IS_ANIMATING)):this.content_.scrollTop<=0&&this.header_.classList.contains(this.CssClasses_.IS_COMPACT)&&(this.header_.classList.remove(this.CssClasses_.CASTING_SHADOW),this.header_.classList.remove(this.CssClasses_.IS_COMPACT),e&&this.header_.classList.add(this.CssClasses_.IS_ANIMATING))}},f.prototype.keyboardEventHandler_=function(e){e.keyCode===this.Keycodes_.ESCAPE&&this.drawer_.classList.contains(this.CssClasses_.IS_DRAWER_OPEN)&&this.toggleDrawer()},f.prototype.screenSizeHandler_=function(){this.screenSizeMediaQuery_.matches?this.element_.classList.add(this.CssClasses_.IS_SMALL_SCREEN):(this.element_.classList.remove(this.CssClasses_.IS_SMALL_SCREEN),this.drawer_&&(this.drawer_.classList.remove(this.CssClasses_.IS_DRAWER_OPEN),this.obfuscator_.classList.remove(this.CssClasses_.IS_DRAWER_OPEN)))},f.prototype.drawerToggleHandler_=function(e){if(e&&"keydown"===e.type){if(e.keyCode!==this.Keycodes_.SPACE&&e.keyCode!==this.Keycodes_.ENTER)return;e.preventDefault()}this.toggleDrawer()},f.prototype.headerTransitionEndHandler_=function(){this.header_.classList.remove(this.CssClasses_.IS_ANIMATING)},f.prototype.headerClickHandler_=function(){this.header_.classList.contains(this.CssClasses_.IS_COMPACT)&&(this.header_.classList.remove(this.CssClasses_.IS_COMPACT),this.header_.classList.add(this.CssClasses_.IS_ANIMATING))},f.prototype.resetTabState_=function(e){for(var t=0;t0?c.classList.add(this.CssClasses_.IS_ACTIVE):c.classList.remove(this.CssClasses_.IS_ACTIVE),this.tabBar_.scrollLeft0)return;this.setFrameCount(1);var i,n,a=e.currentTarget.getBoundingClientRect();if(0===e.clientX&&0===e.clientY)i=Math.round(a.width/2),n=Math.round(a.height/2);else{var l=void 0!==e.clientX?e.clientX:e.touches[0].clientX,o=void 0!==e.clientY?e.clientY:e.touches[0].clientY;i=Math.round(l-a.left),n=Math.round(o-a.top)}this.setRippleXY(i,n),this.setRippleStyles(!0),window.requestAnimationFrame(this.animFrameHandler.bind(this))}},S.prototype.upHandler_=function(e){e&&2!==e.detail&&window.setTimeout(function(){this.rippleElement_.classList.remove(this.CssClasses_.IS_VISIBLE)}.bind(this),0)},S.prototype.init=function(){if(this.element_){var e=this.element_.classList.contains(this.CssClasses_.RIPPLE_CENTER);this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT_IGNORE_EVENTS)||(this.rippleElement_=this.element_.querySelector("."+this.CssClasses_.RIPPLE),this.frameCount_=0,this.rippleSize_=0,this.x_=0,this.y_=0,this.ignoringMouseDown_=!1,this.boundDownHandler=this.downHandler_.bind(this),this.element_.addEventListener("mousedown",this.boundDownHandler),this.element_.addEventListener("touchstart",this.boundDownHandler),this.boundUpHandler=this.upHandler_.bind(this),this.element_.addEventListener("mouseup",this.boundUpHandler),this.element_.addEventListener("mouseleave",this.boundUpHandler),this.element_.addEventListener("touchend",this.boundUpHandler),this.element_.addEventListener("blur",this.boundUpHandler),this.getFrameCount=function(){return this.frameCount_},this.setFrameCount=function(e){this.frameCount_=e},this.getRippleElement=function(){return this.rippleElement_},this.setRippleXY=function(e,t){this.x_=e,this.y_=t},this.setRippleStyles=function(t){if(null!==this.rippleElement_){var s,i,n,a="translate("+this.x_+"px, "+this.y_+"px)";t?(i=this.Constant_.INITIAL_SCALE,n=this.Constant_.INITIAL_SIZE):(i=this.Constant_.FINAL_SCALE,n=this.rippleSize_+"px",e&&(a="translate("+this.boundWidth/2+"px, "+this.boundHeight/2+"px)")),s="translate(-50%, -50%) "+a+i,this.rippleElement_.style.webkitTransform=s,this.rippleElement_.style.msTransform=s,this.rippleElement_.style.transform=s,t?this.rippleElement_.classList.remove(this.CssClasses_.IS_ANIMATING):this.rippleElement_.classList.add(this.CssClasses_.IS_ANIMATING)}},this.animFrameHandler=function(){this.frameCount_-- >0?window.requestAnimationFrame(this.animFrameHandler.bind(this)):this.setRippleStyles(!1)})}},s.register({constructor:S,classAsString:"MaterialRipple",cssClass:"mdl-js-ripple-effect",widget:!1})}(); +//# sourceMappingURL=material.min.js.map diff --git a/client/src/jsMain/resources/styles/containers.css b/client/src/jsMain/resources/styles/containers.css new file mode 100644 index 00000000..75a1ce2c --- /dev/null +++ b/client/src/jsMain/resources/styles/containers.css @@ -0,0 +1,5 @@ +.vertical_container { + display: flex; + flex-direction: column; + align-items: center; +} diff --git a/client/src/jsMain/resources/styles/material.min.css b/client/src/jsMain/resources/styles/material.min.css new file mode 100644 index 00000000..9db19125 --- /dev/null +++ b/client/src/jsMain/resources/styles/material.min.css @@ -0,0 +1,8 @@ +/** + * material-design-lite - Material Design Components in CSS, JS and HTML + * @version v1.3.0 + * @license Apache-2.0 + * @copyright 2015 Google, Inc. + * @link https://github.com/google/material-design-lite + */ +@charset "UTF-8";html{color:rgba(0,0,0,.87)}::-moz-selection{background:#b3d4fc;text-shadow:none}::selection{background:#b3d4fc;text-shadow:none}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}.browserupgrade{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.hidden{display:none!important}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}@media print{*,*:before,*:after,*:first-letter{background:transparent!important;color:#000!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}a,.mdl-accordion,.mdl-button,.mdl-card,.mdl-checkbox,.mdl-dropdown-menu,.mdl-icon-toggle,.mdl-item,.mdl-radio,.mdl-slider,.mdl-switch,.mdl-tabs__tab{-webkit-tap-highlight-color:transparent;-webkit-tap-highlight-color:rgba(255,255,255,0)}html{width:100%;height:100%;-ms-touch-action:manipulation;touch-action:manipulation}body{width:100%;min-height:100%}main{display:block}*[hidden]{display:none!important}html,body{font-family:"Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:20px}h1,h2,h3,h4,h5,h6,p{padding:0}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400;line-height:1.35;letter-spacing:-.02em;opacity:.54;font-size:.6em}h1{font-size:56px;line-height:1.35;letter-spacing:-.02em;margin:24px 0}h1,h2{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h2{font-size:45px;line-height:48px}h2,h3{margin:24px 0}h3{font-size:34px;line-height:40px}h3,h4{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h4{font-size:24px;line-height:32px;-moz-osx-font-smoothing:grayscale;margin:24px 0 16px}h5{font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}h5,h6{font-family:"Roboto","Helvetica","Arial",sans-serif;margin:24px 0 16px}h6{font-size:16px;letter-spacing:.04em}h6,p{font-weight:400;line-height:24px}p{font-size:14px;letter-spacing:0;margin:0 0 16px}a{color:rgb(68,138,255);font-weight:500}blockquote{font-family:"Roboto","Helvetica","Arial",sans-serif;position:relative;font-size:24px;font-weight:300;font-style:italic;line-height:1.35;letter-spacing:.08em}blockquote:before{position:absolute;left:-.5em;content:'“'}blockquote:after{content:'”';margin-left:-.05em}mark{background-color:#f4ff81}dt{font-weight:700}address{font-size:12px;line-height:1;font-style:normal}address,ul,ol{font-weight:400;letter-spacing:0}ul,ol{font-size:14px;line-height:24px}.mdl-typography--display-4,.mdl-typography--display-4-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:112px;font-weight:300;line-height:1;letter-spacing:-.04em}.mdl-typography--display-4-color-contrast{opacity:.54}.mdl-typography--display-3,.mdl-typography--display-3-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:56px;font-weight:400;line-height:1.35;letter-spacing:-.02em}.mdl-typography--display-3-color-contrast{opacity:.54}.mdl-typography--display-2,.mdl-typography--display-2-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:45px;font-weight:400;line-height:48px}.mdl-typography--display-2-color-contrast{opacity:.54}.mdl-typography--display-1,.mdl-typography--display-1-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:34px;font-weight:400;line-height:40px}.mdl-typography--display-1-color-contrast{opacity:.54}.mdl-typography--headline,.mdl-typography--headline-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:24px;font-weight:400;line-height:32px;-moz-osx-font-smoothing:grayscale}.mdl-typography--headline-color-contrast{opacity:.87}.mdl-typography--title,.mdl-typography--title-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}.mdl-typography--title-color-contrast{opacity:.87}.mdl-typography--subhead,.mdl-typography--subhead-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;line-height:24px;letter-spacing:.04em}.mdl-typography--subhead-color-contrast{opacity:.87}.mdl-typography--body-2,.mdl-typography--body-2-color-contrast{font-size:14px;font-weight:700;line-height:24px;letter-spacing:0}.mdl-typography--body-2-color-contrast{opacity:.87}.mdl-typography--body-1,.mdl-typography--body-1-color-contrast{font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-color-contrast{opacity:.87}.mdl-typography--body-2-force-preferred-font,.mdl-typography--body-2-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:24px;letter-spacing:0}.mdl-typography--body-2-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--body-1-force-preferred-font,.mdl-typography--body-1-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--caption,.mdl-typography--caption-force-preferred-font{font-size:12px;font-weight:400;line-height:1;letter-spacing:0}.mdl-typography--caption-force-preferred-font{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--caption-color-contrast,.mdl-typography--caption-force-preferred-font-color-contrast{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;opacity:.54}.mdl-typography--caption-force-preferred-font-color-contrast,.mdl-typography--menu{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--menu{font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--menu-color-contrast{opacity:.87}.mdl-typography--menu-color-contrast,.mdl-typography--button,.mdl-typography--button-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--button,.mdl-typography--button-color-contrast{text-transform:uppercase}.mdl-typography--button-color-contrast{opacity:.87}.mdl-typography--text-left{text-align:left}.mdl-typography--text-right{text-align:right}.mdl-typography--text-center{text-align:center}.mdl-typography--text-justify{text-align:justify}.mdl-typography--text-nowrap{white-space:nowrap}.mdl-typography--text-lowercase{text-transform:lowercase}.mdl-typography--text-uppercase{text-transform:uppercase}.mdl-typography--text-capitalize{text-transform:capitalize}.mdl-typography--font-thin{font-weight:200!important}.mdl-typography--font-light{font-weight:300!important}.mdl-typography--font-regular{font-weight:400!important}.mdl-typography--font-medium{font-weight:500!important}.mdl-typography--font-bold{font-weight:700!important}.mdl-typography--font-black{font-weight:900!important}.material-icons{font-family:'Material Icons';font-weight:400;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased}.mdl-color-text--red{color:#f44336 !important}.mdl-color--red{background-color:#f44336 !important}.mdl-color-text--red-50{color:#ffebee !important}.mdl-color--red-50{background-color:#ffebee !important}.mdl-color-text--red-100{color:#ffcdd2 !important}.mdl-color--red-100{background-color:#ffcdd2 !important}.mdl-color-text--red-200{color:#ef9a9a !important}.mdl-color--red-200{background-color:#ef9a9a !important}.mdl-color-text--red-300{color:#e57373 !important}.mdl-color--red-300{background-color:#e57373 !important}.mdl-color-text--red-400{color:#ef5350 !important}.mdl-color--red-400{background-color:#ef5350 !important}.mdl-color-text--red-500{color:#f44336 !important}.mdl-color--red-500{background-color:#f44336 !important}.mdl-color-text--red-600{color:#e53935 !important}.mdl-color--red-600{background-color:#e53935 !important}.mdl-color-text--red-700{color:#d32f2f !important}.mdl-color--red-700{background-color:#d32f2f !important}.mdl-color-text--red-800{color:#c62828 !important}.mdl-color--red-800{background-color:#c62828 !important}.mdl-color-text--red-900{color:#b71c1c !important}.mdl-color--red-900{background-color:#b71c1c !important}.mdl-color-text--red-A100{color:#ff8a80 !important}.mdl-color--red-A100{background-color:#ff8a80 !important}.mdl-color-text--red-A200{color:#ff5252 !important}.mdl-color--red-A200{background-color:#ff5252 !important}.mdl-color-text--red-A400{color:#ff1744 !important}.mdl-color--red-A400{background-color:#ff1744 !important}.mdl-color-text--red-A700{color:#d50000 !important}.mdl-color--red-A700{background-color:#d50000 !important}.mdl-color-text--pink{color:#e91e63 !important}.mdl-color--pink{background-color:#e91e63 !important}.mdl-color-text--pink-50{color:#fce4ec !important}.mdl-color--pink-50{background-color:#fce4ec !important}.mdl-color-text--pink-100{color:#f8bbd0 !important}.mdl-color--pink-100{background-color:#f8bbd0 !important}.mdl-color-text--pink-200{color:#f48fb1 !important}.mdl-color--pink-200{background-color:#f48fb1 !important}.mdl-color-text--pink-300{color:#f06292 !important}.mdl-color--pink-300{background-color:#f06292 !important}.mdl-color-text--pink-400{color:#ec407a !important}.mdl-color--pink-400{background-color:#ec407a !important}.mdl-color-text--pink-500{color:#e91e63 !important}.mdl-color--pink-500{background-color:#e91e63 !important}.mdl-color-text--pink-600{color:#d81b60 !important}.mdl-color--pink-600{background-color:#d81b60 !important}.mdl-color-text--pink-700{color:#c2185b !important}.mdl-color--pink-700{background-color:#c2185b !important}.mdl-color-text--pink-800{color:#ad1457 !important}.mdl-color--pink-800{background-color:#ad1457 !important}.mdl-color-text--pink-900{color:#880e4f !important}.mdl-color--pink-900{background-color:#880e4f !important}.mdl-color-text--pink-A100{color:#ff80ab !important}.mdl-color--pink-A100{background-color:#ff80ab !important}.mdl-color-text--pink-A200{color:#ff4081 !important}.mdl-color--pink-A200{background-color:#ff4081 !important}.mdl-color-text--pink-A400{color:#f50057 !important}.mdl-color--pink-A400{background-color:#f50057 !important}.mdl-color-text--pink-A700{color:#c51162 !important}.mdl-color--pink-A700{background-color:#c51162 !important}.mdl-color-text--purple{color:#9c27b0 !important}.mdl-color--purple{background-color:#9c27b0 !important}.mdl-color-text--purple-50{color:#f3e5f5 !important}.mdl-color--purple-50{background-color:#f3e5f5 !important}.mdl-color-text--purple-100{color:#e1bee7 !important}.mdl-color--purple-100{background-color:#e1bee7 !important}.mdl-color-text--purple-200{color:#ce93d8 !important}.mdl-color--purple-200{background-color:#ce93d8 !important}.mdl-color-text--purple-300{color:#ba68c8 !important}.mdl-color--purple-300{background-color:#ba68c8 !important}.mdl-color-text--purple-400{color:#ab47bc !important}.mdl-color--purple-400{background-color:#ab47bc !important}.mdl-color-text--purple-500{color:#9c27b0 !important}.mdl-color--purple-500{background-color:#9c27b0 !important}.mdl-color-text--purple-600{color:#8e24aa !important}.mdl-color--purple-600{background-color:#8e24aa !important}.mdl-color-text--purple-700{color:#7b1fa2 !important}.mdl-color--purple-700{background-color:#7b1fa2 !important}.mdl-color-text--purple-800{color:#6a1b9a !important}.mdl-color--purple-800{background-color:#6a1b9a !important}.mdl-color-text--purple-900{color:#4a148c !important}.mdl-color--purple-900{background-color:#4a148c !important}.mdl-color-text--purple-A100{color:#ea80fc !important}.mdl-color--purple-A100{background-color:#ea80fc !important}.mdl-color-text--purple-A200{color:#e040fb !important}.mdl-color--purple-A200{background-color:#e040fb !important}.mdl-color-text--purple-A400{color:#d500f9 !important}.mdl-color--purple-A400{background-color:#d500f9 !important}.mdl-color-text--purple-A700{color:#a0f !important}.mdl-color--purple-A700{background-color:#a0f !important}.mdl-color-text--deep-purple{color:#673ab7 !important}.mdl-color--deep-purple{background-color:#673ab7 !important}.mdl-color-text--deep-purple-50{color:#ede7f6 !important}.mdl-color--deep-purple-50{background-color:#ede7f6 !important}.mdl-color-text--deep-purple-100{color:#d1c4e9 !important}.mdl-color--deep-purple-100{background-color:#d1c4e9 !important}.mdl-color-text--deep-purple-200{color:#b39ddb !important}.mdl-color--deep-purple-200{background-color:#b39ddb !important}.mdl-color-text--deep-purple-300{color:#9575cd !important}.mdl-color--deep-purple-300{background-color:#9575cd !important}.mdl-color-text--deep-purple-400{color:#7e57c2 !important}.mdl-color--deep-purple-400{background-color:#7e57c2 !important}.mdl-color-text--deep-purple-500{color:#673ab7 !important}.mdl-color--deep-purple-500{background-color:#673ab7 !important}.mdl-color-text--deep-purple-600{color:#5e35b1 !important}.mdl-color--deep-purple-600{background-color:#5e35b1 !important}.mdl-color-text--deep-purple-700{color:#512da8 !important}.mdl-color--deep-purple-700{background-color:#512da8 !important}.mdl-color-text--deep-purple-800{color:#4527a0 !important}.mdl-color--deep-purple-800{background-color:#4527a0 !important}.mdl-color-text--deep-purple-900{color:#311b92 !important}.mdl-color--deep-purple-900{background-color:#311b92 !important}.mdl-color-text--deep-purple-A100{color:#b388ff !important}.mdl-color--deep-purple-A100{background-color:#b388ff !important}.mdl-color-text--deep-purple-A200{color:#7c4dff !important}.mdl-color--deep-purple-A200{background-color:#7c4dff !important}.mdl-color-text--deep-purple-A400{color:#651fff !important}.mdl-color--deep-purple-A400{background-color:#651fff !important}.mdl-color-text--deep-purple-A700{color:#6200ea !important}.mdl-color--deep-purple-A700{background-color:#6200ea !important}.mdl-color-text--indigo{color:#3f51b5 !important}.mdl-color--indigo{background-color:#3f51b5 !important}.mdl-color-text--indigo-50{color:#e8eaf6 !important}.mdl-color--indigo-50{background-color:#e8eaf6 !important}.mdl-color-text--indigo-100{color:#c5cae9 !important}.mdl-color--indigo-100{background-color:#c5cae9 !important}.mdl-color-text--indigo-200{color:#9fa8da !important}.mdl-color--indigo-200{background-color:#9fa8da !important}.mdl-color-text--indigo-300{color:#7986cb !important}.mdl-color--indigo-300{background-color:#7986cb !important}.mdl-color-text--indigo-400{color:#5c6bc0 !important}.mdl-color--indigo-400{background-color:#5c6bc0 !important}.mdl-color-text--indigo-500{color:#3f51b5 !important}.mdl-color--indigo-500{background-color:#3f51b5 !important}.mdl-color-text--indigo-600{color:#3949ab !important}.mdl-color--indigo-600{background-color:#3949ab !important}.mdl-color-text--indigo-700{color:#303f9f !important}.mdl-color--indigo-700{background-color:#303f9f !important}.mdl-color-text--indigo-800{color:#283593 !important}.mdl-color--indigo-800{background-color:#283593 !important}.mdl-color-text--indigo-900{color:#1a237e !important}.mdl-color--indigo-900{background-color:#1a237e !important}.mdl-color-text--indigo-A100{color:#8c9eff !important}.mdl-color--indigo-A100{background-color:#8c9eff !important}.mdl-color-text--indigo-A200{color:#536dfe !important}.mdl-color--indigo-A200{background-color:#536dfe !important}.mdl-color-text--indigo-A400{color:#3d5afe !important}.mdl-color--indigo-A400{background-color:#3d5afe !important}.mdl-color-text--indigo-A700{color:#304ffe !important}.mdl-color--indigo-A700{background-color:#304ffe !important}.mdl-color-text--blue{color:#2196f3 !important}.mdl-color--blue{background-color:#2196f3 !important}.mdl-color-text--blue-50{color:#e3f2fd !important}.mdl-color--blue-50{background-color:#e3f2fd !important}.mdl-color-text--blue-100{color:#bbdefb !important}.mdl-color--blue-100{background-color:#bbdefb !important}.mdl-color-text--blue-200{color:#90caf9 !important}.mdl-color--blue-200{background-color:#90caf9 !important}.mdl-color-text--blue-300{color:#64b5f6 !important}.mdl-color--blue-300{background-color:#64b5f6 !important}.mdl-color-text--blue-400{color:#42a5f5 !important}.mdl-color--blue-400{background-color:#42a5f5 !important}.mdl-color-text--blue-500{color:#2196f3 !important}.mdl-color--blue-500{background-color:#2196f3 !important}.mdl-color-text--blue-600{color:#1e88e5 !important}.mdl-color--blue-600{background-color:#1e88e5 !important}.mdl-color-text--blue-700{color:#1976d2 !important}.mdl-color--blue-700{background-color:#1976d2 !important}.mdl-color-text--blue-800{color:#1565c0 !important}.mdl-color--blue-800{background-color:#1565c0 !important}.mdl-color-text--blue-900{color:#0d47a1 !important}.mdl-color--blue-900{background-color:#0d47a1 !important}.mdl-color-text--blue-A100{color:#82b1ff !important}.mdl-color--blue-A100{background-color:#82b1ff !important}.mdl-color-text--blue-A200{color:#448aff !important}.mdl-color--blue-A200{background-color:#448aff !important}.mdl-color-text--blue-A400{color:#2979ff !important}.mdl-color--blue-A400{background-color:#2979ff !important}.mdl-color-text--blue-A700{color:#2962ff !important}.mdl-color--blue-A700{background-color:#2962ff !important}.mdl-color-text--light-blue{color:#03a9f4 !important}.mdl-color--light-blue{background-color:#03a9f4 !important}.mdl-color-text--light-blue-50{color:#e1f5fe !important}.mdl-color--light-blue-50{background-color:#e1f5fe !important}.mdl-color-text--light-blue-100{color:#b3e5fc !important}.mdl-color--light-blue-100{background-color:#b3e5fc !important}.mdl-color-text--light-blue-200{color:#81d4fa !important}.mdl-color--light-blue-200{background-color:#81d4fa !important}.mdl-color-text--light-blue-300{color:#4fc3f7 !important}.mdl-color--light-blue-300{background-color:#4fc3f7 !important}.mdl-color-text--light-blue-400{color:#29b6f6 !important}.mdl-color--light-blue-400{background-color:#29b6f6 !important}.mdl-color-text--light-blue-500{color:#03a9f4 !important}.mdl-color--light-blue-500{background-color:#03a9f4 !important}.mdl-color-text--light-blue-600{color:#039be5 !important}.mdl-color--light-blue-600{background-color:#039be5 !important}.mdl-color-text--light-blue-700{color:#0288d1 !important}.mdl-color--light-blue-700{background-color:#0288d1 !important}.mdl-color-text--light-blue-800{color:#0277bd !important}.mdl-color--light-blue-800{background-color:#0277bd !important}.mdl-color-text--light-blue-900{color:#01579b !important}.mdl-color--light-blue-900{background-color:#01579b !important}.mdl-color-text--light-blue-A100{color:#80d8ff !important}.mdl-color--light-blue-A100{background-color:#80d8ff !important}.mdl-color-text--light-blue-A200{color:#40c4ff !important}.mdl-color--light-blue-A200{background-color:#40c4ff !important}.mdl-color-text--light-blue-A400{color:#00b0ff !important}.mdl-color--light-blue-A400{background-color:#00b0ff !important}.mdl-color-text--light-blue-A700{color:#0091ea !important}.mdl-color--light-blue-A700{background-color:#0091ea !important}.mdl-color-text--cyan{color:#00bcd4 !important}.mdl-color--cyan{background-color:#00bcd4 !important}.mdl-color-text--cyan-50{color:#e0f7fa !important}.mdl-color--cyan-50{background-color:#e0f7fa !important}.mdl-color-text--cyan-100{color:#b2ebf2 !important}.mdl-color--cyan-100{background-color:#b2ebf2 !important}.mdl-color-text--cyan-200{color:#80deea !important}.mdl-color--cyan-200{background-color:#80deea !important}.mdl-color-text--cyan-300{color:#4dd0e1 !important}.mdl-color--cyan-300{background-color:#4dd0e1 !important}.mdl-color-text--cyan-400{color:#26c6da !important}.mdl-color--cyan-400{background-color:#26c6da !important}.mdl-color-text--cyan-500{color:#00bcd4 !important}.mdl-color--cyan-500{background-color:#00bcd4 !important}.mdl-color-text--cyan-600{color:#00acc1 !important}.mdl-color--cyan-600{background-color:#00acc1 !important}.mdl-color-text--cyan-700{color:#0097a7 !important}.mdl-color--cyan-700{background-color:#0097a7 !important}.mdl-color-text--cyan-800{color:#00838f !important}.mdl-color--cyan-800{background-color:#00838f !important}.mdl-color-text--cyan-900{color:#006064 !important}.mdl-color--cyan-900{background-color:#006064 !important}.mdl-color-text--cyan-A100{color:#84ffff !important}.mdl-color--cyan-A100{background-color:#84ffff !important}.mdl-color-text--cyan-A200{color:#18ffff !important}.mdl-color--cyan-A200{background-color:#18ffff !important}.mdl-color-text--cyan-A400{color:#00e5ff !important}.mdl-color--cyan-A400{background-color:#00e5ff !important}.mdl-color-text--cyan-A700{color:#00b8d4 !important}.mdl-color--cyan-A700{background-color:#00b8d4 !important}.mdl-color-text--teal{color:#009688 !important}.mdl-color--teal{background-color:#009688 !important}.mdl-color-text--teal-50{color:#e0f2f1 !important}.mdl-color--teal-50{background-color:#e0f2f1 !important}.mdl-color-text--teal-100{color:#b2dfdb !important}.mdl-color--teal-100{background-color:#b2dfdb !important}.mdl-color-text--teal-200{color:#80cbc4 !important}.mdl-color--teal-200{background-color:#80cbc4 !important}.mdl-color-text--teal-300{color:#4db6ac !important}.mdl-color--teal-300{background-color:#4db6ac !important}.mdl-color-text--teal-400{color:#26a69a !important}.mdl-color--teal-400{background-color:#26a69a !important}.mdl-color-text--teal-500{color:#009688 !important}.mdl-color--teal-500{background-color:#009688 !important}.mdl-color-text--teal-600{color:#00897b !important}.mdl-color--teal-600{background-color:#00897b !important}.mdl-color-text--teal-700{color:#00796b !important}.mdl-color--teal-700{background-color:#00796b !important}.mdl-color-text--teal-800{color:#00695c !important}.mdl-color--teal-800{background-color:#00695c !important}.mdl-color-text--teal-900{color:#004d40 !important}.mdl-color--teal-900{background-color:#004d40 !important}.mdl-color-text--teal-A100{color:#a7ffeb !important}.mdl-color--teal-A100{background-color:#a7ffeb !important}.mdl-color-text--teal-A200{color:#64ffda !important}.mdl-color--teal-A200{background-color:#64ffda !important}.mdl-color-text--teal-A400{color:#1de9b6 !important}.mdl-color--teal-A400{background-color:#1de9b6 !important}.mdl-color-text--teal-A700{color:#00bfa5 !important}.mdl-color--teal-A700{background-color:#00bfa5 !important}.mdl-color-text--green{color:#4caf50 !important}.mdl-color--green{background-color:#4caf50 !important}.mdl-color-text--green-50{color:#e8f5e9 !important}.mdl-color--green-50{background-color:#e8f5e9 !important}.mdl-color-text--green-100{color:#c8e6c9 !important}.mdl-color--green-100{background-color:#c8e6c9 !important}.mdl-color-text--green-200{color:#a5d6a7 !important}.mdl-color--green-200{background-color:#a5d6a7 !important}.mdl-color-text--green-300{color:#81c784 !important}.mdl-color--green-300{background-color:#81c784 !important}.mdl-color-text--green-400{color:#66bb6a !important}.mdl-color--green-400{background-color:#66bb6a !important}.mdl-color-text--green-500{color:#4caf50 !important}.mdl-color--green-500{background-color:#4caf50 !important}.mdl-color-text--green-600{color:#43a047 !important}.mdl-color--green-600{background-color:#43a047 !important}.mdl-color-text--green-700{color:#388e3c !important}.mdl-color--green-700{background-color:#388e3c !important}.mdl-color-text--green-800{color:#2e7d32 !important}.mdl-color--green-800{background-color:#2e7d32 !important}.mdl-color-text--green-900{color:#1b5e20 !important}.mdl-color--green-900{background-color:#1b5e20 !important}.mdl-color-text--green-A100{color:#b9f6ca !important}.mdl-color--green-A100{background-color:#b9f6ca !important}.mdl-color-text--green-A200{color:#69f0ae !important}.mdl-color--green-A200{background-color:#69f0ae !important}.mdl-color-text--green-A400{color:#00e676 !important}.mdl-color--green-A400{background-color:#00e676 !important}.mdl-color-text--green-A700{color:#00c853 !important}.mdl-color--green-A700{background-color:#00c853 !important}.mdl-color-text--light-green{color:#8bc34a !important}.mdl-color--light-green{background-color:#8bc34a !important}.mdl-color-text--light-green-50{color:#f1f8e9 !important}.mdl-color--light-green-50{background-color:#f1f8e9 !important}.mdl-color-text--light-green-100{color:#dcedc8 !important}.mdl-color--light-green-100{background-color:#dcedc8 !important}.mdl-color-text--light-green-200{color:#c5e1a5 !important}.mdl-color--light-green-200{background-color:#c5e1a5 !important}.mdl-color-text--light-green-300{color:#aed581 !important}.mdl-color--light-green-300{background-color:#aed581 !important}.mdl-color-text--light-green-400{color:#9ccc65 !important}.mdl-color--light-green-400{background-color:#9ccc65 !important}.mdl-color-text--light-green-500{color:#8bc34a !important}.mdl-color--light-green-500{background-color:#8bc34a !important}.mdl-color-text--light-green-600{color:#7cb342 !important}.mdl-color--light-green-600{background-color:#7cb342 !important}.mdl-color-text--light-green-700{color:#689f38 !important}.mdl-color--light-green-700{background-color:#689f38 !important}.mdl-color-text--light-green-800{color:#558b2f !important}.mdl-color--light-green-800{background-color:#558b2f !important}.mdl-color-text--light-green-900{color:#33691e !important}.mdl-color--light-green-900{background-color:#33691e !important}.mdl-color-text--light-green-A100{color:#ccff90 !important}.mdl-color--light-green-A100{background-color:#ccff90 !important}.mdl-color-text--light-green-A200{color:#b2ff59 !important}.mdl-color--light-green-A200{background-color:#b2ff59 !important}.mdl-color-text--light-green-A400{color:#76ff03 !important}.mdl-color--light-green-A400{background-color:#76ff03 !important}.mdl-color-text--light-green-A700{color:#64dd17 !important}.mdl-color--light-green-A700{background-color:#64dd17 !important}.mdl-color-text--lime{color:#cddc39 !important}.mdl-color--lime{background-color:#cddc39 !important}.mdl-color-text--lime-50{color:#f9fbe7 !important}.mdl-color--lime-50{background-color:#f9fbe7 !important}.mdl-color-text--lime-100{color:#f0f4c3 !important}.mdl-color--lime-100{background-color:#f0f4c3 !important}.mdl-color-text--lime-200{color:#e6ee9c !important}.mdl-color--lime-200{background-color:#e6ee9c !important}.mdl-color-text--lime-300{color:#dce775 !important}.mdl-color--lime-300{background-color:#dce775 !important}.mdl-color-text--lime-400{color:#d4e157 !important}.mdl-color--lime-400{background-color:#d4e157 !important}.mdl-color-text--lime-500{color:#cddc39 !important}.mdl-color--lime-500{background-color:#cddc39 !important}.mdl-color-text--lime-600{color:#c0ca33 !important}.mdl-color--lime-600{background-color:#c0ca33 !important}.mdl-color-text--lime-700{color:#afb42b !important}.mdl-color--lime-700{background-color:#afb42b !important}.mdl-color-text--lime-800{color:#9e9d24 !important}.mdl-color--lime-800{background-color:#9e9d24 !important}.mdl-color-text--lime-900{color:#827717 !important}.mdl-color--lime-900{background-color:#827717 !important}.mdl-color-text--lime-A100{color:#f4ff81 !important}.mdl-color--lime-A100{background-color:#f4ff81 !important}.mdl-color-text--lime-A200{color:#eeff41 !important}.mdl-color--lime-A200{background-color:#eeff41 !important}.mdl-color-text--lime-A400{color:#c6ff00 !important}.mdl-color--lime-A400{background-color:#c6ff00 !important}.mdl-color-text--lime-A700{color:#aeea00 !important}.mdl-color--lime-A700{background-color:#aeea00 !important}.mdl-color-text--yellow{color:#ffeb3b !important}.mdl-color--yellow{background-color:#ffeb3b !important}.mdl-color-text--yellow-50{color:#fffde7 !important}.mdl-color--yellow-50{background-color:#fffde7 !important}.mdl-color-text--yellow-100{color:#fff9c4 !important}.mdl-color--yellow-100{background-color:#fff9c4 !important}.mdl-color-text--yellow-200{color:#fff59d !important}.mdl-color--yellow-200{background-color:#fff59d !important}.mdl-color-text--yellow-300{color:#fff176 !important}.mdl-color--yellow-300{background-color:#fff176 !important}.mdl-color-text--yellow-400{color:#ffee58 !important}.mdl-color--yellow-400{background-color:#ffee58 !important}.mdl-color-text--yellow-500{color:#ffeb3b !important}.mdl-color--yellow-500{background-color:#ffeb3b !important}.mdl-color-text--yellow-600{color:#fdd835 !important}.mdl-color--yellow-600{background-color:#fdd835 !important}.mdl-color-text--yellow-700{color:#fbc02d !important}.mdl-color--yellow-700{background-color:#fbc02d !important}.mdl-color-text--yellow-800{color:#f9a825 !important}.mdl-color--yellow-800{background-color:#f9a825 !important}.mdl-color-text--yellow-900{color:#f57f17 !important}.mdl-color--yellow-900{background-color:#f57f17 !important}.mdl-color-text--yellow-A100{color:#ffff8d !important}.mdl-color--yellow-A100{background-color:#ffff8d !important}.mdl-color-text--yellow-A200{color:#ff0 !important}.mdl-color--yellow-A200{background-color:#ff0 !important}.mdl-color-text--yellow-A400{color:#ffea00 !important}.mdl-color--yellow-A400{background-color:#ffea00 !important}.mdl-color-text--yellow-A700{color:#ffd600 !important}.mdl-color--yellow-A700{background-color:#ffd600 !important}.mdl-color-text--amber{color:#ffc107 !important}.mdl-color--amber{background-color:#ffc107 !important}.mdl-color-text--amber-50{color:#fff8e1 !important}.mdl-color--amber-50{background-color:#fff8e1 !important}.mdl-color-text--amber-100{color:#ffecb3 !important}.mdl-color--amber-100{background-color:#ffecb3 !important}.mdl-color-text--amber-200{color:#ffe082 !important}.mdl-color--amber-200{background-color:#ffe082 !important}.mdl-color-text--amber-300{color:#ffd54f !important}.mdl-color--amber-300{background-color:#ffd54f !important}.mdl-color-text--amber-400{color:#ffca28 !important}.mdl-color--amber-400{background-color:#ffca28 !important}.mdl-color-text--amber-500{color:#ffc107 !important}.mdl-color--amber-500{background-color:#ffc107 !important}.mdl-color-text--amber-600{color:#ffb300 !important}.mdl-color--amber-600{background-color:#ffb300 !important}.mdl-color-text--amber-700{color:#ffa000 !important}.mdl-color--amber-700{background-color:#ffa000 !important}.mdl-color-text--amber-800{color:#ff8f00 !important}.mdl-color--amber-800{background-color:#ff8f00 !important}.mdl-color-text--amber-900{color:#ff6f00 !important}.mdl-color--amber-900{background-color:#ff6f00 !important}.mdl-color-text--amber-A100{color:#ffe57f !important}.mdl-color--amber-A100{background-color:#ffe57f !important}.mdl-color-text--amber-A200{color:#ffd740 !important}.mdl-color--amber-A200{background-color:#ffd740 !important}.mdl-color-text--amber-A400{color:#ffc400 !important}.mdl-color--amber-A400{background-color:#ffc400 !important}.mdl-color-text--amber-A700{color:#ffab00 !important}.mdl-color--amber-A700{background-color:#ffab00 !important}.mdl-color-text--orange{color:#ff9800 !important}.mdl-color--orange{background-color:#ff9800 !important}.mdl-color-text--orange-50{color:#fff3e0 !important}.mdl-color--orange-50{background-color:#fff3e0 !important}.mdl-color-text--orange-100{color:#ffe0b2 !important}.mdl-color--orange-100{background-color:#ffe0b2 !important}.mdl-color-text--orange-200{color:#ffcc80 !important}.mdl-color--orange-200{background-color:#ffcc80 !important}.mdl-color-text--orange-300{color:#ffb74d !important}.mdl-color--orange-300{background-color:#ffb74d !important}.mdl-color-text--orange-400{color:#ffa726 !important}.mdl-color--orange-400{background-color:#ffa726 !important}.mdl-color-text--orange-500{color:#ff9800 !important}.mdl-color--orange-500{background-color:#ff9800 !important}.mdl-color-text--orange-600{color:#fb8c00 !important}.mdl-color--orange-600{background-color:#fb8c00 !important}.mdl-color-text--orange-700{color:#f57c00 !important}.mdl-color--orange-700{background-color:#f57c00 !important}.mdl-color-text--orange-800{color:#ef6c00 !important}.mdl-color--orange-800{background-color:#ef6c00 !important}.mdl-color-text--orange-900{color:#e65100 !important}.mdl-color--orange-900{background-color:#e65100 !important}.mdl-color-text--orange-A100{color:#ffd180 !important}.mdl-color--orange-A100{background-color:#ffd180 !important}.mdl-color-text--orange-A200{color:#ffab40 !important}.mdl-color--orange-A200{background-color:#ffab40 !important}.mdl-color-text--orange-A400{color:#ff9100 !important}.mdl-color--orange-A400{background-color:#ff9100 !important}.mdl-color-text--orange-A700{color:#ff6d00 !important}.mdl-color--orange-A700{background-color:#ff6d00 !important}.mdl-color-text--deep-orange{color:#ff5722 !important}.mdl-color--deep-orange{background-color:#ff5722 !important}.mdl-color-text--deep-orange-50{color:#fbe9e7 !important}.mdl-color--deep-orange-50{background-color:#fbe9e7 !important}.mdl-color-text--deep-orange-100{color:#ffccbc !important}.mdl-color--deep-orange-100{background-color:#ffccbc !important}.mdl-color-text--deep-orange-200{color:#ffab91 !important}.mdl-color--deep-orange-200{background-color:#ffab91 !important}.mdl-color-text--deep-orange-300{color:#ff8a65 !important}.mdl-color--deep-orange-300{background-color:#ff8a65 !important}.mdl-color-text--deep-orange-400{color:#ff7043 !important}.mdl-color--deep-orange-400{background-color:#ff7043 !important}.mdl-color-text--deep-orange-500{color:#ff5722 !important}.mdl-color--deep-orange-500{background-color:#ff5722 !important}.mdl-color-text--deep-orange-600{color:#f4511e !important}.mdl-color--deep-orange-600{background-color:#f4511e !important}.mdl-color-text--deep-orange-700{color:#e64a19 !important}.mdl-color--deep-orange-700{background-color:#e64a19 !important}.mdl-color-text--deep-orange-800{color:#d84315 !important}.mdl-color--deep-orange-800{background-color:#d84315 !important}.mdl-color-text--deep-orange-900{color:#bf360c !important}.mdl-color--deep-orange-900{background-color:#bf360c !important}.mdl-color-text--deep-orange-A100{color:#ff9e80 !important}.mdl-color--deep-orange-A100{background-color:#ff9e80 !important}.mdl-color-text--deep-orange-A200{color:#ff6e40 !important}.mdl-color--deep-orange-A200{background-color:#ff6e40 !important}.mdl-color-text--deep-orange-A400{color:#ff3d00 !important}.mdl-color--deep-orange-A400{background-color:#ff3d00 !important}.mdl-color-text--deep-orange-A700{color:#dd2c00 !important}.mdl-color--deep-orange-A700{background-color:#dd2c00 !important}.mdl-color-text--brown{color:#795548 !important}.mdl-color--brown{background-color:#795548 !important}.mdl-color-text--brown-50{color:#efebe9 !important}.mdl-color--brown-50{background-color:#efebe9 !important}.mdl-color-text--brown-100{color:#d7ccc8 !important}.mdl-color--brown-100{background-color:#d7ccc8 !important}.mdl-color-text--brown-200{color:#bcaaa4 !important}.mdl-color--brown-200{background-color:#bcaaa4 !important}.mdl-color-text--brown-300{color:#a1887f !important}.mdl-color--brown-300{background-color:#a1887f !important}.mdl-color-text--brown-400{color:#8d6e63 !important}.mdl-color--brown-400{background-color:#8d6e63 !important}.mdl-color-text--brown-500{color:#795548 !important}.mdl-color--brown-500{background-color:#795548 !important}.mdl-color-text--brown-600{color:#6d4c41 !important}.mdl-color--brown-600{background-color:#6d4c41 !important}.mdl-color-text--brown-700{color:#5d4037 !important}.mdl-color--brown-700{background-color:#5d4037 !important}.mdl-color-text--brown-800{color:#4e342e !important}.mdl-color--brown-800{background-color:#4e342e !important}.mdl-color-text--brown-900{color:#3e2723 !important}.mdl-color--brown-900{background-color:#3e2723 !important}.mdl-color-text--grey{color:#9e9e9e !important}.mdl-color--grey{background-color:#9e9e9e !important}.mdl-color-text--grey-50{color:#fafafa !important}.mdl-color--grey-50{background-color:#fafafa !important}.mdl-color-text--grey-100{color:#f5f5f5 !important}.mdl-color--grey-100{background-color:#f5f5f5 !important}.mdl-color-text--grey-200{color:#eee !important}.mdl-color--grey-200{background-color:#eee !important}.mdl-color-text--grey-300{color:#e0e0e0 !important}.mdl-color--grey-300{background-color:#e0e0e0 !important}.mdl-color-text--grey-400{color:#bdbdbd !important}.mdl-color--grey-400{background-color:#bdbdbd !important}.mdl-color-text--grey-500{color:#9e9e9e !important}.mdl-color--grey-500{background-color:#9e9e9e !important}.mdl-color-text--grey-600{color:#757575 !important}.mdl-color--grey-600{background-color:#757575 !important}.mdl-color-text--grey-700{color:#616161 !important}.mdl-color--grey-700{background-color:#616161 !important}.mdl-color-text--grey-800{color:#424242 !important}.mdl-color--grey-800{background-color:#424242 !important}.mdl-color-text--grey-900{color:#212121 !important}.mdl-color--grey-900{background-color:#212121 !important}.mdl-color-text--blue-grey{color:#607d8b !important}.mdl-color--blue-grey{background-color:#607d8b !important}.mdl-color-text--blue-grey-50{color:#eceff1 !important}.mdl-color--blue-grey-50{background-color:#eceff1 !important}.mdl-color-text--blue-grey-100{color:#cfd8dc !important}.mdl-color--blue-grey-100{background-color:#cfd8dc !important}.mdl-color-text--blue-grey-200{color:#b0bec5 !important}.mdl-color--blue-grey-200{background-color:#b0bec5 !important}.mdl-color-text--blue-grey-300{color:#90a4ae !important}.mdl-color--blue-grey-300{background-color:#90a4ae !important}.mdl-color-text--blue-grey-400{color:#78909c !important}.mdl-color--blue-grey-400{background-color:#78909c !important}.mdl-color-text--blue-grey-500{color:#607d8b !important}.mdl-color--blue-grey-500{background-color:#607d8b !important}.mdl-color-text--blue-grey-600{color:#546e7a !important}.mdl-color--blue-grey-600{background-color:#546e7a !important}.mdl-color-text--blue-grey-700{color:#455a64 !important}.mdl-color--blue-grey-700{background-color:#455a64 !important}.mdl-color-text--blue-grey-800{color:#37474f !important}.mdl-color--blue-grey-800{background-color:#37474f !important}.mdl-color-text--blue-grey-900{color:#263238 !important}.mdl-color--blue-grey-900{background-color:#263238 !important}.mdl-color--black{background-color:#000 !important}.mdl-color-text--black{color:#000 !important}.mdl-color--white{background-color:#fff !important}.mdl-color-text--white{color:#fff !important}.mdl-color--primary{background-color:rgb(63,81,181)!important}.mdl-color--primary-contrast{background-color:rgb(255,255,255)!important}.mdl-color--primary-dark{background-color:rgb(48,63,159)!important}.mdl-color--accent{background-color:rgb(68,138,255)!important}.mdl-color--accent-contrast{background-color:rgb(255,255,255)!important}.mdl-color-text--primary{color:rgb(63,81,181)!important}.mdl-color-text--primary-contrast{color:rgb(255,255,255)!important}.mdl-color-text--primary-dark{color:rgb(48,63,159)!important}.mdl-color-text--accent{color:rgb(68,138,255)!important}.mdl-color-text--accent-contrast{color:rgb(255,255,255)!important}.mdl-ripple{background:#000;border-radius:50%;height:50px;left:0;opacity:0;pointer-events:none;position:absolute;top:0;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);width:50px;overflow:hidden}.mdl-ripple.is-animating{transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1);transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1),-webkit-transform .3s cubic-bezier(0,0,.2,1)}.mdl-ripple.is-visible{opacity:.3}.mdl-animation--default,.mdl-animation--fast-out-slow-in{transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-animation--linear-out-slow-in{transition-timing-function:cubic-bezier(0,0,.2,1)}.mdl-animation--fast-out-linear-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.mdl-badge{position:relative;white-space:nowrap;margin-right:24px}.mdl-badge:not([data-badge]){margin-right:auto}.mdl-badge[data-badge]:after{content:attr(data-badge);display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:center;-ms-flex-line-pack:center;align-content:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;position:absolute;top:-11px;right:-24px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:600;font-size:12px;width:22px;height:22px;border-radius:50%;background:rgb(68,138,255);color:rgb(255,255,255)}.mdl-button .mdl-badge[data-badge]:after{top:-10px;right:-5px}.mdl-badge.mdl-badge--no-background[data-badge]:after{color:rgb(68,138,255);background:rgba(255,255,255,.2);box-shadow:0 0 1px gray}.mdl-badge.mdl-badge--overlap{margin-right:10px}.mdl-badge.mdl-badge--overlap:after{right:-10px}.mdl-button{background:0 0;border:none;border-radius:2px;color:#000;position:relative;height:36px;margin:0;min-width:64px;padding:0 16px;display:inline-block;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;letter-spacing:0;overflow:hidden;will-change:box-shadow;transition:box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);outline:none;cursor:pointer;text-decoration:none;text-align:center;line-height:36px;vertical-align:middle}.mdl-button::-moz-focus-inner{border:0}.mdl-button:hover{background-color:rgba(158,158,158,.2)}.mdl-button:focus:not(:active){background-color:rgba(0,0,0,.12)}.mdl-button:active{background-color:rgba(158,158,158,.4)}.mdl-button.mdl-button--colored{color:rgb(63,81,181)}.mdl-button.mdl-button--colored:focus:not(:active){background-color:rgba(0,0,0,.12)}input.mdl-button[type="submit"]{-webkit-appearance:none}.mdl-button--raised{background:rgba(158,158,158,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-button--raised:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--raised:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--raised.mdl-button--colored{background:rgb(63,81,181);color:rgb(255,255,255)}.mdl-button--raised.mdl-button--colored:hover{background-color:rgb(63,81,181)}.mdl-button--raised.mdl-button--colored:active{background-color:rgb(63,81,181)}.mdl-button--raised.mdl-button--colored:focus:not(:active){background-color:rgb(63,81,181)}.mdl-button--raised.mdl-button--colored .mdl-ripple{background:rgb(255,255,255)}.mdl-button--fab{border-radius:50%;font-size:24px;height:56px;margin:auto;min-width:56px;width:56px;padding:0;overflow:hidden;background:rgba(158,158,158,.2);box-shadow:0 1px 1.5px 0 rgba(0,0,0,.12),0 1px 1px 0 rgba(0,0,0,.24);position:relative;line-height:normal}.mdl-button--fab .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--fab.mdl-button--mini-fab{height:40px;min-width:40px;width:40px}.mdl-button--fab .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button--fab:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--fab:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--fab.mdl-button--colored{background:rgb(68,138,255);color:rgb(255,255,255)}.mdl-button--fab.mdl-button--colored:hover{background-color:rgb(68,138,255)}.mdl-button--fab.mdl-button--colored:focus:not(:active){background-color:rgb(68,138,255)}.mdl-button--fab.mdl-button--colored:active{background-color:rgb(68,138,255)}.mdl-button--fab.mdl-button--colored .mdl-ripple{background:rgb(255,255,255)}.mdl-button--icon{border-radius:50%;font-size:24px;height:32px;margin-left:0;margin-right:0;min-width:32px;width:32px;padding:0;overflow:hidden;color:inherit;line-height:normal}.mdl-button--icon .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon{height:24px;min-width:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon .material-icons{top:0;left:0}.mdl-button--icon .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button__ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-button[disabled] .mdl-button__ripple-container .mdl-ripple,.mdl-button.mdl-button--disabled .mdl-button__ripple-container .mdl-ripple{background-color:transparent}.mdl-button--primary.mdl-button--primary{color:rgb(63,81,181)}.mdl-button--primary.mdl-button--primary .mdl-ripple{background:rgb(255,255,255)}.mdl-button--primary.mdl-button--primary.mdl-button--raised,.mdl-button--primary.mdl-button--primary.mdl-button--fab{color:rgb(255,255,255);background-color:rgb(63,81,181)}.mdl-button--accent.mdl-button--accent{color:rgb(68,138,255)}.mdl-button--accent.mdl-button--accent .mdl-ripple{background:rgb(255,255,255)}.mdl-button--accent.mdl-button--accent.mdl-button--raised,.mdl-button--accent.mdl-button--accent.mdl-button--fab{color:rgb(255,255,255);background-color:rgb(68,138,255)}.mdl-button[disabled][disabled],.mdl-button.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26);cursor:default;background-color:transparent}.mdl-button--fab[disabled][disabled],.mdl-button--fab.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-button--raised[disabled][disabled],.mdl-button--raised.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26);box-shadow:none}.mdl-button--colored[disabled][disabled],.mdl-button--colored.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26)}.mdl-button .material-icons{vertical-align:middle}.mdl-card{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;font-size:16px;font-weight:400;min-height:200px;overflow:hidden;width:330px;z-index:1;position:relative;background:#fff;border-radius:2px;box-sizing:border-box}.mdl-card__media{background-color:rgb(68,138,255);background-repeat:repeat;background-position:50% 50%;background-size:cover;background-origin:padding-box;background-attachment:scroll;box-sizing:border-box}.mdl-card__title{-webkit-align-items:center;-ms-flex-align:center;align-items:center;color:#000;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:stretch;-ms-flex-pack:stretch;justify-content:stretch;line-height:normal;padding:16px;-webkit-perspective-origin:165px 56px;perspective-origin:165px 56px;-webkit-transform-origin:165px 56px;transform-origin:165px 56px;box-sizing:border-box}.mdl-card__title.mdl-card--border{border-bottom:1px solid rgba(0,0,0,.1)}.mdl-card__title-text{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end;color:inherit;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:24px;font-weight:300;line-height:normal;overflow:hidden;-webkit-transform-origin:149px 48px;transform-origin:149px 48px;margin:0}.mdl-card__subtitle-text{font-size:14px;color:rgba(0,0,0,.54);margin:0}.mdl-card__supporting-text{color:rgba(0,0,0,.54);font-size:1rem;line-height:18px;overflow:hidden;padding:16px;width:90%}.mdl-card__supporting-text.mdl-card--border{border-bottom:1px solid rgba(0,0,0,.1)}.mdl-card__actions{font-size:16px;line-height:normal;width:100%;background-color:transparent;padding:8px;box-sizing:border-box}.mdl-card__actions.mdl-card--border{border-top:1px solid rgba(0,0,0,.1)}.mdl-card--expand{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-card__menu{position:absolute;right:16px;top:16px}.mdl-checkbox{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0}.mdl-checkbox.is-upgraded{padding-left:24px}.mdl-checkbox__input{line-height:24px}.mdl-checkbox.is-upgraded .mdl-checkbox__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-checkbox__box-outline{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;overflow:hidden;border:2px solid rgba(0,0,0,.54);border-radius:2px;z-index:2}.mdl-checkbox.is-checked .mdl-checkbox__box-outline{border:2px solid rgb(63,81,181)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__box-outline,.mdl-checkbox.is-disabled .mdl-checkbox__box-outline{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__focus-helper{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;border-radius:50%;background-color:transparent}.mdl-checkbox.is-focused .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-checkbox.is-focused.is-checked .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(63,81,181,.26);background-color:rgba(63,81,181,.26)}.mdl-checkbox__tick-outline{position:absolute;top:0;left:0;height:100%;width:100%;-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");background:0 0;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background}.mdl-checkbox.is-checked .mdl-checkbox__tick-outline{background:rgb(63,81,181)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}fieldset[disabled] .mdl-checkbox.is-checked .mdl-checkbox__tick-outline,.mdl-checkbox.is-checked.is-disabled .mdl-checkbox__tick-outline{background:rgba(0,0,0,.26)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}.mdl-checkbox__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0}fieldset[disabled] .mdl-checkbox .mdl-checkbox__label,.mdl-checkbox.is-disabled .mdl-checkbox__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__ripple-container{position:absolute;z-index:2;top:-6px;left:-10px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-checkbox__ripple-container .mdl-ripple{background:rgb(63,81,181)}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container{cursor:auto}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container .mdl-ripple,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container .mdl-ripple{background:0 0}.mdl-chip{height:32px;font-family:"Roboto","Helvetica","Arial",sans-serif;line-height:32px;padding:0 12px;border:0;border-radius:16px;background-color:#dedede;display:inline-block;color:rgba(0,0,0,.87);margin:2px 0;font-size:0;white-space:nowrap}.mdl-chip__text{font-size:13px;vertical-align:middle;display:inline-block}.mdl-chip__action{height:24px;width:24px;background:0 0;opacity:.54;cursor:pointer;padding:0;margin:0 0 0 4px;font-size:13px;text-decoration:none;color:rgba(0,0,0,.87);border:none;outline:none}.mdl-chip__action,.mdl-chip__contact{display:inline-block;vertical-align:middle;overflow:hidden;text-align:center}.mdl-chip__contact{height:32px;width:32px;border-radius:16px;margin-right:8px;font-size:18px;line-height:32px}.mdl-chip:focus{outline:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-chip:active{background-color:#d6d6d6}.mdl-chip--deletable{padding-right:4px}.mdl-chip--contact{padding-left:0}.mdl-data-table{position:relative;border:1px solid rgba(0,0,0,.12);border-collapse:collapse;white-space:nowrap;font-size:13px;background-color:#fff}.mdl-data-table thead{padding-bottom:3px}.mdl-data-table thead .mdl-data-table__select{margin-top:0}.mdl-data-table tbody tr{position:relative;height:48px;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background-color}.mdl-data-table tbody tr.is-selected{background-color:#e0e0e0}.mdl-data-table tbody tr:hover{background-color:#eee}.mdl-data-table td{text-align:right}.mdl-data-table th{padding:0 18px 12px 18px;text-align:right}.mdl-data-table td:first-of-type,.mdl-data-table th:first-of-type{padding-left:24px}.mdl-data-table td:last-of-type,.mdl-data-table th:last-of-type{padding-right:24px}.mdl-data-table td{position:relative;height:48px;border-top:1px solid rgba(0,0,0,.12);border-bottom:1px solid rgba(0,0,0,.12);padding:12px 18px;box-sizing:border-box}.mdl-data-table td,.mdl-data-table td .mdl-data-table__select{vertical-align:middle}.mdl-data-table th{position:relative;vertical-align:bottom;text-overflow:ellipsis;font-weight:700;line-height:24px;letter-spacing:0;height:48px;font-size:12px;color:rgba(0,0,0,.54);padding-bottom:8px;box-sizing:border-box}.mdl-data-table th.mdl-data-table__header--sorted-ascending,.mdl-data-table th.mdl-data-table__header--sorted-descending{color:rgba(0,0,0,.87)}.mdl-data-table th.mdl-data-table__header--sorted-ascending:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:before{font-family:'Material Icons';font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased;font-size:16px;content:"\e5d8";margin-right:5px;vertical-align:sub}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover{cursor:pointer}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover:before{color:rgba(0,0,0,.26)}.mdl-data-table th.mdl-data-table__header--sorted-descending:before{content:"\e5db"}.mdl-data-table__select{width:16px}.mdl-data-table__cell--non-numeric.mdl-data-table__cell--non-numeric{text-align:left}.mdl-dialog{border:none;box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2);width:280px}.mdl-dialog__title{padding:24px 24px 0;margin:0;font-size:2.5rem}.mdl-dialog__actions{padding:8px 8px 8px 24px;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row-reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap}.mdl-dialog__actions>*{margin-right:8px;height:36px}.mdl-dialog__actions>*:first-child{margin-right:0}.mdl-dialog__actions--full-width{padding:0 0 8px}.mdl-dialog__actions--full-width>*{height:48px;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;padding-right:16px;margin-right:0;text-align:right}.mdl-dialog__content{padding:20px 24px 24px;color:rgba(0,0,0,.54)}.mdl-mega-footer{padding:16px 40px;color:#9e9e9e;background-color:#424242}.mdl-mega-footer--top-section:after,.mdl-mega-footer--middle-section:after,.mdl-mega-footer--bottom-section:after,.mdl-mega-footer__top-section:after,.mdl-mega-footer__middle-section:after,.mdl-mega-footer__bottom-section:after{content:'';display:block;clear:both}.mdl-mega-footer--left-section,.mdl-mega-footer__left-section,.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{margin-bottom:16px}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:block;margin-bottom:16px;color:inherit;text-decoration:none}@media screen and (min-width:760px){.mdl-mega-footer--left-section,.mdl-mega-footer__left-section{float:left}.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{float:right}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:inline-block;margin-left:16px;line-height:36px;vertical-align:middle}}.mdl-mega-footer--social-btn,.mdl-mega-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{display:block;position:relative}@media screen and (min-width:760px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{width:33%}.mdl-mega-footer--drop-down-section:nth-child(1),.mdl-mega-footer--drop-down-section:nth-child(2),.mdl-mega-footer__drop-down-section:nth-child(1),.mdl-mega-footer__drop-down-section:nth-child(2){float:left}.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(3){float:right}.mdl-mega-footer--drop-down-section:nth-child(3):after,.mdl-mega-footer__drop-down-section:nth-child(3):after{clear:right}.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section:nth-child(4){clear:right;float:right}.mdl-mega-footer--middle-section:after,.mdl-mega-footer__middle-section:after{content:'';display:block;clear:both}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:0}}@media screen and (min-width:1024px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section,.mdl-mega-footer__drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(4){width:24%;float:left}}.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{position:absolute;width:100%;height:55.8px;padding:32px;margin:-16px 0 0;cursor:pointer;z-index:1;opacity:0}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CE'}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list{display:none}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CF'}.mdl-mega-footer--heading,.mdl-mega-footer__heading{position:relative;width:100%;padding-right:39.8px;margin-bottom:16px;box-sizing:border-box;font-size:14px;line-height:23.8px;font-weight:500;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;color:#e0e0e0}.mdl-mega-footer--heading:after,.mdl-mega-footer__heading:after{content:'';position:absolute;top:0;right:0;display:block;width:23.8px;height:23.8px;background-size:cover}.mdl-mega-footer--link-list,.mdl-mega-footer__link-list{list-style:none;padding:0;margin:0 0 32px}.mdl-mega-footer--link-list:after,.mdl-mega-footer__link-list:after{clear:both;display:block;content:''}.mdl-mega-footer--link-list li,.mdl-mega-footer__link-list li{font-size:14px;font-weight:400;letter-spacing:0;line-height:20px}.mdl-mega-footer--link-list a,.mdl-mega-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}@media screen and (min-width:760px){.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{display:none}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{content:''}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list{display:block}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{content:''}}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:16px;margin-bottom:16px}.mdl-logo{margin-bottom:16px;color:#fff}.mdl-mega-footer--bottom-section .mdl-mega-footer--link-list li,.mdl-mega-footer__bottom-section .mdl-mega-footer__link-list li{float:left;margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-logo{float:left;margin-bottom:0;margin-right:16px}}.mdl-mini-footer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:32px 16px;color:#9e9e9e;background-color:#424242}.mdl-mini-footer:after{content:'';display:block}.mdl-mini-footer .mdl-logo{line-height:36px}.mdl-mini-footer--link-list,.mdl-mini-footer__link-list{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;list-style:none;margin:0;padding:0}.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{line-height:36px}}.mdl-mini-footer--link-list a,.mdl-mini-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}.mdl-mini-footer--left-section,.mdl-mini-footer__left-section{display:inline-block;-webkit-order:0;-ms-flex-order:0;order:0}.mdl-mini-footer--right-section,.mdl-mini-footer__right-section{display:inline-block;-webkit-order:1;-ms-flex-order:1;order:1}.mdl-mini-footer--social-btn,.mdl-mini-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-icon-toggle{position:relative;z-index:1;vertical-align:middle;display:inline-block;height:32px;margin:0;padding:0}.mdl-icon-toggle__input{line-height:32px}.mdl-icon-toggle.is-upgraded .mdl-icon-toggle__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-icon-toggle__label{display:inline-block;position:relative;cursor:pointer;height:32px;width:32px;min-width:32px;color:#616161;border-radius:50%;padding:0;margin-left:0;margin-right:0;text-align:center;background-color:transparent;will-change:background-color;transition:background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1)}.mdl-icon-toggle__label.material-icons{line-height:32px;font-size:24px}.mdl-icon-toggle.is-checked .mdl-icon-toggle__label{color:rgb(63,81,181)}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__label{color:rgba(0,0,0,.26);cursor:auto;transition:none}.mdl-icon-toggle.is-focused .mdl-icon-toggle__label{background-color:rgba(0,0,0,.12)}.mdl-icon-toggle.is-focused.is-checked .mdl-icon-toggle__label{background-color:rgba(63,81,181,.26)}.mdl-icon-toggle__ripple-container{position:absolute;z-index:2;top:-2px;left:-2px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-icon-toggle__ripple-container .mdl-ripple{background:#616161}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container{cursor:auto}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container .mdl-ripple{background:0 0}.mdl-list{display:block;padding:8px 0;list-style:none}.mdl-list__item{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;letter-spacing:.04em;line-height:1;min-height:48px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;padding:16px;cursor:default;color:rgba(0,0,0,.87);overflow:hidden}.mdl-list__item,.mdl-list__item .mdl-list__item-primary-content{box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.mdl-list__item .mdl-list__item-primary-content{-webkit-order:0;-ms-flex-order:0;order:0;-webkit-flex-grow:2;-ms-flex-positive:2;flex-grow:2;text-decoration:none}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-icon{margin-right:32px}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-avatar{margin-right:16px}.mdl-list__item .mdl-list__item-secondary-content{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:column;-ms-flex-flow:column;flex-flow:column;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end;margin-left:16px}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-action label{display:inline}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-info{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;color:rgba(0,0,0,.54)}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-sub-header{padding:0 0 0 16px}.mdl-list__item-icon,.mdl-list__item-icon.material-icons{height:24px;width:24px;font-size:24px;box-sizing:border-box;color:#757575}.mdl-list__item-avatar,.mdl-list__item-avatar.material-icons{height:40px;width:40px;box-sizing:border-box;border-radius:50%;background-color:#757575;font-size:40px;color:#fff}.mdl-list__item--two-line{height:72px}.mdl-list__item--two-line .mdl-list__item-primary-content{height:36px;line-height:20px;display:block}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-avatar{float:left}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left;margin-top:6px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-secondary-content{height:36px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-sub-title{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-list__item--three-line{height:88px}.mdl-list__item--three-line .mdl-list__item-primary-content{height:52px;line-height:20px;display:block}.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-avatar,.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left}.mdl-list__item--three-line .mdl-list__item-secondary-content{height:52px}.mdl-list__item--three-line .mdl-list__item-text-body{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;height:52px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-menu__container{display:block;margin:0;padding:0;border:none;position:absolute;overflow:visible;height:0;width:0;visibility:hidden;z-index:-1}.mdl-menu__container.is-visible,.mdl-menu__container.is-animating{z-index:999;visibility:visible}.mdl-menu__outline{display:block;background:#fff;margin:0;padding:0;border:none;border-radius:2px;position:absolute;top:0;left:0;overflow:hidden;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:0 0;transform-origin:0 0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);will-change:transform;transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1);transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1),-webkit-transform .3s cubic-bezier(.4,0,.2,1);z-index:-1}.mdl-menu__container.is-visible .mdl-menu__outline{opacity:1;-webkit-transform:scale(1);transform:scale(1);z-index:999}.mdl-menu__outline.mdl-menu--bottom-right{-webkit-transform-origin:100% 0;transform-origin:100% 0}.mdl-menu__outline.mdl-menu--top-left{-webkit-transform-origin:0 100%;transform-origin:0 100%}.mdl-menu__outline.mdl-menu--top-right{-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.mdl-menu{position:absolute;list-style:none;top:0;left:0;height:auto;width:auto;min-width:124px;padding:8px 0;margin:0;opacity:0;clip:rect(0 0 0 0);z-index:-1}.mdl-menu__container.is-visible .mdl-menu{opacity:1;z-index:999}.mdl-menu.is-animating{transition:opacity .2s cubic-bezier(.4,0,.2,1),clip .3s cubic-bezier(.4,0,.2,1)}.mdl-menu.mdl-menu--bottom-right{left:auto;right:0}.mdl-menu.mdl-menu--top-left{top:auto;bottom:0}.mdl-menu.mdl-menu--top-right{top:auto;left:auto;bottom:0;right:0}.mdl-menu.mdl-menu--unaligned{top:auto;left:auto}.mdl-menu__item{display:block;border:none;color:rgba(0,0,0,.87);background-color:transparent;text-align:left;margin:0;padding:0 16px;outline-color:#bdbdbd;position:relative;overflow:hidden;font-size:14px;font-weight:400;letter-spacing:0;text-decoration:none;cursor:pointer;height:48px;line-height:48px;white-space:nowrap;opacity:0;transition:opacity .2s cubic-bezier(.4,0,.2,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-menu__container.is-visible .mdl-menu__item{opacity:1}.mdl-menu__item::-moz-focus-inner{border:0}.mdl-menu__item--full-bleed-divider{border-bottom:1px solid rgba(0,0,0,.12)}.mdl-menu__item[disabled],.mdl-menu__item[data-mdl-disabled]{color:#bdbdbd;background-color:transparent;cursor:auto}.mdl-menu__item[disabled]:hover,.mdl-menu__item[data-mdl-disabled]:hover{background-color:transparent}.mdl-menu__item[disabled]:focus,.mdl-menu__item[data-mdl-disabled]:focus{background-color:transparent}.mdl-menu__item[disabled] .mdl-ripple,.mdl-menu__item[data-mdl-disabled] .mdl-ripple{background:0 0}.mdl-menu__item:hover{background-color:#eee}.mdl-menu__item:focus{outline:none;background-color:#eee}.mdl-menu__item:active{background-color:#e0e0e0}.mdl-menu__item--ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-progress{display:block;position:relative;height:4px;width:500px;max-width:100%}.mdl-progress>.bar{display:block;position:absolute;top:0;bottom:0;width:0%;transition:width .2s cubic-bezier(.4,0,.2,1)}.mdl-progress>.progressbar{background-color:rgb(63,81,181);z-index:1;left:0}.mdl-progress>.bufferbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,rgb(63,81,181),rgb(63,81,181));z-index:0;left:0}.mdl-progress>.auxbar{right:0}@supports (-webkit-appearance:none){.mdl-progress:not(.mdl-progress--indeterminate):not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate):not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,rgb(63,81,181),rgb(63,81,181));-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=")}}.mdl-progress:not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.9),rgba(255,255,255,.9)),linear-gradient(to right,rgb(63,81,181),rgb(63,81,181))}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1{-webkit-animation-name:indeterminate1;animation-name:indeterminate1}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1,.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-color:rgb(63,81,181);-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-image:none;-webkit-animation-name:indeterminate2;animation-name:indeterminate2}@-webkit-keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@-webkit-keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}@keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}.mdl-navigation{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;box-sizing:border-box}.mdl-navigation__link{color:#424242;text-decoration:none;margin:0;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0;opacity:.87}.mdl-navigation__link .material-icons{vertical-align:middle}.mdl-layout{width:100%;height:100%;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;overflow-y:auto;overflow-x:hidden;position:relative;-webkit-overflow-scrolling:touch}.mdl-layout.is-small-screen .mdl-layout--large-screen-only{display:none}.mdl-layout:not(.is-small-screen) .mdl-layout--small-screen-only{display:none}.mdl-layout__container{position:absolute;width:100%;height:100%}.mdl-layout__title,.mdl-layout-title{display:block;position:relative;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;line-height:1;letter-spacing:.02em;font-weight:400;box-sizing:border-box}.mdl-layout-spacer{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-layout__drawer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;width:240px;height:100%;max-height:100%;position:absolute;top:0;left:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);box-sizing:border-box;border-right:1px solid #e0e0e0;background:#fafafa;-webkit-transform:translateX(-250px);transform:translateX(-250px);-webkit-transform-style:preserve-3d;transform-style:preserve-3d;will-change:transform;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;color:#424242;overflow:visible;overflow-y:auto;z-index:5}.mdl-layout__drawer.is-visible{-webkit-transform:translateX(0);transform:translateX(0)}.mdl-layout__drawer.is-visible~.mdl-layout__content.mdl-layout__content{overflow:hidden}.mdl-layout__drawer>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:64px;padding-left:40px}@media screen and (max-width:1024px){.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:56px;padding-left:16px}}.mdl-layout__drawer .mdl-navigation{-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-align-items:stretch;-ms-flex-align:stretch;align-items:stretch;padding-top:16px}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{display:block;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding:16px 40px;margin:0;color:#757575}@media screen and (max-width:1024px){.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{padding:16px}}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link:hover{background-color:#e0e0e0}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link--current{background-color:#e0e0e0;color:#000}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__drawer{-webkit-transform:translateX(0);transform:translateX(0)}}.mdl-layout__drawer-button{display:block;position:absolute;height:48px;width:48px;border:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden;text-align:center;cursor:pointer;font-size:26px;line-height:56px;font-family:Helvetica,Arial,sans-serif;margin:8px 12px;top:0;left:0;color:rgb(255,255,255);z-index:4}.mdl-layout__header .mdl-layout__drawer-button{position:absolute;color:rgb(255,255,255);background-color:inherit}@media screen and (max-width:1024px){.mdl-layout__header .mdl-layout__drawer-button{margin:4px}}@media screen and (max-width:1024px){.mdl-layout__drawer-button{margin:4px;color:rgba(0,0,0,.5)}}@media screen and (min-width:1025px){.mdl-layout__drawer-button{line-height:54px}.mdl-layout--no-desktop-drawer-button .mdl-layout__drawer-button,.mdl-layout--fixed-drawer>.mdl-layout__drawer-button,.mdl-layout--no-drawer-button .mdl-layout__drawer-button{display:none}}.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;box-sizing:border-box;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;width:100%;margin:0;padding:0;border:none;min-height:64px;max-height:1000px;z-index:3;background-color:rgb(63,81,181);color:rgb(255,255,255);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:max-height,box-shadow}@media screen and (max-width:1024px){.mdl-layout__header{min-height:56px}}.mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen)>.mdl-layout__header{margin-left:240px;width:calc(100% - 240px)}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__header .mdl-layout__header-row{padding-left:40px}}.mdl-layout__header>.mdl-layout-icon{position:absolute;left:40px;top:16px;height:32px;width:32px;overflow:hidden;z-index:3;display:block}@media screen and (max-width:1024px){.mdl-layout__header>.mdl-layout-icon{left:16px;top:12px}}.mdl-layout.has-drawer .mdl-layout__header>.mdl-layout-icon{display:none}.mdl-layout__header.is-compact{max-height:64px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact{max-height:56px}}.mdl-layout__header.is-compact.has-tabs{height:112px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact.has-tabs{min-height:104px}}@media screen and (max-width:1024px){.mdl-layout__header{display:none}.mdl-layout--fixed-header>.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex}}.mdl-layout__header--transparent.mdl-layout__header--transparent{background-color:transparent;box-shadow:none}.mdl-layout__header--seamed,.mdl-layout__header--scroll{box-shadow:none}.mdl-layout__header--waterfall{box-shadow:none;overflow:hidden}.mdl-layout__header--waterfall.is-casting-shadow{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-layout__header--waterfall.mdl-layout__header--waterfall-hide-top{-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}.mdl-layout__header-row{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;box-sizing:border-box;-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:64px;margin:0;padding:0 40px 0 80px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:40px}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__header-row{padding-left:40px}}@media screen and (max-width:1024px){.mdl-layout__header-row{height:56px;padding:0 16px 0 72px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:16px}}.mdl-layout__header-row>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__header--scroll .mdl-layout__header-row{width:100%}.mdl-layout__header-row .mdl-navigation{margin:0;padding:0;height:64px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-ms-flex-align:center;align-items:center}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation{height:56px}}.mdl-layout__header-row .mdl-navigation__link{display:block;color:rgb(255,255,255);line-height:64px;padding:0 24px}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation__link{line-height:56px;padding:0 16px}}.mdl-layout__obfuscator{background-color:transparent;position:absolute;top:0;left:0;height:100%;width:100%;z-index:4;visibility:hidden;transition-property:background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-layout__obfuscator.is-visible{background-color:rgba(0,0,0,.5);visibility:visible}@supports (pointer-events:auto){.mdl-layout__obfuscator{background-color:rgba(0,0,0,.5);opacity:0;transition-property:opacity;visibility:visible;pointer-events:none}.mdl-layout__obfuscator.is-visible{pointer-events:auto;opacity:1}}.mdl-layout__content{-ms-flex:0 1 auto;position:relative;display:inline-block;overflow-y:auto;overflow-x:hidden;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;z-index:1;-webkit-overflow-scrolling:touch}.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:240px}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow:visible}@media screen and (max-width:1024px){.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:0}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow-y:auto;overflow-x:hidden}}.mdl-layout__tab-bar{height:96px;margin:0;width:calc(100% - 112px);padding:0 0 0 56px;display:-webkit-flex;display:-ms-flexbox;display:flex;background-color:rgb(63,81,181);overflow-y:hidden;overflow-x:scroll}.mdl-layout__tab-bar::-webkit-scrollbar{display:none}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}}@media screen and (max-width:1024px){.mdl-layout__tab-bar{width:calc(100% - 60px);padding:0 0 0 60px}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{width:calc(100% - 8px);padding-left:4px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar{padding:0;overflow:hidden;width:100%}.mdl-layout__tab-bar-container{position:relative;height:48px;width:100%;border:none;margin:0;z-index:2;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden}.mdl-layout__container>.mdl-layout__tab-bar-container{position:absolute;top:0;left:0}.mdl-layout__tab-bar-button{display:inline-block;position:absolute;top:0;height:48px;width:56px;z-index:4;text-align:center;background-color:rgb(63,81,181);color:transparent;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button{width:16px}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button .material-icons,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button .material-icons{position:relative;left:-4px}@media screen and (max-width:1024px){.mdl-layout__tab-bar-button{width:60px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar-button{display:none}.mdl-layout__tab-bar-button .material-icons{line-height:48px}.mdl-layout__tab-bar-button.is-active{color:rgb(255,255,255)}.mdl-layout__tab-bar-left-button{left:0}.mdl-layout__tab-bar-right-button{right:0}.mdl-layout__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(255,255,255,.6);overflow:hidden}@media screen and (max-width:1024px){.mdl-layout__tab{padding:0 12px}}.mdl-layout--fixed-tabs .mdl-layout__tab{float:none;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;padding:0}.mdl-layout.is-upgraded .mdl-layout__tab.is-active{color:rgb(255,255,255)}.mdl-layout.is-upgraded .mdl-layout__tab.is-active::after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:rgb(68,138,255);-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-layout__tab .mdl-layout__tab-ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-layout__tab .mdl-layout__tab-ripple-container .mdl-ripple{background-color:rgb(255,255,255)}.mdl-layout__tab-panel{display:block}.mdl-layout.is-upgraded .mdl-layout__tab-panel{display:none}.mdl-layout.is-upgraded .mdl-layout__tab-panel.is-active{display:block}.mdl-radio{position:relative;font-size:16px;line-height:24px;display:inline-block;vertical-align:middle;box-sizing:border-box;height:24px;margin:0;padding-left:0}.mdl-radio.is-upgraded{padding-left:24px}.mdl-radio__button{line-height:24px}.mdl-radio.is-upgraded .mdl-radio__button{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-radio__outer-circle{position:absolute;top:4px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;border:2px solid rgba(0,0,0,.54);border-radius:50%;z-index:2}.mdl-radio.is-checked .mdl-radio__outer-circle{border:2px solid rgb(63,81,181)}.mdl-radio__outer-circle fieldset[disabled] .mdl-radio,.mdl-radio.is-disabled .mdl-radio__outer-circle{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-radio__inner-circle{position:absolute;z-index:1;margin:0;top:8px;left:4px;box-sizing:border-box;width:8px;height:8px;cursor:pointer;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;-webkit-transform:scale(0,0);transform:scale(0,0);border-radius:50%;background:rgb(63,81,181)}.mdl-radio.is-checked .mdl-radio__inner-circle{-webkit-transform:scale(1,1);transform:scale(1,1)}fieldset[disabled] .mdl-radio .mdl-radio__inner-circle,.mdl-radio.is-disabled .mdl-radio__inner-circle{background:rgba(0,0,0,.26);cursor:auto}.mdl-radio.is-focused .mdl-radio__inner-circle{box-shadow:0 0 0 10px rgba(0,0,0,.1)}.mdl-radio__label{cursor:pointer}fieldset[disabled] .mdl-radio .mdl-radio__label,.mdl-radio.is-disabled .mdl-radio__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-radio__ripple-container{position:absolute;z-index:2;top:-9px;left:-13px;box-sizing:border-box;width:42px;height:42px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-radio__ripple-container .mdl-ripple{background:rgb(63,81,181)}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container,.mdl-radio.is-disabled .mdl-radio__ripple-container{cursor:auto}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container .mdl-ripple,.mdl-radio.is-disabled .mdl-radio__ripple-container .mdl-ripple{background:0 0}_:-ms-input-placeholder,:root .mdl-slider.mdl-slider.is-upgraded{-ms-appearance:none;height:32px;margin:0}.mdl-slider{width:calc(100% - 40px);margin:0 20px}.mdl-slider.is-upgraded{-webkit-appearance:none;-moz-appearance:none;appearance:none;height:2px;background:0 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;outline:0;padding:0;color:rgb(63,81,181);-webkit-align-self:center;-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center;z-index:1;cursor:pointer}.mdl-slider.is-upgraded::-moz-focus-outer{border:0}.mdl-slider.is-upgraded::-ms-tooltip{display:none}.mdl-slider.is-upgraded::-webkit-slider-runnable-track{background:0 0}.mdl-slider.is-upgraded::-moz-range-track{background:0 0;border:none}.mdl-slider.is-upgraded::-ms-track{background:0 0;color:transparent;height:2px;width:100%;border:none}.mdl-slider.is-upgraded::-ms-fill-lower{padding:0;background:linear-gradient(to right,transparent,transparent 16px,rgb(63,81,181)16px,rgb(63,81,181)0)}.mdl-slider.is-upgraded::-ms-fill-upper{padding:0;background:linear-gradient(to left,transparent,transparent 16px,rgba(0,0,0,.26)16px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background:rgb(63,81,181);border:none;transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded::-moz-range-thumb{-moz-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background-image:none;background:rgb(63,81,181);border:none}.mdl-slider.is-upgraded:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(63,81,181,.26)}.mdl-slider.is-upgraded:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(63,81,181,.26)}.mdl-slider.is-upgraded:active::-webkit-slider-thumb{background-image:none;background:rgb(63,81,181);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded:active::-moz-range-thumb{background-image:none;background:rgb(63,81,181);transform:scale(1.5)}.mdl-slider.is-upgraded::-ms-thumb{width:32px;height:32px;border:none;border-radius:50%;background:rgb(63,81,181);transform:scale(.375);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgb(63,81,181)0%,rgb(63,81,181)37.5%,rgba(63,81,181,.26)37.5%,rgba(63,81,181,.26)100%);transform:scale(1)}.mdl-slider.is-upgraded:active::-ms-thumb{background:rgb(63,81,181);transform:scale(.5625)}.mdl-slider.is-upgraded.is-lowest-value::-webkit-slider-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-moz-range-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:active::-webkit-slider-thumb{border:1.6px solid rgba(0,0,0,.26);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:9px}.mdl-slider.is-upgraded.is-lowest-value:active::-moz-range-thumb{border:1.5px solid rgba(0,0,0,.26);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value::-ms-thumb{background:radial-gradient(circle closest-side,transparent 0%,transparent 66.67%,rgba(0,0,0,.26)66.67%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgba(0,0,0,.12)0%,rgba(0,0,0,.12)25%,rgba(0,0,0,.26)25%,rgba(0,0,0,.26)37.5%,rgba(0,0,0,.12)37.5%,rgba(0,0,0,.12)100%);transform:scale(1)}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-thumb{transform:scale(.5625);background:radial-gradient(circle closest-side,transparent 0%,transparent 77.78%,rgba(0,0,0,.26)77.78%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-lower{background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-fill-upper{margin-left:9px}.mdl-slider.is-upgraded:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled::-webkit-slider-thumb{-webkit-transform:scale(.667);transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded:disabled::-moz-range-thumb{transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-lower{background-color:rgba(0,0,0,.26);left:-6px}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-webkit-slider-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;-webkit-transform:scale(.667);transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-moz-range-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded:disabled:active::-ms-thumb,.mdl-slider.is-upgraded:disabled::-ms-thumb{transform:scale(.25);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-ms-thumb{transform:scale(.25);background:radial-gradient(circle closest-side,transparent 0%,transparent 50%,rgba(0,0,0,.26)50%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded:disabled::-ms-fill-lower{margin-right:6px;background:linear-gradient(to right,transparent,transparent 25px,rgba(0,0,0,.26)25px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded:disabled::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-fill-upper{margin-left:6px}.mdl-slider__ie-container{height:18px;overflow:visible;border:none;margin:none;padding:none}.mdl-slider__container{height:18px;position:relative;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.mdl-slider__container,.mdl-slider__background-flex{background:0 0;display:-webkit-flex;display:-ms-flexbox;display:flex}.mdl-slider__background-flex{position:absolute;height:2px;width:calc(100% - 52px);top:50%;left:0;margin:0 26px;overflow:hidden;border:0;padding:0;-webkit-transform:translate(0,-1px);transform:translate(0,-1px)}.mdl-slider__background-lower{background:rgb(63,81,181)}.mdl-slider__background-lower,.mdl-slider__background-upper{-webkit-flex:0;-ms-flex:0;flex:0;position:relative;border:0;padding:0}.mdl-slider__background-upper{background:rgba(0,0,0,.26);transition:left .18s cubic-bezier(.4,0,.2,1)}.mdl-snackbar{position:fixed;bottom:0;left:50%;cursor:default;background-color:#323232;z-index:3;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;font-family:"Roboto","Helvetica","Arial",sans-serif;will-change:transform;-webkit-transform:translate(0,80px);transform:translate(0,80px);transition:transform .25s cubic-bezier(.4,0,1,1);transition:transform .25s cubic-bezier(.4,0,1,1),-webkit-transform .25s cubic-bezier(.4,0,1,1);pointer-events:none}@media (max-width:479px){.mdl-snackbar{width:100%;left:0;min-height:48px;max-height:80px}}@media (min-width:480px){.mdl-snackbar{min-width:288px;max-width:568px;border-radius:2px;-webkit-transform:translate(-50%,80px);transform:translate(-50%,80px)}}.mdl-snackbar--active{-webkit-transform:translate(0,0);transform:translate(0,0);pointer-events:auto;transition:transform .25s cubic-bezier(0,0,.2,1);transition:transform .25s cubic-bezier(0,0,.2,1),-webkit-transform .25s cubic-bezier(0,0,.2,1)}@media (min-width:480px){.mdl-snackbar--active{-webkit-transform:translate(-50%,0);transform:translate(-50%,0)}}.mdl-snackbar__text{padding:14px 12px 14px 24px;vertical-align:middle;color:#fff;float:left}.mdl-snackbar__action{background:0 0;border:none;color:rgb(68,138,255);float:right;padding:14px 24px 14px 12px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;line-height:1;letter-spacing:0;overflow:hidden;outline:none;opacity:0;pointer-events:none;cursor:pointer;text-decoration:none;text-align:center;-webkit-align-self:center;-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center}.mdl-snackbar__action::-moz-focus-inner{border:0}.mdl-snackbar__action:not([aria-hidden]){opacity:1;pointer-events:auto}.mdl-spinner{display:inline-block;position:relative;width:28px;height:28px}.mdl-spinner:not(.is-upgraded).is-active:after{content:"Loading..."}.mdl-spinner.is-upgraded.is-active{-webkit-animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite;animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite}@-webkit-keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.mdl-spinner__layer{position:absolute;width:100%;height:100%;opacity:0}.mdl-spinner__layer-1{border-color:#42a5f5}.mdl-spinner--single-color .mdl-spinner__layer-1{border-color:rgb(63,81,181)}.mdl-spinner.is-active .mdl-spinner__layer-1{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-2{border-color:#f44336}.mdl-spinner--single-color .mdl-spinner__layer-2{border-color:rgb(63,81,181)}.mdl-spinner.is-active .mdl-spinner__layer-2{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-3{border-color:#fdd835}.mdl-spinner--single-color .mdl-spinner__layer-3{border-color:rgb(63,81,181)}.mdl-spinner.is-active .mdl-spinner__layer-3{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-4{border-color:#4caf50}.mdl-spinner--single-color .mdl-spinner__layer-4{border-color:rgb(63,81,181)}.mdl-spinner.is-active .mdl-spinner__layer-4{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@-webkit-keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}@keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}.mdl-spinner__gap-patch{position:absolute;box-sizing:border-box;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__gap-patch .mdl-spinner__circle{width:1000%;left:-450%}.mdl-spinner__circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__circle-clipper.mdl-spinner__left{float:left}.mdl-spinner__circle-clipper.mdl-spinner__right{float:right}.mdl-spinner__circle-clipper .mdl-spinner__circle{width:200%}.mdl-spinner__circle{box-sizing:border-box;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent!important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0;left:0}.mdl-spinner__left .mdl-spinner__circle{border-right-color:transparent!important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.mdl-spinner.is-active .mdl-spinner__left .mdl-spinner__circle{-webkit-animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__right .mdl-spinner__circle{left:-100%;border-left-color:transparent!important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.mdl-spinner.is-active .mdl-spinner__right .mdl-spinner__circle{-webkit-animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}@keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}.mdl-switch{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0;overflow:visible;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-switch.is-upgraded{padding-left:28px}.mdl-switch__input{line-height:24px}.mdl-switch.is-upgraded .mdl-switch__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-switch__track{background:rgba(0,0,0,.26);position:absolute;left:0;top:5px;height:14px;width:36px;border-radius:14px;cursor:pointer}.mdl-switch.is-checked .mdl-switch__track{background:rgba(63,81,181,.5)}.mdl-switch__track fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__track{background:rgba(0,0,0,.12);cursor:auto}.mdl-switch__thumb{background:#fafafa;position:absolute;left:0;top:2px;height:20px;width:20px;border-radius:50%;cursor:pointer;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:left}.mdl-switch.is-checked .mdl-switch__thumb{background:rgb(63,81,181);left:16px;box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-switch__thumb fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__thumb{background:#bdbdbd;cursor:auto}.mdl-switch__focus-helper{position:absolute;top:50%;left:50%;-webkit-transform:translate(-4px,-4px);transform:translate(-4px,-4px);display:inline-block;box-sizing:border-box;width:8px;height:8px;border-radius:50%;background-color:transparent}.mdl-switch.is-focused .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-switch.is-focused.is-checked .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(63,81,181,.26);background-color:rgba(63,81,181,.26)}.mdl-switch__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0;left:24px}.mdl-switch__label fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__label{color:#bdbdbd;cursor:auto}.mdl-switch__ripple-container{position:absolute;z-index:2;top:-12px;left:-14px;box-sizing:border-box;width:48px;height:48px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000);transition-duration:.4s;transition-timing-function:step-end;transition-property:left}.mdl-switch__ripple-container .mdl-ripple{background:rgb(63,81,181)}.mdl-switch__ripple-container fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__ripple-container{cursor:auto}fieldset[disabled] .mdl-switch .mdl-switch__ripple-container .mdl-ripple,.mdl-switch.is-disabled .mdl-switch__ripple-container .mdl-ripple{background:0 0}.mdl-switch.is-checked .mdl-switch__ripple-container{left:2px}.mdl-tabs{display:block;width:100%}.mdl-tabs__tab-bar{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:space-between;-ms-flex-line-pack:justify;align-content:space-between;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;height:48px;padding:0;margin:0;border-bottom:1px solid #e0e0e0}.mdl-tabs__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(0,0,0,.54);overflow:hidden}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active{color:rgba(0,0,0,.87)}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active:after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:rgb(63,81,181);-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-tabs__tab .mdl-tabs__ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-tabs__tab .mdl-tabs__ripple-container .mdl-ripple{background:rgb(63,81,181)}.mdl-tabs__panel{display:block}.mdl-tabs.is-upgraded .mdl-tabs__panel{display:none}.mdl-tabs.is-upgraded .mdl-tabs__panel.is-active{display:block}@-webkit-keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}@keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}.mdl-textfield{position:relative;font-size:16px;display:inline-block;box-sizing:border-box;width:300px;max-width:100%;margin:0;padding:20px 0}.mdl-textfield .mdl-button{position:absolute;bottom:20px}.mdl-textfield--align-right{text-align:right}.mdl-textfield--full-width{width:100%}.mdl-textfield--expandable{min-width:32px;width:auto;min-height:32px}.mdl-textfield--expandable .mdl-button--icon{top:16px}.mdl-textfield__input{border:none;border-bottom:1px solid rgba(0,0,0,.12);display:block;font-size:16px;font-family:"Helvetica","Arial",sans-serif;margin:0;padding:4px 0;width:100%;background:0 0;text-align:left;color:inherit}.mdl-textfield__input[type="number"]{-moz-appearance:textfield}.mdl-textfield__input[type="number"]::-webkit-inner-spin-button,.mdl-textfield__input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.mdl-textfield.is-focused .mdl-textfield__input{outline:none}.mdl-textfield.is-invalid .mdl-textfield__input{border-color:#d50000;box-shadow:none}fieldset[disabled] .mdl-textfield .mdl-textfield__input,.mdl-textfield.is-disabled .mdl-textfield__input{background-color:transparent;border-bottom:1px dotted rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-textfield textarea.mdl-textfield__input{display:block}.mdl-textfield__label{bottom:0;color:rgba(0,0,0,.26);font-size:16px;left:0;right:0;pointer-events:none;position:absolute;display:block;top:24px;width:100%;overflow:hidden;white-space:nowrap;text-align:left}.mdl-textfield.is-dirty .mdl-textfield__label,.mdl-textfield.has-placeholder .mdl-textfield__label{visibility:hidden}.mdl-textfield--floating-label .mdl-textfield__label{transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{transition:none}fieldset[disabled] .mdl-textfield .mdl-textfield__label,.mdl-textfield.is-disabled.is-disabled .mdl-textfield__label{color:rgba(0,0,0,.26)}.mdl-textfield--floating-label.is-focused .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{color:rgb(63,81,181);font-size:12px;top:4px;visibility:visible}.mdl-textfield--floating-label.is-focused .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__expandable-holder .mdl-textfield__label{top:-16px}.mdl-textfield--floating-label.is-invalid .mdl-textfield__label{color:#d50000;font-size:12px}.mdl-textfield__label:after{background-color:rgb(63,81,181);bottom:20px;content:'';height:2px;left:45%;position:absolute;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);visibility:hidden;width:10px}.mdl-textfield.is-focused .mdl-textfield__label:after{left:0;visibility:visible;width:100%}.mdl-textfield.is-invalid .mdl-textfield__label:after{background-color:#d50000}.mdl-textfield__error{color:#d50000;position:absolute;font-size:12px;margin-top:3px;visibility:hidden;display:block}.mdl-textfield.is-invalid .mdl-textfield__error{visibility:visible}.mdl-textfield__expandable-holder{display:inline-block;position:relative;margin-left:32px;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-block;max-width:.1px}.mdl-textfield.is-focused .mdl-textfield__expandable-holder,.mdl-textfield.is-dirty .mdl-textfield__expandable-holder{max-width:600px}.mdl-textfield__expandable-holder .mdl-textfield__label:after{bottom:0}.mdl-tooltip{-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:top center;transform-origin:top center;z-index:999;background:rgba(97,97,97,.9);border-radius:2px;color:#fff;display:inline-block;font-size:10px;font-weight:500;line-height:14px;max-width:170px;position:fixed;top:-500px;left:-500px;padding:8px;text-align:center}.mdl-tooltip.is-active{-webkit-animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards;animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards}.mdl-tooltip--large{line-height:14px;font-size:14px;padding:16px}@-webkit-keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}@keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}.mdl-shadow--2dp{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-shadow--3dp{box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-shadow--4dp{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2)}.mdl-shadow--6dp{box-shadow:0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.2)}.mdl-shadow--8dp{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.2)}.mdl-shadow--16dp{box-shadow:0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.mdl-shadow--24dp{box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2)}.mdl-grid{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;margin:0 auto;-webkit-align-items:stretch;-ms-flex-align:stretch;align-items:stretch}.mdl-grid.mdl-grid--no-spacing{padding:0}.mdl-cell{box-sizing:border-box}.mdl-cell--top{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start}.mdl-cell--middle{-webkit-align-self:center;-ms-flex-item-align:center;-ms-grid-row-align:center;align-self:center}.mdl-cell--bottom{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end}.mdl-cell--stretch{-webkit-align-self:stretch;-ms-flex-item-align:stretch;-ms-grid-row-align:stretch;align-self:stretch}.mdl-grid.mdl-grid--no-spacing>.mdl-cell{margin:0}.mdl-cell--order-1{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12{-webkit-order:12;-ms-flex-order:12;order:12}@media (max-width:479px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:100%}.mdl-cell--hide-phone{display:none!important}.mdl-cell--order-1-phone.mdl-cell--order-1-phone{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-phone.mdl-cell--order-2-phone{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-phone.mdl-cell--order-3-phone{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-phone.mdl-cell--order-4-phone{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-phone.mdl-cell--order-5-phone{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-phone.mdl-cell--order-6-phone{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-phone.mdl-cell--order-7-phone{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-phone.mdl-cell--order-8-phone{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-phone.mdl-cell--order-9-phone{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-phone.mdl-cell--order-10-phone{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-phone.mdl-cell--order-11-phone{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-phone.mdl-cell--order-12-phone{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:25%}.mdl-cell--2-col,.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:50%}.mdl-cell--3-col,.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:75%}.mdl-cell--4-col,.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:100%}.mdl-cell--5-col,.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:100%}.mdl-cell--6-col,.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:100%}.mdl-cell--7-col,.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:100%}.mdl-cell--8-col,.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:25%}.mdl-cell--2-offset,.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:50%}.mdl-cell--3-offset,.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:75%}}@media (min-width:480px) and (max-width:839px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:50%}.mdl-cell--hide-tablet{display:none!important}.mdl-cell--order-1-tablet.mdl-cell--order-1-tablet{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-tablet.mdl-cell--order-2-tablet{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-tablet.mdl-cell--order-3-tablet{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-tablet.mdl-cell--order-4-tablet{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-tablet.mdl-cell--order-5-tablet{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-tablet.mdl-cell--order-6-tablet{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-tablet.mdl-cell--order-7-tablet{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-tablet.mdl-cell--order-8-tablet{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-tablet.mdl-cell--order-9-tablet{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-tablet.mdl-cell--order-10-tablet{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-tablet.mdl-cell--order-11-tablet{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-tablet.mdl-cell--order-12-tablet{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:calc(12.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:12.5%}.mdl-cell--2-col,.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:25%}.mdl-cell--3-col,.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:calc(37.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:37.5%}.mdl-cell--4-col,.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:50%}.mdl-cell--5-col,.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:calc(62.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:62.5%}.mdl-cell--6-col,.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:75%}.mdl-cell--7-col,.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:calc(87.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:87.5%}.mdl-cell--8-col,.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:calc(12.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:12.5%}.mdl-cell--2-offset,.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:25%}.mdl-cell--3-offset,.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:calc(37.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:37.5%}.mdl-cell--4-offset,.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:50%}.mdl-cell--5-offset,.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:calc(62.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:62.5%}.mdl-cell--6-offset,.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:75%}.mdl-cell--7-offset,.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:calc(87.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:87.5%}}@media (min-width:840px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:33.3333333333%}.mdl-cell--hide-desktop{display:none!important}.mdl-cell--order-1-desktop.mdl-cell--order-1-desktop{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-desktop.mdl-cell--order-2-desktop{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-desktop.mdl-cell--order-3-desktop{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-desktop.mdl-cell--order-4-desktop{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-desktop.mdl-cell--order-5-desktop{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-desktop.mdl-cell--order-6-desktop{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-desktop.mdl-cell--order-7-desktop{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-desktop.mdl-cell--order-8-desktop{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-desktop.mdl-cell--order-9-desktop{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-desktop.mdl-cell--order-10-desktop{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-desktop.mdl-cell--order-11-desktop{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-desktop.mdl-cell--order-12-desktop{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:calc(8.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:8.3333333333%}.mdl-cell--2-col,.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:calc(16.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:16.6666666667%}.mdl-cell--3-col,.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:25%}.mdl-cell--4-col,.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:33.3333333333%}.mdl-cell--5-col,.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:calc(41.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:41.6666666667%}.mdl-cell--6-col,.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:50%}.mdl-cell--7-col,.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:calc(58.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:58.3333333333%}.mdl-cell--8-col,.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:calc(66.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:66.6666666667%}.mdl-cell--9-col,.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:75%}.mdl-cell--10-col,.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:calc(83.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:83.3333333333%}.mdl-cell--11-col,.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:calc(91.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:91.6666666667%}.mdl-cell--12-col,.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:calc(8.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:8.3333333333%}.mdl-cell--2-offset,.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:calc(16.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:16.6666666667%}.mdl-cell--3-offset,.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:25%}.mdl-cell--4-offset,.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:calc(33.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:33.3333333333%}.mdl-cell--5-offset,.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:calc(41.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:41.6666666667%}.mdl-cell--6-offset,.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:50%}.mdl-cell--7-offset,.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:calc(58.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:58.3333333333%}.mdl-cell--8-offset,.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:calc(66.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:66.6666666667%}.mdl-cell--9-offset,.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:75%}.mdl-cell--10-offset,.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:calc(83.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:83.3333333333%}.mdl-cell--11-offset,.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:calc(91.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:91.6666666667%}}body{margin:0}.styleguide-demo h1{margin:48px 24px 0}.styleguide-demo h1:after{content:'';display:block;width:100%;border-bottom:1px solid rgba(0,0,0,.5);margin-top:24px}.styleguide-demo{opacity:0;transition:opacity .6s ease}.styleguide-masthead{height:256px;background:#212121;padding:115px 16px 0}.styleguide-container{position:relative;max-width:960px;width:100%}.styleguide-title{color:#fff;bottom:auto;position:relative;font-size:56px;font-weight:300;line-height:1;letter-spacing:-.02em}.styleguide-title:after{border-bottom:0}.styleguide-title span{font-weight:300}.mdl-styleguide .mdl-layout__drawer .mdl-navigation__link{padding:10px 24px}.demosLoaded .styleguide-demo{opacity:1}iframe{display:block;width:100%;border:none}iframe.heightSet{overflow:hidden}.demo-wrapper{margin:24px}.demo-wrapper iframe{border:1px solid rgba(0,0,0,.5)} \ No newline at end of file diff --git a/client/src/jsMain/resources/styles/visibility.css b/client/src/jsMain/resources/styles/visibility.css new file mode 100644 index 00000000..70e87cad --- /dev/null +++ b/client/src/jsMain/resources/styles/visibility.css @@ -0,0 +1,3 @@ +.gone { + display: none; +} diff --git a/client/src/jvmMain/kotlin/dev/inmo/postssystem/client/CMD.kt b/client/src/jvmMain/kotlin/dev/inmo/postssystem/client/CMD.kt new file mode 100644 index 00000000..77de5c6a --- /dev/null +++ b/client/src/jvmMain/kotlin/dev/inmo/postssystem/client/CMD.kt @@ -0,0 +1,35 @@ +package dev.inmo.postssystem.client + +import dev.inmo.postssystem.features.users.common.ReadUsersStorage +import dev.inmo.micro_utils.repos.pagination.getAll +import org.koin.core.context.startKoin + +fun readLine(suggestionText: String): String { + while (true) { + println(suggestionText) + readLine() ?.let { return it } + } +} + +suspend fun main(args: Array) { + val serverUrl = readLine("Server url:") + val login = readLine("Username:") + val password = readLine("Password:") + + val koin = startKoin { +// modules(getAuthorizedFeaturesDIModule(serverUrl, AuthCreds(Username(login), password))) + }.koin + + while (true) { + val chosen = readLine( + """ + Choose action: + 1. Show server users + """.trimIndent() + ).toIntOrNull() + when (chosen) { + 1 -> println(koin.get().getAll { getByPagination(it) }) + else -> println("Sorry, I didn't understand") + } + } +} diff --git a/client/src/main/AndroidManifest.xml b/client/src/main/AndroidManifest.xml new file mode 100644 index 00000000..65d0ab6d --- /dev/null +++ b/client/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/DateTimeUtils.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/DateTimeUtils.kt deleted file mode 100644 index b91f03e6..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/DateTimeUtils.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.inmo.postssystem.core - -import com.soywiz.klock.DateTime - -typealias UnixMillis = Double -val MIN_DATE = DateTime(Double.MIN_VALUE) -val MAX_DATE = DateTime(Double.MAX_VALUE) diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/IdUtils.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/IdUtils.kt deleted file mode 100644 index 6d6e4607..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/IdUtils.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.inmo.postssystem.core - -import com.benasher44.uuid.uuid4 -import dev.inmo.postssystem.core.content.ContentId -import dev.inmo.postssystem.core.post.PostId - -fun generateId() = uuid4().toString() -fun generatePostId(): PostId = generateId() -fun generateContentId(): ContentId = generateId() diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/Content.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/Content.kt deleted file mode 100644 index f3e0f65f..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/Content.kt +++ /dev/null @@ -1,41 +0,0 @@ -package dev.inmo.postssystem.core.content - -import kotlinx.serialization.Serializable -import kotlinx.serialization.modules.* - -typealias ContentId = String - -/** - * Content which is planned to be registered in database - */ -interface Content - -/** - * That is a content which in fact just a link to another content. It would be useful in case when user wish to reuse - * some content - */ -@Serializable -data class OtherContentLinkContent( - val otherId: ContentId -) : Content - - -fun SerializersModuleBuilder.includeContentsSerializers( - block: PolymorphicModuleBuilder.() -> Unit -) { - polymorphic(Content::class) { - subclass(OtherContentLinkContent::class, OtherContentLinkContent.serializer()) - block() - } -} - -/** - * Content which is already registered in database. Using its [id] you can retrieve all known - * [dev.inmo.postssystem.core.post.RegisteredPost]s by using - * [dev.inmo.postssystem.core.post.repo.ReadPostsRepo.getPostsByContent] - */ -@Serializable -data class RegisteredContent( - val id: ContentId, - val content: Content -) diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/ContentRepo.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/ContentRepo.kt deleted file mode 100644 index a76eb94e..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/ContentRepo.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.inmo.postssystem.core.content.api - -import dev.inmo.micro_utils.repos.StandardCRUDRepo -import dev.inmo.postssystem.core.content.* - -interface ContentRepo : ReadContentRepo, WriteContentRepo, StandardCRUDRepo diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/ReadContentRepo.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/ReadContentRepo.kt deleted file mode 100644 index f65fae5d..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/ReadContentRepo.kt +++ /dev/null @@ -1,13 +0,0 @@ -package dev.inmo.postssystem.core.content.api - -import dev.inmo.postssystem.core.content.ContentId -import dev.inmo.postssystem.core.content.RegisteredContent -import dev.inmo.micro_utils.pagination.Pagination -import dev.inmo.micro_utils.pagination.PaginationResult -import dev.inmo.micro_utils.repos.ReadStandardCRUDRepo -import dev.inmo.micro_utils.repos.pagination.getAll - -/** - * Simple read API by different properties of [dev.inmo.postssystem.core.content.Content]. - */ -interface ReadContentRepo : ReadStandardCRUDRepo diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/WriteContentRepo.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/WriteContentRepo.kt deleted file mode 100644 index 368dcbfa..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/WriteContentRepo.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.inmo.postssystem.core.content.api - -import dev.inmo.micro_utils.repos.WriteStandardCRUDRepo -import dev.inmo.postssystem.core.content.* -import kotlinx.coroutines.flow.Flow - -interface WriteContentRepo : WriteStandardCRUDRepo diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/BusinessContentRepo.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/BusinessContentRepo.kt deleted file mode 100644 index ef6a79cb..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/BusinessContentRepo.kt +++ /dev/null @@ -1,117 +0,0 @@ -package dev.inmo.postssystem.core.content.api.business - -import dev.inmo.micro_utils.pagination.* -import dev.inmo.micro_utils.repos.UpdatedValuePair -import dev.inmo.postssystem.core.content.* -import dev.inmo.postssystem.core.content.api.* -import dev.inmo.postssystem.core.generateContentId -import kotlinx.coroutines.flow.* - -interface BusinessContentRepoContentAdapter { - val type: AdapterType - suspend fun storeContent(contentId: ContentId, content: Content): Boolean - suspend fun getContent(contentId: ContentId): Content? - suspend fun removeContent(contentId: ContentId) -} - -class BusinessReadContentRepo( - adapters: List, - private val helperRepo: BusinessContentRepoReadHelper, -) : ReadContentRepo { - private val adaptersMap: Map = adapters.map { - it.type to it - }.toMap() - override suspend fun contains(id: ContentId): Boolean = helperRepo.contains(id) - - override suspend fun count(): Long = helperRepo.count() - - override suspend fun getById(id: ContentId): RegisteredContent? = helperRepo.getType(id) ?.let { - adaptersMap[it] ?.getContent(id) ?.let { content -> - RegisteredContent(id, content) - } - } - - override suspend fun getByPagination( - pagination: Pagination - ): PaginationResult = helperRepo.getKeysByPagination( - pagination - ).let { - it.results.mapNotNull { - getById(it) - }.createPaginationResult( - it, - count() - ) - } -} - -class BusinessWriteContentRepo( - private val adapters: List, - private val helperRepo: BusinessContentRepoHelper -) : WriteContentRepo { - private val adaptersMap = adapters.map { it.type to it }.toMap() - private val _deletedObjectsIdsFlow = MutableSharedFlow() - override val deletedObjectsIdsFlow: Flow = _deletedObjectsIdsFlow.asSharedFlow() - private val _newObjectsFlow = MutableSharedFlow() - override val newObjectsFlow: Flow = _newObjectsFlow.asSharedFlow() - private val _updatedObjectsFlow = MutableSharedFlow() - override val updatedObjectsFlow: Flow = _updatedObjectsFlow.asSharedFlow() - - override suspend fun create(values: List): List { - return values.mapNotNull { content -> - if (content is OtherContentLinkContent) { - adapters.forEach { - val existsContent = it.getContent(content.otherId) - if (existsContent != null) { - return@mapNotNull RegisteredContent( - content.otherId, - existsContent - ) - } - } - } - val contentId = generateContentId() - val adapter = adapters.firstOrNull { it.storeContent(contentId, content) } ?: return@mapNotNull null - if (!helperRepo.saveType(contentId, adapter.type)) { - adapter.removeContent(contentId) - } - RegisteredContent(contentId, content).also { _newObjectsFlow.emit(it) } - } - } - - override suspend fun deleteById(ids: List) { - ids.forEach { contentId -> - adaptersMap[helperRepo.getType(contentId)] ?.removeContent(contentId) ?: adapters.forEach { - it.removeContent(contentId) - } - helperRepo.deleteContentId(contentId) - _deletedObjectsIdsFlow.emit(contentId) - } - } - - override suspend fun update(id: ContentId, value: Content): RegisteredContent? { - adaptersMap[helperRepo.getType(id)] ?.removeContent(id) ?: adapters.forEach { - it.removeContent(id) - } - adapters.firstOrNull { it.storeContent(id, value) } ?: return null - return RegisteredContent(id, value).also { _updatedObjectsFlow.emit(it) } - } - - override suspend fun update(values: List>): List { - return values.mapNotNull { - update(it.first, it.second) - } - } - -} - -class BusinessContentRepo( - adapters: List, - helperRepo: BusinessContentRepoHelper -) : ContentRepo, ReadContentRepo by BusinessReadContentRepo( - adapters, - helperRepo -), WriteContentRepo by BusinessWriteContentRepo( - adapters, - helperRepo -) diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/BusinessContentRepoHelper.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/BusinessContentRepoHelper.kt deleted file mode 100644 index 15797642..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/BusinessContentRepoHelper.kt +++ /dev/null @@ -1,59 +0,0 @@ -package dev.inmo.postssystem.core.content.api.business - -import dev.inmo.micro_utils.pagination.Pagination -import dev.inmo.micro_utils.pagination.PaginationResult -import dev.inmo.micro_utils.repos.* -import dev.inmo.postssystem.core.content.ContentId - -typealias AdapterType = String - -interface BusinessContentRepoReadHelper : Repo { - suspend fun getKeysByPagination(pagination: Pagination): PaginationResult - suspend fun contains(contentId: ContentId): Boolean - suspend fun getType(contentId: ContentId): AdapterType? - suspend fun count(): Long -} -interface BusinessContentRepoWriteHelper : Repo { - suspend fun deleteContentId(contentId: ContentId) - suspend fun saveType(contentId: ContentId, type: AdapterType): Boolean -} -interface BusinessContentRepoHelper : BusinessContentRepoReadHelper, BusinessContentRepoWriteHelper - -class KeyValueBusinessContentRepoReadHelper( - private val keyValueRepo: ReadStandardKeyValueRepo -) : BusinessContentRepoReadHelper { - override suspend fun getKeysByPagination(pagination: Pagination): PaginationResult = keyValueRepo.keys(pagination) - override suspend fun contains(contentId: ContentId): Boolean = keyValueRepo.contains(contentId) - override suspend fun getType(contentId: ContentId): AdapterType? = keyValueRepo.get(contentId) - override suspend fun count(): Long = keyValueRepo.count() -} - -class KeyValueBusinessContentRepoWriteHelper( - private val keyValueRepo: WriteStandardKeyValueRepo -) : BusinessContentRepoWriteHelper { - override suspend fun deleteContentId(contentId: ContentId) { keyValueRepo.unset(contentId) } - - override suspend fun saveType(contentId: ContentId, type: AdapterType): Boolean { - keyValueRepo.set(contentId, type) - return true - } -} - -class KeyValueBusinessContentRepoHelper( - private val keyValueRepo: StandardKeyValueRepo -) : BusinessContentRepoHelper, BusinessContentRepoReadHelper by KeyValueBusinessContentRepoReadHelper( - keyValueRepo -), BusinessContentRepoWriteHelper by KeyValueBusinessContentRepoWriteHelper( - keyValueRepo -) - -fun StandardKeyValueRepo.asBusinessContentRepo( - adapters: List -) = BusinessContentRepo( - adapters, - KeyValueBusinessContentRepoHelper(this) -) - -fun StandardKeyValueRepo.asBusinessContentRepo( - vararg adapters: BusinessContentRepoContentAdapter -) = asBusinessContentRepo(adapters.toList()) diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/KeyValueBusinessContentRepoAdapter.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/KeyValueBusinessContentRepoAdapter.kt deleted file mode 100644 index 6916036f..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/KeyValueBusinessContentRepoAdapter.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.inmo.postssystem.core.content.api.business.content_adapters - -import dev.inmo.micro_utils.repos.* -import dev.inmo.postssystem.core.content.Content -import dev.inmo.postssystem.core.content.ContentId -import dev.inmo.postssystem.core.content.api.business.AdapterType -import dev.inmo.postssystem.core.content.api.business.BusinessContentRepoContentAdapter - -class KeyValueBusinessContentRepoAdapter( - override val type: AdapterType, - private val keyValueRepo: StandardKeyValueRepo, - private val contentToData: suspend (Content) -> T?, - private val dataToContent: suspend (T) -> Content -) : BusinessContentRepoContentAdapter { - override suspend fun storeContent(contentId: ContentId, content: Content): Boolean { - keyValueRepo.set(contentId, contentToData(content) ?: return false) - return true - } - - override suspend fun getContent(contentId: ContentId): Content? { - return dataToContent( - keyValueRepo.get(contentId) ?: return null - ) - } - - override suspend fun removeContent(contentId: ContentId) { - keyValueRepo.unset(contentId) - } -} \ No newline at end of file diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/binary/BinaryBusinessContentRepoContentAdapter.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/binary/BinaryBusinessContentRepoContentAdapter.kt deleted file mode 100644 index b7aab38b..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/binary/BinaryBusinessContentRepoContentAdapter.kt +++ /dev/null @@ -1,54 +0,0 @@ -package dev.inmo.postssystem.core.content.api.business.content_adapters.binary - -import dev.inmo.micro_utils.repos.* -import dev.inmo.postssystem.core.content.Content -import dev.inmo.postssystem.core.content.ContentId -import dev.inmo.postssystem.core.content.api.business.AdapterType -import dev.inmo.postssystem.core.content.api.business.BusinessContentRepoContentAdapter -import dev.inmo.postssystem.core.content.api.business.content_adapters.KeyValueBusinessContentRepoAdapter -import kotlinx.serialization.json.Json - -private val format = Json { ignoreUnknownKeys = true } - -class BinaryBusinessContentRepoContentAdapter( - private val dataStore: KeyValueRepo, - private val filesStore: KeyValueRepo, - private val removeOnAbsentInOneOfStores: Boolean = false -) : BusinessContentRepoContentAdapter { - override val type: AdapterType - get() = "binary" - - override suspend fun storeContent(contentId: ContentId, content: Content): Boolean { - (content as? BinaryContent) ?.also { - filesStore.set(contentId, it.dataAllocator()) - dataStore.set( - contentId, - format.encodeToString(BinaryContent.serializer(), it.copy { ByteArray(0) }) - ) - } ?: return false - return true - } - - override suspend fun getContent(contentId: ContentId): Content? { - return filesStore.get(contentId) ?.let { - val serializedData = dataStore.get(contentId) - if (serializedData != null) { - format.decodeFromString(BinaryContent.serializer(), serializedData).copy { - it - } - } else { - null - } - } ?: null.also { - if (removeOnAbsentInOneOfStores) { - filesStore.unset(contentId) - dataStore.unset(contentId) - } - } - } - - override suspend fun removeContent(contentId: ContentId) { - filesStore.unset(contentId) - dataStore.unset(contentId) - } -} \ No newline at end of file diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/binary/BinaryContent.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/binary/BinaryContent.kt deleted file mode 100644 index 5c483c0c..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/binary/BinaryContent.kt +++ /dev/null @@ -1,19 +0,0 @@ -package dev.inmo.postssystem.core.content.api.business.content_adapters.binary - -import dev.inmo.micro_utils.common.ByteArrayAllocator -import dev.inmo.micro_utils.common.ByteArrayAllocatorSerializer -import dev.inmo.micro_utils.mime_types.KnownMimeTypes -import dev.inmo.micro_utils.mime_types.MimeType -import dev.inmo.postssystem.core.content.Content -import kotlinx.serialization.Serializable - -@Serializable -data class BinaryContent( - val mimeType: MimeType, - val originalFileName: String, - @Serializable(ByteArrayAllocatorSerializer::class) - val dataAllocator: ByteArrayAllocator -) : Content - -val BinaryContent.isImage: Boolean - get() = mimeType is KnownMimeTypes.Image diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/text/TextBusinessContentRepoContentAdapter.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/text/TextBusinessContentRepoContentAdapter.kt deleted file mode 100644 index 214ff8d2..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/text/TextBusinessContentRepoContentAdapter.kt +++ /dev/null @@ -1,15 +0,0 @@ -package dev.inmo.postssystem.core.content.api.business.content_adapters.text - -import dev.inmo.micro_utils.repos.* -import dev.inmo.postssystem.core.content.ContentId -import dev.inmo.postssystem.core.content.api.business.BusinessContentRepoContentAdapter -import dev.inmo.postssystem.core.content.api.business.content_adapters.KeyValueBusinessContentRepoAdapter - -class TextBusinessContentRepoContentAdapter( - private val keyValueRepo: StandardKeyValueRepo -) : BusinessContentRepoContentAdapter by KeyValueBusinessContentRepoAdapter( - "regularText", - keyValueRepo, - { (it as? TextContent) ?.text }, - { TextContent(it) } -) \ No newline at end of file diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/text/TextContent.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/text/TextContent.kt deleted file mode 100644 index 896291b7..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/text/TextContent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.inmo.postssystem.core.content.api.business.content_adapters.text - -import dev.inmo.postssystem.core.content.Content -import kotlinx.serialization.Serializable - -@Serializable -data class TextContent( - val text: String -) : Content diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/post/Post.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/post/Post.kt deleted file mode 100644 index aaf7187a..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/post/Post.kt +++ /dev/null @@ -1,61 +0,0 @@ -package dev.inmo.postssystem.core.post - -import dev.inmo.postssystem.core.UnixMillis -import dev.inmo.postssystem.core.content.ContentId -import com.soywiz.klock.DateTime -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient - -typealias PostId = String -typealias ContentIds = List - -/** - * Base interface for creating of new post. Usually, it is just [SimplePost] instance - */ -@Serializable -sealed class Post { - abstract val content: ContentIds -} - -/** - * Root entity of the whole system. Can be retrieved from [dev.inmo.postssystem.core.post.repo.ReadPostsRepo] - * by getting and created in [dev.inmo.postssystem.core.post.repo.WritePostsRepo] by inserting of [Post] - * instance - */ -@Serializable -sealed class RegisteredPost : Post() { - abstract val id: PostId - - abstract override val content: ContentIds - - abstract val creationDate: DateTime -} - -/** - * Base and currently (1st Nov 2019) single realisation of [Post]. There is [SimpleRegisteredPost] which is technically - * is [Post] too, but it is not direct [Post] realisation - */ -@Serializable -data class SimplePost( - override val content: ContentIds -) : Post() - -/** - * Base and currently (1st Nov 2019) single realisation of [RegisteredPost] - */ -@Serializable -data class SimpleRegisteredPost( - override val id: PostId, - override val content: ContentIds, - private val creationDateTimeMillis: UnixMillis -) : RegisteredPost() { - @Transient - override val creationDate: DateTime = DateTime(creationDateTimeMillis) -} - -@Suppress("FunctionName") -fun RegisteredPost( - id: PostId, - content: ContentIds, - creationDate: DateTime -) = SimpleRegisteredPost(id, content, creationDate.unixMillis) \ No newline at end of file diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/post/repo/PostsRepo.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/post/repo/PostsRepo.kt deleted file mode 100644 index 1cd2494a..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/post/repo/PostsRepo.kt +++ /dev/null @@ -1,3 +0,0 @@ -package dev.inmo.postssystem.core.post.repo - -interface PostsRepo : ReadPostsRepo, WritePostsRepo diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/post/repo/ReadPostsRepo.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/post/repo/ReadPostsRepo.kt deleted file mode 100644 index 60ab64dc..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/post/repo/ReadPostsRepo.kt +++ /dev/null @@ -1,53 +0,0 @@ -package dev.inmo.postssystem.core.post.repo - -import dev.inmo.postssystem.core.MAX_DATE -import dev.inmo.postssystem.core.MIN_DATE -import dev.inmo.postssystem.core.content.ContentId -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.post.RegisteredPost -import com.soywiz.klock.DateTime -import dev.inmo.micro_utils.pagination.* - -/** - * Simple read API by different properties - */ -interface ReadPostsRepo { - /** - * @return [Set] of [PostId]s which can be used to get data using [getPostById] - */ - suspend fun getPostsIds(): Set - - /** - * @return [RegisteredPost] if it is available by [id] - */ - suspend fun getPostById(id: PostId): RegisteredPost? - - /** - * @return all [RegisteredPost]s which contains content with specified content [id] - */ - suspend fun getPostsByContent(id: ContentId): List - - /** - * @return all [RegisteredPost]s which was registered between [from] and [to]. Range will be used INCLUSIVE, line \[[from], [to]\] - */ - suspend fun getPostsByCreatingDates( - from: DateTime = MIN_DATE, - to: DateTime = MAX_DATE, - pagination: Pagination = FirstPagePagination() - ): PaginationResult - - /** - * @return all posts by pages basing on their creation date - */ - suspend fun getPostsByPagination(pagination: Pagination): PaginationResult -} - -suspend fun ReadPostsRepo.getPostsByCreatingDates( - from: DateTime? = null, - to: DateTime? = null, - pagination: Pagination = FirstPagePagination() -) = getPostsByCreatingDates( - from ?: MIN_DATE, - to ?: MAX_DATE, - pagination -) diff --git a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/post/repo/WritePostsRepo.kt b/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/post/repo/WritePostsRepo.kt deleted file mode 100644 index 4d2b5160..00000000 --- a/core/api/src/commonMain/kotlin/dev/inmo/postssystem/core/post/repo/WritePostsRepo.kt +++ /dev/null @@ -1,21 +0,0 @@ -package dev.inmo.postssystem.core.post.repo - -import dev.inmo.postssystem.core.post.* -import kotlinx.coroutines.flow.Flow - -interface WritePostsRepo { - val postCreatedFlow: Flow - val postDeletedFlow: Flow - val postUpdatedFlow: Flow - - /** - * For creating of post you need to create all its [dev.inmo.postssystem.core.content.RegisteredContent] - * and (or just) retrieve their [ContentIds] and put it into some [Post] implementation line [SimplePost]. - * - * This method SHOULD use [PostId] of [RegisteredPost.id] in case if [RegisteredPost] passed - */ - suspend fun createPost(post: Post): RegisteredPost? - suspend fun deletePost(id: PostId): Boolean - - suspend fun updatePostContent(postId: PostId, post: Post): Boolean -} diff --git a/core/api/src/commonTest/kotlin/dev/inmo/postssystem/core/api/ContentSerialization.kt b/core/api/src/commonTest/kotlin/dev/inmo/postssystem/core/api/ContentSerialization.kt deleted file mode 100644 index 6b93e6a0..00000000 --- a/core/api/src/commonTest/kotlin/dev/inmo/postssystem/core/api/ContentSerialization.kt +++ /dev/null @@ -1,75 +0,0 @@ -package dev.inmo.postssystem.core.api - -import dev.inmo.micro_utils.common.ByteArrayAllocator -import dev.inmo.micro_utils.mime_types.KnownMimeTypes -import dev.inmo.postssystem.core.content.* -import dev.inmo.postssystem.core.content.api.business.content_adapters.binary.BinaryContent -import dev.inmo.postssystem.core.content.api.business.content_adapters.text.TextContent -import dev.inmo.postssystem.core.generateContentId -import kotlinx.serialization.json.Json -import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.polymorphic -import kotlin.test.Test -import kotlin.test.assertEquals - -private val jsonFormat = Json { - serializersModule = SerializersModule { - polymorphic(Content::class) { - subclass(TextContent::class, TextContent.serializer()) - subclass(BinaryContent::class, BinaryContent.serializer()) - } - } -} - -class ContentSerialization { - private val simpleTextTestEntries = 10 - private val simpleSpecialTestEntries = 10 - - @Test - fun test_that_content_correctly_serializing_and_deserializing() { - val contents = (0 until simpleTextTestEntries).map { - TextContent("Example$it") - } + (0 until simpleSpecialTestEntries).map { - BinaryContent(KnownMimeTypes.Any, "$it.example") { - byteArrayOf(it.toByte()) - } - } - - val registeredContentFakes = contents.map { content -> - RegisteredContent( - generateContentId(), - content - ) - } - - val stringified = registeredContentFakes.map { - jsonFormat.encodeToString(RegisteredContent.serializer(), it) - } - - val parsed = stringified.map { - jsonFormat.decodeFromString(RegisteredContent.serializer(), it) - } - - parsed.forEachIndexed { i, registeredContent -> - val content = registeredContent.content - assertEquals(registeredContentFakes[i].id, registeredContent.id) - when (content) { - is TextContent -> assertEquals(registeredContentFakes[i].content, content) - is BinaryContent -> { - val expectedContent = registeredContentFakes[i].content as BinaryContent - val fakeByteArrayAllocator: ByteArrayAllocator = { byteArrayOf() } - assertEquals( - expectedContent.copy(dataAllocator = fakeByteArrayAllocator), - content.copy(dataAllocator = fakeByteArrayAllocator) - ) - val expectedData = expectedContent.dataAllocator() - val parsedData = content.dataAllocator() - assertEquals(expectedData.size, parsedData.size) - expectedData.withIndex().forEach { (i, byte) -> - assertEquals(byte, parsedData[i]) - } - } - } - } - } -} diff --git a/core/api/src/jvmMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/binary/FilesStoreRepoAdapter.kt b/core/api/src/jvmMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/binary/FilesStoreRepoAdapter.kt deleted file mode 100644 index e6fd261e..00000000 --- a/core/api/src/jvmMain/kotlin/dev/inmo/postssystem/core/content/api/business/content_adapters/binary/FilesStoreRepoAdapter.kt +++ /dev/null @@ -1,79 +0,0 @@ -package dev.inmo.postssystem.core.content.api.business.content_adapters.binary - -import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine -import dev.inmo.micro_utils.coroutines.safelyWithoutExceptions -import dev.inmo.micro_utils.pagination.* -import dev.inmo.micro_utils.repos.KeyValueRepo -import dev.inmo.micro_utils.repos.set -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import java.io.File - -class FilesStoreRepoAdapter( - private val filesRepo: KeyValueRepo, - private val temporalFilesFolder: File -) : KeyValueRepo { - private val File.asByteArray - get() = readBytes() - override val onNewValue: Flow> = filesRepo.onNewValue.map { (filename, file) -> - filename to file.asByteArray - } - override val onValueRemoved: Flow = filesRepo.onValueRemoved - - init { - temporalFilesFolder.mkdirs() - } - - override suspend fun contains(key: String): Boolean = filesRepo.contains(key) - - override suspend fun count(): Long = filesRepo.count() - - override suspend fun get(k: String): ByteArray? = filesRepo.get(k) ?.asByteArray - - override suspend fun keys( - v: ByteArray, - pagination: Pagination, - reversed: Boolean - ): PaginationResult = emptyPaginationResult() - - override suspend fun keys( - pagination: Pagination, - reversed: Boolean - ): PaginationResult = filesRepo.keys(pagination, reversed) - - override suspend fun set(toSet: Map) { - supervisorScope { - toSet.map { (filename, bytes) -> - launch { - safelyWithoutExceptions { - val file = File(temporalFilesFolder, filename).also { - it.delete() - doOutsideOfCoroutine { - it.createNewFile() - it.writeBytes(bytes) - } - } - filesRepo.set(filename, file) - doOutsideOfCoroutine { file.delete() } - } - } - } - }.joinAll() - } - - override suspend fun unset(toUnset: List) = filesRepo.unset(toUnset) - - override suspend fun values( - pagination: Pagination, - reversed: Boolean - ): PaginationResult = filesRepo.values(pagination, reversed).let { - PaginationResult( - it.page, - it.pagesNumber, - it.results.map { it.readBytes() }, - it.size - ) - } - -} diff --git a/core/api/src/main/AndroidManifest.xml b/core/api/src/main/AndroidManifest.xml deleted file mode 100644 index 22770f50..00000000 --- a/core/api/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/core/exposed/build.gradle b/core/exposed/build.gradle deleted file mode 100644 index 3f6b7f64..00000000 --- a/core/exposed/build.gradle +++ /dev/null @@ -1,26 +0,0 @@ -plugins { - id "org.jetbrains.kotlin.multiplatform" - id "org.jetbrains.kotlin.plugin.serialization" -} - -apply from: "$mppJavaProjectPresetPath" - -kotlin { - sourceSets { - commonMain { - dependencies { - api "dev.inmo:micro_utils.repos.exposed:$microutils_version" - - api project(":postssystem.core.api") - } - } - jvmTest { - dependencies { - implementation "org.jetbrains.exposed:exposed-jdbc:$exposed_version" - implementation "org.xerial:sqlite-jdbc:$test_sqlite_version" - implementation "org.jetbrains.kotlin:kotlin-test" - implementation "org.jetbrains.kotlin:kotlin-test-junit" - } - } - } -} diff --git a/core/exposed/gradle.properties b/core/exposed/gradle.properties deleted file mode 100644 index abc24679..00000000 --- a/core/exposed/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -test_sqlite_version=3.28.0 diff --git a/core/exposed/src/jvmMain/kotlin/dev/inmo/postssystem/core/exposed/ExposedPostsRepo.kt b/core/exposed/src/jvmMain/kotlin/dev/inmo/postssystem/core/exposed/ExposedPostsRepo.kt deleted file mode 100644 index 8fa74ad0..00000000 --- a/core/exposed/src/jvmMain/kotlin/dev/inmo/postssystem/core/exposed/ExposedPostsRepo.kt +++ /dev/null @@ -1,205 +0,0 @@ -package dev.inmo.postssystem.core.exposed - -import dev.inmo.postssystem.core.content.ContentId -import dev.inmo.postssystem.core.generatePostId -import dev.inmo.postssystem.core.post.* -import dev.inmo.postssystem.core.post.repo.PostsRepo -import com.soywiz.klock.* -import dev.inmo.micro_utils.pagination.* -import dev.inmo.micro_utils.repos.exposed.ExposedRepo -import dev.inmo.micro_utils.repos.exposed.initTable -import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.transactions.transaction - -private class PostsRepoContentRelations( - override val database: Database -) : Table(), ExposedRepo { - private val postIdColumn = text("postId") - private val contentIdColumn = text("contentId") - - init { - initTable() - } - - fun getPostContents(postId: PostId): List { - return transaction(db = database) { - select { postIdColumn.eq(postId) }.map { it[contentIdColumn] } - } - } - - fun getContentPosts(contentId: ContentId): List { - return transaction(db = database) { - select { contentIdColumn.eq(contentId) }.map { it[postIdColumn] } - } - } - - fun linkPostAndContents(postId: PostId, vararg contentIds: ContentId) { - transaction(db = database) { - val leftToPut = contentIds.toSet() - getPostContents(postId) - leftToPut.forEach { contentId -> - insert { - it[postIdColumn] = postId - it[contentIdColumn] = contentId - } - } - } - } - fun unlinkPostAndContents(postId: PostId, vararg contentIds: ContentId): Boolean { - return transaction(db = database) { - deleteWhere { - postIdColumn.eq(postId).and(contentIdColumn.inList(contentIds.toList())) - } > 0 - } - } -} - -private val dateTimeFormat = DateFormat("EEE, dd MMM yyyy HH:mm:ss z") - -private class PostsRepoDatabaseTable( - private val database: Database, - tableName: String = "" -) : PostsRepo, Table(tableName) { - private val contentsTable = PostsRepoContentRelations(database) - - private val idColumn = text("postId") - private val creationDateColumn = text("creationDate").default( - DateTime.now().toString(dateTimeFormat) - ) - - - private val postCreatedBroadcastChannel = BroadcastChannel(Channel.BUFFERED) - override val postCreatedFlow: Flow = postCreatedBroadcastChannel.asFlow() - - private val postDeletedBroadcastChannel = BroadcastChannel(Channel.BUFFERED) - override val postDeletedFlow: Flow = postDeletedBroadcastChannel.asFlow() - - private val postUpdatedBroadcastChannel = BroadcastChannel(Channel.BUFFERED) - override val postUpdatedFlow: Flow = postUpdatedBroadcastChannel.asFlow() - - init { - transaction (db = database) { - SchemaUtils.createMissingTablesAndColumns(this@PostsRepoDatabaseTable) - } - } - - private fun ResultRow.toRegisteredPost(): RegisteredPost = get(idColumn).let { id -> - SimpleRegisteredPost( - id, - contentsTable.getPostContents(id), - dateTimeFormat.parse(get(creationDateColumn)).local.unixMillis - ) - } - - override suspend fun createPost(post: Post): RegisteredPost? { - val id = (post as? RegisteredPost) ?.let { it.id } ?: generatePostId() - return transaction( - db = database - ) { - insert { - it[idColumn] = id - } - contentsTable.linkPostAndContents(id, *post.content.toTypedArray()) - select { idColumn.eq(id) }.firstOrNull() ?.toRegisteredPost() - } ?.also { - postCreatedBroadcastChannel.send(it) - } - } - - override suspend fun deletePost(id: PostId): Boolean { - val post = getPostById(id) ?: return false - return (transaction( - db = database - ) { - deleteWhere { idColumn.eq(id) } - } > 0).also { - if (it) { - postDeletedBroadcastChannel.send(post) - contentsTable.unlinkPostAndContents(id, *post.content.toTypedArray()) - } - } - } - - override suspend fun updatePostContent(postId: PostId, post: Post): Boolean { - return transaction( - db = database - ) { - val alreadyLinked = contentsTable.getPostContents(postId) - val toRemove = alreadyLinked - post.content - val toInsert = post.content - alreadyLinked - val updated = (toRemove.isNotEmpty() && contentsTable.unlinkPostAndContents(postId, *toRemove.toTypedArray())) || toInsert.isNotEmpty() - if (toInsert.isNotEmpty()) { - contentsTable.linkPostAndContents(postId, *toInsert.toTypedArray()) - } - updated - }.also { - if (it) { - getPostById(postId) ?.also { updatedPost -> postUpdatedBroadcastChannel.send(updatedPost) } - } - } - } - override suspend fun getPostsIds(): Set { - return transaction( - db = database - ) { - selectAll().map { it[idColumn] }.toSet() - } - } - - override suspend fun getPostById(id: PostId): RegisteredPost? { - return transaction( - db = database - ) { - select { idColumn.eq(id) }.firstOrNull() ?.toRegisteredPost() - } - } - - override suspend fun getPostsByContent(id: ContentId): List { - return transaction( - db = database - ) { - val postsIds = contentsTable.getContentPosts(id) - select { idColumn.inList(postsIds) }.map { it.toRegisteredPost() } - } - } - - override suspend fun getPostsByCreatingDates( - from: DateTime, - to: DateTime, - pagination: Pagination - ): PaginationResult { - return transaction( - db = database - ) { - select { creationDateColumn.between(from, to) }.paginate( - pagination - ).map { - it.toRegisteredPost() - }.createPaginationResult( - pagination, - selectAll().count() - ) - } - } - - override suspend fun getPostsByPagination(pagination: Pagination): PaginationResult { - return transaction( - db = database - ) { - val posts = selectAll().paginate(pagination).orderBy(creationDateColumn).map { - it.toRegisteredPost() - } - val postsNumber = selectAll().count() - posts.createPaginationResult(pagination, postsNumber) - } - } - -} - -class ExposedPostsRepo ( - database: Database, - tableName: String = "" -) : PostsRepo by PostsRepoDatabaseTable(database, tableName) diff --git a/core/exposed/src/jvmTest/kotlin/dev/inmo/postssystem/core/exposed/ExposedContentAPICommonTests.kt b/core/exposed/src/jvmTest/kotlin/dev/inmo/postssystem/core/exposed/ExposedContentAPICommonTests.kt deleted file mode 100644 index 6b3adfdd..00000000 --- a/core/exposed/src/jvmTest/kotlin/dev/inmo/postssystem/core/exposed/ExposedContentAPICommonTests.kt +++ /dev/null @@ -1,86 +0,0 @@ -package dev.inmo.postssystem.core.exposed - -import dev.inmo.micro_utils.repos.create -import dev.inmo.micro_utils.repos.deleteById -import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo -import dev.inmo.postssystem.core.content.ContentId -import dev.inmo.postssystem.core.content.api.business.AdapterType -import dev.inmo.postssystem.core.content.api.business.asBusinessContentRepo -import dev.inmo.postssystem.core.content.api.business.content_adapters.text.TextBusinessContentRepoContentAdapter -import dev.inmo.postssystem.core.content.api.business.content_adapters.text.TextContent -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.first -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.transactions.transactionManager -import java.io.File -import java.sql.Connection -import kotlin.test.Test -import kotlin.test.assertEquals - -class ExposedContentRepoCommonTests { - private val tempFolder = System.getProperty("java.io.tmpdir")!! - - @Test - fun `Test that it is possible to use several different databases at one time`() { - val numberOfDatabases = 8 - - val databaseFiles = (0 until numberOfDatabases).map { - "$tempFolder/ExposedContentAPICommonTestsDB$it.db" - } - - val apis = databaseFiles.map { - File(it).also { - it.delete() - it.deleteOnExit() - } - val database = Database.Companion.connect("jdbc:sqlite:$it", driver = "org.sqlite.JDBC").also { - it.transactionManager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE - } - ExposedKeyValueRepo( - database, - { text("contentId") }, - { text("adapterType") }, - "ContentRepo" - ).asBusinessContentRepo( - TextBusinessContentRepoContentAdapter( - ExposedKeyValueRepo( - database, - { text("contentId") }, - { text("text") }, - "TextContentRepo" - ) - ) - ) - } - - val results = apis.mapIndexed { i, api -> - val expectedContent = TextContent(i.toString()) - val contents = runBlocking { api.create(TextContent(i.toString())) } - val idsCount = runBlocking { api.count() } - assertEquals(idsCount, 1) - assert(contents.isNotEmpty()) - assertEquals( - expectedContent, - contents.first().content - ) - contents.first() - } - - results.forEachIndexed { i, content -> - apis.forEachIndexed { j, api -> - assert( - runBlocking { - api.getById(content.id) == (if (i != j) null else content) - } - ) - runBlocking { - api.deleteById(content.id) - } - } - } - - databaseFiles.forEach { - File(it).delete() - } - } -} \ No newline at end of file diff --git a/core/exposed/src/jvmTest/kotlin/dev/inmo/postssystem/core/exposed/ExposedPostsRepoCommonTests.kt b/core/exposed/src/jvmTest/kotlin/dev/inmo/postssystem/core/exposed/ExposedPostsRepoCommonTests.kt deleted file mode 100644 index bf21d4a6..00000000 --- a/core/exposed/src/jvmTest/kotlin/dev/inmo/postssystem/core/exposed/ExposedPostsRepoCommonTests.kt +++ /dev/null @@ -1,64 +0,0 @@ -package dev.inmo.postssystem.core.exposed - -import dev.inmo.postssystem.core.post.SimplePost -import kotlinx.coroutines.runBlocking -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.transactions.transactionManager -import java.io.File -import java.sql.Connection -import kotlin.test.* - -class ExposedPostsRepoCommonTests { - private val tempFolder = System.getProperty("java.io.tmpdir")!! - - private val numberOfDatabases = 8 - private lateinit var databaseFiles: List - private lateinit var apis: List - - @BeforeTest - fun prepare() { - databaseFiles = (0 until numberOfDatabases).map { - File("$tempFolder/ExposedPostsAPICommonTestsDB$it.db") - } - apis = databaseFiles.map { - val database = Database.connect("jdbc:sqlite:${it.absolutePath}").also { - it.transactionManager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE - } - ExposedPostsRepo( - database - ) - } - } - - @Test - fun `Test that it is possible to use several different databases at one time`() { - val posts = apis.mapIndexed { i, api -> - val content = runBlocking { api.createPost(SimplePost(listOf(i.toString()))) } - assert(content != null) - assert(runBlocking { api.getPostsIds().size == 1 }) - content!! - } - - posts.forEachIndexed { i, post -> - apis.forEachIndexed { j, api -> - assert( - runBlocking { - api.getPostById(post.id) == (if (i != j) null else post) - } - ) - assert( - runBlocking { - api.deletePost(post.id) == (i == j) - } - ) - } - } - } - - @AfterTest - fun `Close and delete databases`() { - databaseFiles.forEach { - it.delete() - } - } -} diff --git a/core/ktor/client/gradle.properties b/core/ktor/client/gradle.properties deleted file mode 100644 index 8b137891..00000000 --- a/core/ktor/client/gradle.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/core/ktor/client/settings.gradle b/core/ktor/client/settings.gradle deleted file mode 100644 index 8b137891..00000000 --- a/core/ktor/client/settings.gradle +++ /dev/null @@ -1 +0,0 @@ - diff --git a/core/ktor/client/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/client/post/PostsRepoKtorClient.kt b/core/ktor/client/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/client/post/PostsRepoKtorClient.kt deleted file mode 100644 index 744029af..00000000 --- a/core/ktor/client/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/client/post/PostsRepoKtorClient.kt +++ /dev/null @@ -1,28 +0,0 @@ -package dev.inmo.postssystem.core.ktor.client.post - -import dev.inmo.postssystem.core.ktor.postsRootRoute -import dev.inmo.postssystem.core.post.repo.* -import io.ktor.client.HttpClient -import io.ktor.client.features.websocket.WebSockets - -class PostsRepoKtorClient private constructor( - readPostsRepo: ReadPostsRepo, - writePostsRepo: WritePostsRepo -) : PostsRepo, ReadPostsRepo by readPostsRepo, WritePostsRepo by writePostsRepo { - constructor( - baseUrl: String, - rootRoute: String? = postsRootRoute, - client: HttpClient = HttpClient { - install(WebSockets) - } - ) : this( - ReadPostsRepoKtorClient( - rootRoute ?.let { "${baseUrl}/$rootRoute" } ?: baseUrl, - client - ), - WritePostsRepoKtorClient( - rootRoute ?.let { "${baseUrl}/$rootRoute" } ?: baseUrl, - client - ) - ) -} diff --git a/core/ktor/client/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/client/post/ReadPostsRepoKtorClient.kt b/core/ktor/client/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/client/post/ReadPostsRepoKtorClient.kt deleted file mode 100644 index 41545012..00000000 --- a/core/ktor/client/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/client/post/ReadPostsRepoKtorClient.kt +++ /dev/null @@ -1,56 +0,0 @@ -package dev.inmo.postssystem.core.ktor.client.post - -import dev.inmo.postssystem.core.content.ContentId -import dev.inmo.postssystem.core.ktor.* -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.post.RegisteredPost -import dev.inmo.postssystem.core.post.repo.ReadPostsRepo -import com.soywiz.klock.DateTime -import dev.inmo.micro_utils.ktor.client.uniget -import dev.inmo.micro_utils.ktor.common.asFromToUrlPart -import dev.inmo.micro_utils.ktor.common.buildStandardUrl -import dev.inmo.micro_utils.pagination.* -import io.ktor.client.HttpClient -import kotlinx.serialization.builtins.nullable - -class ReadPostsRepoKtorClient( - private val baseUrl: String, - private val client: HttpClient = HttpClient() -) : ReadPostsRepo { - override suspend fun getPostsIds(): Set = client.uniget( - buildStandardUrl(baseUrl, getPostsIdsRoute), - postIdsSerializer - ) - - override suspend fun getPostById(id: PostId): RegisteredPost? = client.uniget( - buildStandardUrl(baseUrl, "$getPostByIdRoute/$id"), - RegisteredPost.serializer().nullable - ) - - override suspend fun getPostsByContent(id: ContentId): List = client.uniget( - buildStandardUrl(baseUrl, "$getPostsByContentRoute/$id"), - registeredPostsListSerializer - ) - - override suspend fun getPostsByCreatingDates( - from: DateTime, - to: DateTime, - pagination: Pagination - ): PaginationResult = client.uniget( - buildStandardUrl( - baseUrl, - getPostsByCreatingDatesRoute, - (from to to).asFromToUrlPart + pagination.asUrlQueryParts - ), - registeredPostsPaginationResultSerializer - ) - - override suspend fun getPostsByPagination(pagination: Pagination): PaginationResult = client.uniget( - buildStandardUrl( - baseUrl, - getPostsByCreatingDatesRoute, - pagination.asUrlQueryParts - ), - registeredPostsPaginationResultSerializer - ) -} \ No newline at end of file diff --git a/core/ktor/client/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/client/post/WritePostsRepoKtorClient.kt b/core/ktor/client/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/client/post/WritePostsRepoKtorClient.kt deleted file mode 100644 index 90d11551..00000000 --- a/core/ktor/client/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/client/post/WritePostsRepoKtorClient.kt +++ /dev/null @@ -1,49 +0,0 @@ -package dev.inmo.postssystem.core.ktor.client.post - -import dev.inmo.postssystem.core.ktor.* -import dev.inmo.postssystem.core.post.* -import dev.inmo.postssystem.core.post.repo.WritePostsRepo -import dev.inmo.micro_utils.ktor.client.* -import io.ktor.client.HttpClient -import io.ktor.client.features.websocket.WebSockets -import kotlinx.coroutines.flow.Flow -import kotlinx.serialization.builtins.nullable -import kotlinx.serialization.builtins.serializer - -class WritePostsRepoKtorClient ( - private val baseUrl: String, - private val client: HttpClient = HttpClient { - install(WebSockets) - } -) : WritePostsRepo { - override val postCreatedFlow: Flow = client.createStandardWebsocketFlow( - "$baseUrl/$postCreatedFlowRoute", - deserializer = RegisteredPost.serializer() - ) - override val postDeletedFlow: Flow = client.createStandardWebsocketFlow( - "$baseUrl/$postDeletedFlowRoute", - deserializer = RegisteredPost.serializer() - ) - override val postUpdatedFlow: Flow = client.createStandardWebsocketFlow( - "$baseUrl/$postUpdatedFlowRoute", - deserializer = RegisteredPost.serializer() - ) - - override suspend fun createPost(post: Post): RegisteredPost? = client.unipost( - "$baseUrl/$createPostRoute", - BodyPair(Post.serializer(), post), - RegisteredPost.serializer().nullable - ) - - override suspend fun deletePost(id: PostId): Boolean = client.unipost( - "$baseUrl/$deletePostRoute", - BodyPair(PostId.serializer(), id), - Boolean.serializer() - ) - - override suspend fun updatePostContent(postId: PostId, post: Post): Boolean = client.unipost( - "$baseUrl/$updatePostContentRoute", - BodyPair(UpdatePostObject.serializer(), UpdatePostObject(postId, post)), - Boolean.serializer() - ) -} diff --git a/core/ktor/client/src/main/AndroidManifest.xml b/core/ktor/client/src/main/AndroidManifest.xml deleted file mode 100644 index 3681aaa3..00000000 --- a/core/ktor/client/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/core/ktor/common/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/ContentRoutes.kt b/core/ktor/common/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/ContentRoutes.kt deleted file mode 100644 index 6e5a18f2..00000000 --- a/core/ktor/common/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/ContentRoutes.kt +++ /dev/null @@ -1,13 +0,0 @@ -package dev.inmo.postssystem.core.ktor - -const val contentRootRoute = "content" - -const val getContentsIdsRoute = "getContentsIds" -const val getContentByIdRoute = "getContentById" -const val getContentByPaginationRoute = "getContentByPagination" - -const val registerContentRoute = "registerContent" -const val deleteContentRoute = "deleteContent" - -const val contentCreatedFlowRoute = "contentCreatedFlow" -const val contentDeletedFlowRoute = "contentDeletedFlow" diff --git a/core/ktor/common/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/PostRoutes.kt b/core/ktor/common/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/PostRoutes.kt deleted file mode 100644 index 52223d78..00000000 --- a/core/ktor/common/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/PostRoutes.kt +++ /dev/null @@ -1,17 +0,0 @@ -package dev.inmo.postssystem.core.ktor - -const val postsRootRoute = "post" -const val publishedPostsSubRoute = "published" - -const val getPostsIdsRoute = "getPostsIds" -const val getPostByIdRoute = "getPostById" -const val getPostsByContentRoute = "getPostsByContent" -const val getPostsByCreatingDatesRoute = "getPostsByCreatingDates" -const val getPostsByPaginationRoute = "getPostsByPagination" - -const val postCreatedFlowRoute = "postCreatedFlow" -const val postDeletedFlowRoute = "postDeletedFlow" -const val postUpdatedFlowRoute = "postUpdatedFlow" -const val createPostRoute = "createPost" -const val deletePostRoute = "deletePost" -const val updatePostContentRoute = "updatePostContent" \ No newline at end of file diff --git a/core/ktor/common/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/Serializers.kt b/core/ktor/common/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/Serializers.kt deleted file mode 100644 index 0b9daab9..00000000 --- a/core/ktor/common/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/Serializers.kt +++ /dev/null @@ -1,14 +0,0 @@ -package dev.inmo.postssystem.core.ktor - -import dev.inmo.postssystem.core.content.ContentId -import dev.inmo.postssystem.core.content.RegisteredContent -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.post.RegisteredPost -import dev.inmo.micro_utils.pagination.PaginationResult -import kotlinx.serialization.builtins.* - -val contentIdsSerializer = SetSerializer(ContentId.serializer()) -val postIdsSerializer = SetSerializer(PostId.serializer()) -val registeredPostsListSerializer = ListSerializer(RegisteredPost.serializer()) -val registeredPostsPaginationResultSerializer = PaginationResult.serializer(RegisteredPost.serializer()) -val registeredContentPaginationResultSerializer = PaginationResult.serializer(RegisteredContent.serializer()) diff --git a/core/ktor/common/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/UpdatePostObject.kt b/core/ktor/common/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/UpdatePostObject.kt deleted file mode 100644 index 6e1137b3..00000000 --- a/core/ktor/common/src/commonMain/kotlin/dev/inmo/postssystem/core/ktor/UpdatePostObject.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.inmo.postssystem.core.ktor - -import dev.inmo.postssystem.core.post.Post -import dev.inmo.postssystem.core.post.PostId -import kotlinx.serialization.Serializable - -@Serializable -data class UpdatePostObject( - val postId: PostId, - val post: Post -) diff --git a/core/ktor/common/src/main/AndroidManifest.xml b/core/ktor/common/src/main/AndroidManifest.xml deleted file mode 100644 index d17b3d4d..00000000 --- a/core/ktor/common/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/core/ktor/server/build.gradle b/core/ktor/server/build.gradle deleted file mode 100644 index d4838c8a..00000000 --- a/core/ktor/server/build.gradle +++ /dev/null @@ -1,26 +0,0 @@ -plugins { - id "org.jetbrains.kotlin.multiplatform" - id "org.jetbrains.kotlin.plugin.serialization" -} - -apply from: "$mppJavaProjectPresetPath" - -kotlin { - sourceSets { - commonMain { - dependencies { - api "dev.inmo:micro_utils.pagination.ktor.server:$microutils_version" - api "dev.inmo:micro_utils.ktor.server:$microutils_version" - - api project(":postssystem.core.ktor.common") - } - } - jvmTest { - dependencies { - implementation "org.xerial:sqlite-jdbc:$test_sqlite_version" - implementation "org.jetbrains.kotlin:kotlin-test" - implementation "org.jetbrains.kotlin:kotlin-test-junit" - } - } - } -} diff --git a/core/ktor/server/src/jvmMain/kotlin/dev/inmo/postssystem/core/ktor/server/post/PostsRepoRoutingConfigurator.kt b/core/ktor/server/src/jvmMain/kotlin/dev/inmo/postssystem/core/ktor/server/post/PostsRepoRoutingConfigurator.kt deleted file mode 100644 index f12e7dde..00000000 --- a/core/ktor/server/src/jvmMain/kotlin/dev/inmo/postssystem/core/ktor/server/post/PostsRepoRoutingConfigurator.kt +++ /dev/null @@ -1,30 +0,0 @@ -package dev.inmo.postssystem.core.ktor.server.post - -import dev.inmo.postssystem.core.ktor.postsRootRoute -import dev.inmo.postssystem.core.post.repo.PostsRepo -import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator -import io.ktor.routing.Route -import io.ktor.routing.route - -private inline fun configurator(proxyTo: PostsRepo): Route.() -> Unit = { - configureReadPostsRepoRoutes(proxyTo) - configureWritePostsRepoRoutes(proxyTo) -} - -fun Route.configurePostsRepoRoutes( - proxyTo: PostsRepo, - rootRoute: String? = postsRootRoute -) { - rootRoute ?.also { - route(it, configurator(proxyTo)) - } ?: configurator(proxyTo).invoke(this) -} - -class PostsRepoRoutingConfigurator( - private val proxyTo: PostsRepo, - private val rootRoute: String? = postsRootRoute -) : ApplicationRoutingConfigurator.Element { - override fun Route.invoke() { - configurePostsRepoRoutes(proxyTo, rootRoute) - } -} diff --git a/core/ktor/server/src/jvmMain/kotlin/dev/inmo/postssystem/core/ktor/server/post/ReadPostsRepoRoutingConfigurator.kt b/core/ktor/server/src/jvmMain/kotlin/dev/inmo/postssystem/core/ktor/server/post/ReadPostsRepoRoutingConfigurator.kt deleted file mode 100644 index fe1e9166..00000000 --- a/core/ktor/server/src/jvmMain/kotlin/dev/inmo/postssystem/core/ktor/server/post/ReadPostsRepoRoutingConfigurator.kt +++ /dev/null @@ -1,70 +0,0 @@ -package dev.inmo.postssystem.core.ktor.server.post - -import dev.inmo.postssystem.core.MAX_DATE -import dev.inmo.postssystem.core.MIN_DATE -import dev.inmo.postssystem.core.content.ContentId -import dev.inmo.postssystem.core.ktor.* -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.post.RegisteredPost -import dev.inmo.postssystem.core.post.repo.ReadPostsRepo -import dev.inmo.micro_utils.ktor.server.* -import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator -import dev.inmo.micro_utils.pagination.extractPagination -import io.ktor.application.call -import io.ktor.routing.Route -import io.ktor.routing.get -import kotlinx.serialization.builtins.nullable - -fun Route.configureReadPostsRepoRoutes( - proxyTo: ReadPostsRepo -) { - get(getPostsIdsRoute) { - call.unianswer( - postIdsSerializer, - proxyTo.getPostsIds() - ) - } - get("$getPostByIdRoute/{id}") { - val id: PostId = call.getParameterOrSendError("id") ?: return@get - call.unianswer( - RegisteredPost.serializer().nullable, - proxyTo.getPostById(id) - ) - } - get("$getPostsByContentRoute/{id}") { - val id: ContentId = call.getParameterOrSendError("id") ?: return@get - call.unianswer( - registeredPostsListSerializer, - proxyTo.getPostsByContent(id) - ) - } - get(getPostsByCreatingDatesRoute) { - val fromToDateTime = call.request.queryParameters.extractFromToDateTime - val pagination = call.request.queryParameters.extractPagination - - call.unianswer( - registeredPostsPaginationResultSerializer, - proxyTo.getPostsByCreatingDates( - fromToDateTime.first ?: MIN_DATE, - fromToDateTime.second ?: MAX_DATE, - pagination - ) - ) - } - get(getPostsByPaginationRoute) { - val pagination = call.request.queryParameters.extractPagination - - call.unianswer( - registeredPostsPaginationResultSerializer, - proxyTo.getPostsByPagination(pagination) - ) - } -} - -class ReadPostsRepoRoutingConfigurator( - private val proxyTo: ReadPostsRepo -) : ApplicationRoutingConfigurator.Element { - override fun Route.invoke() { - configureReadPostsRepoRoutes(proxyTo) - } -} diff --git a/core/ktor/server/src/jvmMain/kotlin/dev/inmo/postssystem/core/ktor/server/post/WritePostsRepoRoutingConfigurator.kt b/core/ktor/server/src/jvmMain/kotlin/dev/inmo/postssystem/core/ktor/server/post/WritePostsRepoRoutingConfigurator.kt deleted file mode 100644 index 2fc36917..00000000 --- a/core/ktor/server/src/jvmMain/kotlin/dev/inmo/postssystem/core/ktor/server/post/WritePostsRepoRoutingConfigurator.kt +++ /dev/null @@ -1,56 +0,0 @@ -package dev.inmo.postssystem.core.ktor.server.post - -import dev.inmo.postssystem.core.ktor.* -import dev.inmo.postssystem.core.post.* -import dev.inmo.postssystem.core.post.repo.WritePostsRepo -import dev.inmo.micro_utils.ktor.server.* -import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator -import io.ktor.application.call -import io.ktor.routing.Route -import io.ktor.routing.post -import kotlinx.serialization.builtins.nullable -import kotlinx.serialization.builtins.serializer - -fun Route.configureWritePostsRepoRoutes( - proxyTo: WritePostsRepo -) { - includeWebsocketHandling(postCreatedFlowRoute, proxyTo.postCreatedFlow, RegisteredPost.serializer()) - includeWebsocketHandling(postDeletedFlowRoute, proxyTo.postDeletedFlow, RegisteredPost.serializer()) - includeWebsocketHandling(postUpdatedFlowRoute, proxyTo.postUpdatedFlow, RegisteredPost.serializer()) - - post(createPostRoute) { - call.unianswer( - RegisteredPost.serializer().nullable, - proxyTo.createPost( - call.uniload(Post.serializer()) - ) - ) - } - - post(deletePostRoute) { - call.unianswer( - Boolean.serializer(), - proxyTo.deletePost(call.uniload(PostId.serializer())) - ) - } - - post(updatePostContentRoute) { - val updatePostObject = call.uniload(UpdatePostObject.serializer()) - - call.unianswer( - Boolean.serializer(), - proxyTo.updatePostContent( - updatePostObject.postId, - updatePostObject.post - ) - ) - } -} - -class WritePostsRepoRoutingConfigurator( - private val proxyTo: WritePostsRepo -) : ApplicationRoutingConfigurator.Element { - override fun Route.invoke() { - configureWritePostsRepoRoutes(proxyTo) - } -} diff --git a/defaultAndroidSettings b/defaultAndroidSettings deleted file mode 100644 index 99e258a1..00000000 --- a/defaultAndroidSettings +++ /dev/null @@ -1,38 +0,0 @@ -android { - compileSdkVersion "$android_compileSdkVersion".toInteger() - buildToolsVersion "$android_buildToolsVersion" - - defaultConfig { - minSdkVersion "$android_minSdkVersion".toInteger() - targetSdkVersion "$android_compileSdkVersion".toInteger() - versionCode "${android_code_version}".toInteger() - versionName "$version" - } - buildTypes { - release { - minifyEnabled false - } - debug { - debuggable true - } - } - - packagingOptions { - exclude 'META-INF/kotlinx-serialization-runtime.kotlin_module' - exclude 'META-INF/kotlinx-serialization-cbor.kotlin_module' - exclude 'META-INF/kotlinx-serialization-properties.kotlin_module' - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } -} diff --git a/defaultAndroidSettings.gradle b/defaultAndroidSettings.gradle new file mode 100644 index 00000000..85c77baa --- /dev/null +++ b/defaultAndroidSettings.gradle @@ -0,0 +1,68 @@ +apply plugin: 'com.getkeepsafe.dexcount' + +android { + ext { + jvmKotlinFolderFile = { + String sep = File.separator + return new File("${project.projectDir}${sep}src${sep}jvmMain${sep}kotlin") + } + + enableIncludingJvmCodeInAndroidPart = { + File jvmKotlinFolder = jvmKotlinFolderFile() + if (jvmKotlinFolder.exists()) { + android.sourceSets.main.java.srcDirs += jvmKotlinFolder.path + } + } + + disableIncludingJvmCodeInAndroidPart = { + File jvmKotlinFolder = jvmKotlinFolderFile() + String[] oldDirs = android.sourceSets.main.java.srcDirs + android.sourceSets.main.java.srcDirs = [] + for (oldDir in oldDirs) { + if (oldDir != jvmKotlinFolder.path) { + android.sourceSets.main.java.srcDirs += oldDir + } + } + } + } + + compileSdkVersion "$android_compileSdkVersion".toInteger() + buildToolsVersion "$android_buildToolsVersion" + + defaultConfig { + minSdkVersion "$android_minSdkVersion".toInteger() + targetSdkVersion "$android_compileSdkVersion".toInteger() + versionCode "${android_code_version}".toInteger() + versionName "$version" + } + buildTypes { + release { + minifyEnabled false + } + debug { + debuggable true + } + } + + packagingOptions { + exclude 'META-INF/kotlinx-serialization-runtime.kotlin_module' + exclude 'META-INF/kotlinx-serialization-cbor.kotlin_module' + exclude 'META-INF/kotlinx-serialization-properties.kotlin_module' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + sourceSets { + String sep = File.separator + main.java.srcDirs += "src${sep}main${sep}kotlin" + } + + enableIncludingJvmCodeInAndroidPart() +} diff --git a/extensions.gradle b/extensions.gradle index 22283359..9a9b67b9 100644 --- a/extensions.gradle +++ b/extensions.gradle @@ -1,11 +1,25 @@ allprojects { ext { - mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization" - mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject" - mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject" + projectByName = { String name -> + for (subproject in rootProject.subprojects) { + if (subproject.name == name) { + return subproject + } + } + return null + } - defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings" + internalProject = { String name -> + projectByName(name) + } - publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle" + mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle" + mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle" + mppJsProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJsProject.gradle" + mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle" + + defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle" + + publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle" } } diff --git a/publishing/ktor/common/build.gradle b/features/auth/client/build.gradle similarity index 68% rename from publishing/ktor/common/build.gradle rename to features/auth/client/build.gradle index 7137dfd0..68e48b37 100644 --- a/publishing/ktor/common/build.gradle +++ b/features/auth/client/build.gradle @@ -10,9 +10,8 @@ kotlin { sourceSets { commonMain { dependencies { - api "dev.inmo:micro_utils.ktor.common:$microutils_version" - - api project(":postssystem.publishing.api") + api project(":postssystem.features.common.client") + api project(":postssystem.features.auth.common") } } } diff --git a/features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ClientAuthFeature.kt b/features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ClientAuthFeature.kt new file mode 100644 index 00000000..aa227ae0 --- /dev/null +++ b/features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ClientAuthFeature.kt @@ -0,0 +1,55 @@ +package dev.inmo.postssystem.features.auth.client + +import dev.inmo.postssystem.features.auth.common.* +import dev.inmo.postssystem.features.users.common.User +import dev.inmo.micro_utils.ktor.client.UnifiedRequester +import dev.inmo.micro_utils.ktor.common.buildStandardUrl +import io.ktor.client.HttpClient +import io.ktor.client.request.HttpRequestBuilder +import kotlinx.serialization.builtins.nullable + +class ClientAuthFeature( + private val requester: UnifiedRequester, + baseUrl: String +) : AuthFeature { + private val rootUrl = buildStandardUrl(baseUrl.dropLastWhile { it == '/' }, authRootPathPart) + private val fullAuthPath = buildStandardUrl( + rootUrl, + authAuthPathPart + ) + private val fullRefreshPath = buildStandardUrl( + rootUrl, + authRefreshPathPart + ) + private val fullGetMePath = buildStandardUrl( + rootUrl, + authGetMePathPart + ) + + constructor(client: HttpClient, rootUrl: String): this( + UnifiedRequester(client), + rootUrl + ) + + override suspend fun auth(creds: AuthCreds): AuthTokenInfo? = requester.unipost( + fullAuthPath, + AuthCreds.serializer() to creds, + AuthTokenInfo.serializer().nullable + ) + + override suspend fun refresh(refresh: RefreshToken): AuthTokenInfo? = requester.unipost( + fullRefreshPath, + RefreshToken.serializer() to refresh, + AuthTokenInfo.serializer().nullable + ) + + override suspend fun getMe(authToken: AuthToken): User? = requester.unipost( + fullGetMePath, + AuthToken.serializer() to authToken, + User.serializer().nullable + ) + + fun isAuthRequest(builder: HttpRequestBuilder): Boolean = builder.url.buildString().let { + it == fullAuthPath || it == fullRefreshPath + } +} diff --git a/features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ClientCookiesConfigurator.kt b/features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ClientCookiesConfigurator.kt new file mode 100644 index 00000000..08c227ea --- /dev/null +++ b/features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ClientCookiesConfigurator.kt @@ -0,0 +1,112 @@ +package dev.inmo.postssystem.features.auth.client + +import dev.inmo.postssystem.features.auth.common.* +import dev.inmo.postssystem.features.users.common.User +import dev.inmo.micro_utils.common.* +import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions +import io.ktor.client.HttpClientConfig +import io.ktor.client.features.cookies.* +import io.ktor.client.features.expectSuccess +import io.ktor.client.request.* +import io.ktor.client.statement.HttpReceivePipeline +import io.ktor.client.statement.HttpResponse +import io.ktor.http.* +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +object AuthUnavailableException : Exception() + +fun HttpClientConfig<*>.installClientAuthenticator( + baseUrl: String, + scope: CoroutineScope, + initialAuthKey: Either, + onAuthKeyUpdated: suspend (AuthTokenInfo) -> Unit, + onUserRetrieved: suspend (User?) -> Unit, + onAuthKeyInvalidated: suspend () -> Unit +) { +// install(Logging) { +// logger = Logger.DEFAULT +// level = LogLevel.HEADERS +// } + install(HttpCookies) { + // Will keep an in-memory map with all the cookies from previous requests. + storage = AcceptAllCookiesStorage() + } + + val authMutex = Mutex() + var currentRefreshToken: RefreshToken? = null + initialAuthKey.onFirst { + currentRefreshToken = it as? RefreshToken + }.onSecond { + currentRefreshToken = it.refresh + } + val creds = initialAuthKey.t1 as? AuthCreds + var userRefreshJob: Job? = null + + install("Auth Token Refresher") { + val clientAuthFeature = ClientAuthFeature(this, baseUrl) + fun refreshUser(newTokenInfo: AuthTokenInfo) { + userRefreshJob ?.cancel() + userRefreshJob = scope.launchSafelyWithoutExceptions { + onUserRetrieved(clientAuthFeature.getMe(newTokenInfo.token)) + } + } + initialAuthKey.onSecond { refreshUser(it) } + + suspend fun refreshToken() { + val capturedRefresh = currentRefreshToken + + runCatching { + when { + capturedRefresh == null && creds == null -> throw AuthUnavailableException + capturedRefresh != null -> { + currentRefreshToken = null + val newTokenInfo = clientAuthFeature.refresh(capturedRefresh) + currentRefreshToken = newTokenInfo ?.refresh + if (newTokenInfo == null) { + refreshToken() + } else { + onAuthKeyUpdated(newTokenInfo) + refreshUser(newTokenInfo) + } + } + creds != null -> { + val newAuthTokenInfo = clientAuthFeature.auth(creds) + + if (newAuthTokenInfo != null) { + onAuthKeyUpdated(newAuthTokenInfo) + refreshUser(newAuthTokenInfo) + currentRefreshToken = newAuthTokenInfo.refresh + } + } + } + }.onFailure { + onAuthKeyInvalidated() + } + } + + sendPipeline.intercept(HttpSendPipeline.State) { + if (!context.url.buildString().startsWith(baseUrl) || clientAuthFeature.isAuthRequest(context)) { + return@intercept + } + context.expectSuccess = false + if (authMutex.isLocked) { + authMutex.withLock { /* do nothing, just wait while mutex will be freed */ } + } + } + + receivePipeline.intercept(HttpReceivePipeline.Before) { + if ( + context.request.url.toString().startsWith(baseUrl) + && context.response.status == HttpStatusCode.Unauthorized + ) { + authMutex.withLock { refreshToken() } + val newResponse = context.client ?.request{ + takeFrom(context.request) + } ?: return@intercept + proceedWith(newResponse) + } + } + } +} diff --git a/features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ui/AuthUIModel.kt b/features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ui/AuthUIModel.kt new file mode 100644 index 00000000..ef48152a --- /dev/null +++ b/features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ui/AuthUIModel.kt @@ -0,0 +1,8 @@ +package dev.inmo.postssystem.features.auth.client.ui + +import dev.inmo.postssystem.features.auth.common.AuthCreds +import dev.inmo.postssystem.features.common.common.UIModel + +interface AuthUIModel : UIModel { + suspend fun initAuth(serverUrl: String, creds: AuthCreds) +} diff --git a/features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ui/AuthUIState.kt b/features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ui/AuthUIState.kt new file mode 100644 index 00000000..aeaf4f5f --- /dev/null +++ b/features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ui/AuthUIState.kt @@ -0,0 +1,20 @@ +package dev.inmo.postssystem.features.auth.client.ui + +import kotlinx.serialization.Serializable + +@Serializable +sealed class AuthUIError +@Serializable +object ServerUnavailableAuthUIError : AuthUIError() +@Serializable +object AuthIncorrectAuthUIError : AuthUIError() + +@Serializable +sealed class AuthUIState +@Serializable +data class InitAuthUIState(val showError: AuthUIError? = null) : AuthUIState() +val DefaultInitAuthUIState = InitAuthUIState() +@Serializable +object LoadingAuthUIState : AuthUIState() +@Serializable +object AuthorizedAuthUIState : AuthUIState() diff --git a/features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ui/AuthUIViewModel.kt b/features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ui/AuthUIViewModel.kt new file mode 100644 index 00000000..be1d3945 --- /dev/null +++ b/features/auth/client/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/client/ui/AuthUIViewModel.kt @@ -0,0 +1,34 @@ +package dev.inmo.postssystem.features.auth.client.ui + +import dev.inmo.postssystem.features.auth.common.AuthCreds +import dev.inmo.postssystem.features.common.common.UIViewModel +import dev.inmo.postssystem.features.users.common.Username +import kotlinx.coroutines.flow.StateFlow + +class AuthUIViewModel( + private val model: AuthUIModel +) : UIViewModel { + override val currentState: StateFlow + get() = model.currentState + + private fun checkIncomingData( + serverUrl: String, + username: String, + password: String + ): Boolean { + return serverUrl.isNotBlank() && username.isNotBlank() && password.isNotBlank() + } + + suspend fun initAuth( + serverUrl: String, + username: String, + password: String + ) { + if (checkIncomingData(serverUrl, username, password)) { + model.initAuth( + serverUrl.takeIf { it.startsWith("http") } ?: "http://$serverUrl", + AuthCreds(Username(username), password) + ) + } + } +} diff --git a/features/auth/client/src/main/AndroidManifest.xml b/features/auth/client/src/main/AndroidManifest.xml new file mode 100644 index 00000000..6d0e9ea0 --- /dev/null +++ b/features/auth/client/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/business_cases/post_creating/common/build.gradle b/features/auth/common/build.gradle similarity index 63% rename from business_cases/post_creating/common/build.gradle rename to features/auth/common/build.gradle index 132aba83..95e0dd8c 100644 --- a/business_cases/post_creating/common/build.gradle +++ b/features/auth/common/build.gradle @@ -10,10 +10,8 @@ kotlin { sourceSets { commonMain { dependencies { - implementation kotlin('stdlib') - - api project(":postssystem.core.ktor.common") - api project(":postssystem.publishing.ktor.common") + api project(":postssystem.features.common.common") + api project(":postssystem.features.users.common") } } } diff --git a/features/auth/common/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/common/AuthFeature.kt b/features/auth/common/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/common/AuthFeature.kt new file mode 100644 index 00000000..bf6d6396 --- /dev/null +++ b/features/auth/common/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/common/AuthFeature.kt @@ -0,0 +1,9 @@ +package dev.inmo.postssystem.features.auth.common + +import dev.inmo.postssystem.features.users.common.User + +interface AuthFeature { + suspend fun auth(creds: AuthCreds): AuthTokenInfo? + suspend fun refresh(refresh: RefreshToken): AuthTokenInfo? + suspend fun getMe(authToken: AuthToken): User? +} diff --git a/features/auth/common/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/common/AuthModels.kt b/features/auth/common/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/common/AuthModels.kt new file mode 100644 index 00000000..6398c1d1 --- /dev/null +++ b/features/auth/common/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/common/AuthModels.kt @@ -0,0 +1,36 @@ +package dev.inmo.postssystem.features.auth.common + +import com.benasher44.uuid.uuid4 +import dev.inmo.postssystem.features.users.common.Username +import kotlinx.serialization.* +import kotlin.jvm.JvmInline + +sealed interface AuthKey + +@Serializable +@SerialName("authcreds") +data class AuthCreds( + val username: Username, + val password: String +): AuthKey + + +@Serializable +@SerialName("token") +@JvmInline +value class AuthToken(val string: String = uuid4().toString()): AuthKey { + override fun toString(): String = string +} + +@Serializable +@SerialName("refresh") +@JvmInline +value class RefreshToken(val string: String = uuid4().toString()): AuthKey { + override fun toString(): String = string +} + +@Serializable +data class AuthTokenInfo( + val token: AuthToken, + val refresh: RefreshToken +) diff --git a/features/auth/common/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/common/Constants.kt b/features/auth/common/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/common/Constants.kt new file mode 100644 index 00000000..c4e73e0a --- /dev/null +++ b/features/auth/common/src/commonMain/kotlin/dev/inmo/postssystem/features/auth/common/Constants.kt @@ -0,0 +1,8 @@ +package dev.inmo.postssystem.features.auth.common + +const val tokenSessionKey = "token" + +const val authRootPathPart = "auth" +const val authAuthPathPart = "auth" +const val authRefreshPathPart = "refresh" +const val authGetMePathPart = "getMe" diff --git a/features/auth/common/src/main/AndroidManifest.xml b/features/auth/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000..fa5134fc --- /dev/null +++ b/features/auth/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/features/auth/server/build.gradle b/features/auth/server/build.gradle new file mode 100644 index 00000000..40e02224 --- /dev/null +++ b/features/auth/server/build.gradle @@ -0,0 +1,17 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" +} + +apply from: "$mppJavaProjectPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.auth.common") + api project(":postssystem.features.common.server") + } + } + } +} diff --git a/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/AuthenticationRoutingConfigurator.kt b/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/AuthenticationRoutingConfigurator.kt new file mode 100644 index 00000000..74ee9d7a --- /dev/null +++ b/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/AuthenticationRoutingConfigurator.kt @@ -0,0 +1,121 @@ +package dev.inmo.postssystem.features.auth.server + +import dev.inmo.postssystem.features.auth.common.* +import dev.inmo.postssystem.features.auth.server.tokens.AuthTokensService +import dev.inmo.postssystem.features.common.server.sessions.ApplicationAuthenticationConfigurator +import dev.inmo.postssystem.features.users.common.User +import dev.inmo.micro_utils.coroutines.safely +import dev.inmo.micro_utils.ktor.server.configurators.* +import dev.inmo.micro_utils.ktor.server.unianswer +import dev.inmo.micro_utils.ktor.server.uniload +import io.ktor.application.* +import io.ktor.auth.* +import io.ktor.http.HttpStatusCode +import io.ktor.response.respond +import io.ktor.routing.* +import io.ktor.sessions.* +import kotlinx.serialization.builtins.nullable + +data class AuthUserPrincipal( + val user: User +) : Principal + +fun User.principal() = AuthUserPrincipal(this) + + +class AuthenticationRoutingConfigurator( + private val authFeature: AuthFeature, + private val authTokensService: AuthTokensService +) : ApplicationRoutingConfigurator.Element, ApplicationAuthenticationConfigurator.Element { + override fun Route.invoke() { + route(authRootPathPart) { + post(authAuthPathPart) { + safely( + { + // TODO:: add error info + it.printStackTrace() + call.respond( + HttpStatusCode.InternalServerError, + "Something went wrong" + ) + } + ) { + val creds = call.uniload(AuthCreds.serializer()) + + val tokenInfo = authFeature.auth(creds) + + if (tokenInfo == null) { + if (call.response.status() == null) { + call.respond(HttpStatusCode.Forbidden) + } + } else { + call.sessions.set(tokenSessionKey, tokenInfo.token) + call.unianswer( + AuthTokenInfo.serializer().nullable, + tokenInfo + ) + } + } + } + post(authRefreshPathPart) { + safely( + { + // TODO:: add error info + call.respond( + HttpStatusCode.InternalServerError, + "Something went wrong" + ) + } + ) { + val refreshToken = call.uniload(RefreshToken.serializer()) + + val tokenInfo = authFeature.refresh(refreshToken) + + if (tokenInfo == null) { + if (call.response.status() == null) { + call.respond(HttpStatusCode.Forbidden) + } + } else { + call.sessions.set(tokenSessionKey, tokenInfo.token) + call.unianswer( + AuthTokenInfo.serializer().nullable, + tokenInfo + ) + } + } + } + post(authGetMePathPart) { + safely( + { + // TODO:: add error info + call.respond( + HttpStatusCode.InternalServerError, + "Something went wrong" + ) + } + ) { + call.unianswer( + User.serializer().nullable, + authFeature.getMe( + call.uniload(AuthToken.serializer()) + ) + ) + } + } + } + } + + override fun Authentication.Configuration.invoke() { + session { + validate { + val result = authTokensService.getUserPrincipal(it) + if (result.isSuccess) { + result.getOrThrow().principal() + } else { + null + } + } + challenge { call.respond(HttpStatusCode.Unauthorized) } + } + } +} diff --git a/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/SessionAuthenticationConfigurator.kt b/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/SessionAuthenticationConfigurator.kt new file mode 100644 index 00000000..7e51f895 --- /dev/null +++ b/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/SessionAuthenticationConfigurator.kt @@ -0,0 +1,23 @@ +package dev.inmo.postssystem.features.auth.server + +import dev.inmo.postssystem.features.auth.common.AuthToken +import dev.inmo.postssystem.features.common.common.Milliseconds +import dev.inmo.postssystem.features.auth.common.tokenSessionKey +import dev.inmo.micro_utils.ktor.server.configurators.ApplicationSessionsConfigurator +import io.ktor.sessions.* +import java.util.concurrent.TimeUnit + +class SessionAuthenticationConfigurator( + private val maxAge: Milliseconds +) : ApplicationSessionsConfigurator.Element { + private val maxAgeInSeconds = TimeUnit.MILLISECONDS.toSeconds(maxAge) + override fun Sessions.Configuration.invoke() { + cookie(tokenSessionKey) { + cookie.maxAgeInSeconds = maxAgeInSeconds + serializer = object : SessionSerializer { + override fun deserialize(text: String): AuthToken = AuthToken(text) + override fun serialize(session: AuthToken): String = session.string + } + } + } +} diff --git a/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/AuthTokensRepo.kt b/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/AuthTokensRepo.kt new file mode 100644 index 00000000..1fd0ced4 --- /dev/null +++ b/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/AuthTokensRepo.kt @@ -0,0 +1,22 @@ +package dev.inmo.postssystem.features.auth.server.tokens + +import com.soywiz.klock.DateTime +import dev.inmo.postssystem.features.auth.common.AuthToken +import dev.inmo.postssystem.features.auth.common.RefreshToken +import dev.inmo.postssystem.features.users.common.UserId +import dev.inmo.micro_utils.repos.CRUDRepo + +data class AuthTokenModel( + val token: AuthToken, + val refreshToken: RefreshToken, + val userId: UserId, + val expiring: DateTime, + val die: DateTime +) + +interface AuthTokensRepo : CRUDRepo { + suspend fun getByRefreshToken(refreshToken: RefreshToken): AuthTokenModel? + suspend fun replaceToken(toRemove: AuthToken, toInsert: AuthTokenModel): Boolean + suspend fun deleteDied(now: DateTime = DateTime.now()) +} + diff --git a/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/AuthTokensService.kt b/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/AuthTokensService.kt new file mode 100644 index 00000000..e360943c --- /dev/null +++ b/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/AuthTokensService.kt @@ -0,0 +1,18 @@ +package dev.inmo.postssystem.features.auth.server.tokens + +import dev.inmo.postssystem.features.auth.common.* +import dev.inmo.postssystem.features.users.common.User + +sealed class AuthTokenException : Exception() + +object AuthTokenExpiredException : AuthTokenException() +object AuthTokenNotFoundException : AuthTokenException() +object UserNotFoundException : AuthTokenException() + + +interface AuthTokensService { + /** + * @return [User] or one of failure exceptions: [AuthTokenException] + */ + suspend fun getUserPrincipal(authToken: AuthToken): Result +} diff --git a/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/DefaultAuthTokensService.kt b/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/DefaultAuthTokensService.kt new file mode 100644 index 00000000..49aec53c --- /dev/null +++ b/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/DefaultAuthTokensService.kt @@ -0,0 +1,184 @@ +package dev.inmo.postssystem.features.auth.server.tokens + +import com.soywiz.klock.DateTime +import com.soywiz.klock.milliseconds +import dev.inmo.postssystem.features.auth.common.* +import dev.inmo.postssystem.features.common.common.Milliseconds +import dev.inmo.postssystem.features.users.common.* +import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions +import dev.inmo.micro_utils.repos.create +import dev.inmo.micro_utils.repos.deleteById +import dev.inmo.micro_utils.repos.exposed.AbstractExposedCRUDRepo +import dev.inmo.micro_utils.repos.exposed.initTable +import kotlinx.coroutines.* +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.statements.InsertStatement +import org.jetbrains.exposed.sql.statements.UpdateStatement +import org.jetbrains.exposed.sql.transactions.transaction + +private class ExposedAuthTokensRepo( + override val database: Database +) : AuthTokensRepo, AbstractExposedCRUDRepo( + tableName = "ExposedAuthTokensRepo" +) { + private val tokenColumn = text("token") + private val refreshTokenColumn = text("refreshToken") + private val userIdColumn = long("userId") + private val expiringColumn = double("expiring") + private val dieColumn = double("die") + override val primaryKey: PrimaryKey = PrimaryKey(tokenColumn) + + override val selectByIds: SqlExpressionBuilder.(List) -> Op = { + tokenColumn.inList(it.map { it.string }) + } + + override val selectById: SqlExpressionBuilder.(AuthToken) -> Op = { + tokenColumn.eq(it.string) + } + override val ResultRow.asObject: AuthTokenModel + get() = AuthTokenModel( + AuthToken(get(tokenColumn)), + RefreshToken(get(refreshTokenColumn)), + UserId(get(userIdColumn)), + DateTime(get(expiringColumn)), + DateTime(get(dieColumn)) + ) + + init { + initTable() + } + + override fun insert(value: AuthTokenModel, it: InsertStatement) { + it[tokenColumn] = value.token.string + it[refreshTokenColumn] = value.refreshToken.string + it[userIdColumn] = value.userId.long + it[expiringColumn] = value.expiring.unixMillisDouble + it[dieColumn] = value.die.unixMillisDouble + } + + override fun update(id: AuthToken, value: AuthTokenModel, it: UpdateStatement) { + it[tokenColumn] = value.token.string + it[refreshTokenColumn] = value.refreshToken.string + it[userIdColumn] = value.userId.long + it[expiringColumn] = value.expiring.unixMillisDouble + it[dieColumn] = value.die.unixMillisDouble + } + + override fun InsertStatement.asObject(value: AuthTokenModel): AuthTokenModel = AuthTokenModel( + AuthToken(get(tokenColumn)), + RefreshToken(get(refreshTokenColumn)), + UserId(get(userIdColumn)), + DateTime(get(expiringColumn)), + DateTime(get(dieColumn)) + ) + + override suspend fun getByRefreshToken(refreshToken: RefreshToken): AuthTokenModel? = transaction(database) { + select { refreshTokenColumn.eq(refreshToken.string) }.limit(1).firstOrNull() ?.asObject + } + + override suspend fun replaceToken(toRemove: AuthToken, toInsert: AuthTokenModel): Boolean = transaction { + deleteWhere { tokenColumn.eq(toRemove.string) } > 0 && insert { + insert(toInsert, it) + }.insertedCount > 0 + } + + override suspend fun deleteDied( + now: DateTime + ) = transaction(database) { + val nowAsDouble = now.unixMillisDouble + val tokens = select { dieColumn.less(nowAsDouble) }.map { it[tokenColumn] } + deleteWhere { dieColumn.less(nowAsDouble) } + tokens + }.forEach { + deleteObjectsIdsChannel.emit(AuthToken(it)) + } +} + +class DefaultAuthTokensService( + private val authTokensRepo: AuthTokensRepo, + private val usersRepo: ReadUsersStorage, + private val userAuthenticator: UserAuthenticator, + private val tokenLifetime: Milliseconds, + private val cleaningScope: CoroutineScope +) : AuthTokensService, AuthFeature { + private val tokenDieLifetime = tokenLifetime * 2 + + constructor( + database: Database, + usersRepo: ReadUsersStorage, + userAuthenticator: UserAuthenticator, + tokenLifetime: Milliseconds, + cleaningScope: CoroutineScope + ): this( + ExposedAuthTokensRepo(database), + usersRepo, + userAuthenticator, + tokenLifetime, + cleaningScope + ) + + init { + cleaningScope.launchSafelyWithoutExceptions { + while (isActive) { + authTokensRepo.deleteDied() + delay(tokenLifetime) + } + } + } + + override suspend fun getUserPrincipal(authToken: AuthToken): Result { + val authTokenModel = authTokensRepo.getById(authToken) ?: return Result.failure(AuthTokenNotFoundException) + return if (authTokenModel.expiring < DateTime.now()) { + Result.failure(AuthTokenExpiredException) + } else { + val user = usersRepo.getById(authTokenModel.userId) ?: return Result.failure(UserNotFoundException) + Result.success(user) + } + } + + override suspend fun auth(creds: AuthCreds): AuthTokenInfo? { + val user = userAuthenticator(creds) ?: return null + val now = DateTime.now() + val preAuthTokenModel = AuthTokenModel( + AuthToken(), + RefreshToken(), + user.id, + now + tokenLifetime.milliseconds, + now + tokenDieLifetime.milliseconds + ) + val tokenModel = authTokensRepo.create(preAuthTokenModel).firstOrNull() ?: return null + return AuthTokenInfo( + tokenModel.token, + tokenModel.refreshToken + ) + } + + override suspend fun refresh(refresh: RefreshToken): AuthTokenInfo? { + val previousAuthTokenModel = authTokensRepo.getByRefreshToken(refresh) ?: return null + val now = DateTime.now() + + if (previousAuthTokenModel.die < now) { + authTokensRepo.deleteById(previousAuthTokenModel.token) + return null + } + + val newAuthTokenModel = AuthTokenModel( + AuthToken(), + RefreshToken(), + previousAuthTokenModel.userId, + now + tokenLifetime.milliseconds, + now + tokenDieLifetime.milliseconds + ) + return if (authTokensRepo.replaceToken(previousAuthTokenModel.token, newAuthTokenModel)) { + AuthTokenInfo(newAuthTokenModel.token, newAuthTokenModel.refreshToken) + } else { + null + } + } + + override suspend fun getMe(authToken: AuthToken): User? { + return usersRepo.getById( + authTokensRepo.getById(authToken) ?.userId ?: return null + ) + } +} diff --git a/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/UserAuthenticator.kt b/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/UserAuthenticator.kt new file mode 100644 index 00000000..c3d877db --- /dev/null +++ b/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/UserAuthenticator.kt @@ -0,0 +1,9 @@ +package dev.inmo.postssystem.features.auth.server.tokens + +import dev.inmo.postssystem.features.auth.common.AuthCreds +import dev.inmo.postssystem.features.users.common.User + +fun interface UserAuthenticator { + suspend operator fun invoke(authCreds: AuthCreds): User? +} + diff --git a/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/UsersAuths.kt b/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/UsersAuths.kt new file mode 100644 index 00000000..30e90067 --- /dev/null +++ b/features/auth/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/auth/server/tokens/UsersAuths.kt @@ -0,0 +1,44 @@ +package dev.inmo.postssystem.features.auth.server.tokens + +import dev.inmo.postssystem.features.auth.common.AuthCreds +import dev.inmo.postssystem.features.users.common.* +import dev.inmo.micro_utils.repos.exposed.AbstractExposedReadCRUDRepo +import dev.inmo.micro_utils.repos.exposed.initTable +import org.jetbrains.exposed.sql.* + +private class ExposedUsersAuthenticationRepo( + override val database: Database, + private val usersRepo: ExposedUsersStorage +) : AbstractExposedReadCRUDRepo("UsersAuthentications") { + private val passwordColumn = text("password") + private val userIdColumn = long("userid").uniqueIndex() references usersRepo.userIdColumn + + override val primaryKey: PrimaryKey = PrimaryKey(userIdColumn) + + override val ResultRow.asObject: UserId + get() = UserId(get(userIdColumn)) + override val selectById: SqlExpressionBuilder.(AuthCreds) -> Op = { + usersRepo.select { + usersRepo.usernameColumn.eq(it.username.string) + }.firstOrNull() ?.get(usersRepo.userIdColumn) ?.let { userId -> + userIdColumn.eq(userId).and(passwordColumn.eq(it.password)) + } ?: Op.FALSE + } + + init { + initTable() + uniqueIndex("${tableName}_user_password", userIdColumn, passwordColumn) + } +} + +fun exposedUsersAuthenticator( + database: Database, + usersRepo: ExposedUsersStorage +): UserAuthenticator { + val usersAuthenticatorRepo = ExposedUsersAuthenticationRepo(database, usersRepo) + return UserAuthenticator { + val userId = usersAuthenticatorRepo.getById(it) ?: return@UserAuthenticator null + usersRepo.getById(userId) + } +} + diff --git a/core/ktor/client/build.gradle b/features/common/client/build.gradle similarity index 52% rename from core/ktor/client/build.gradle rename to features/common/client/build.gradle index 440347fd..8e304e7f 100644 --- a/core/ktor/client/build.gradle +++ b/features/common/client/build.gradle @@ -10,12 +10,10 @@ kotlin { sourceSets { commonMain { dependencies { - implementation kotlin('stdlib') - - api "dev.inmo:micro_utils.pagination.ktor.common:$microutils_version" - api "dev.inmo:micro_utils.ktor.client:$microutils_version" - - api project(":postssystem.core.ktor.common") + api project(":postssystem.features.common.common") + api "dev.inmo:micro_utils.repos.ktor.client:$microutils_version" + api "io.ktor:ktor-client-auth:$ktor_version" + api "io.ktor:ktor-client-logging:$ktor_version" } } } diff --git a/features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/DefaultUIFlow.kt b/features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/DefaultUIFlow.kt new file mode 100644 index 00000000..f9fe1895 --- /dev/null +++ b/features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/DefaultUIFlow.kt @@ -0,0 +1,7 @@ +package dev.inmo.postssystem.features.common.common + +import kotlinx.coroutines.flow.MutableStateFlow + +inline fun DefaultMVVMStateFlow( + state: T +) = MutableStateFlow(state) diff --git a/features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/UIModel.kt b/features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/UIModel.kt new file mode 100644 index 00000000..970fc36c --- /dev/null +++ b/features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/UIModel.kt @@ -0,0 +1,15 @@ +package dev.inmo.postssystem.features.common.common + +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +interface UIModel { + val currentState: StateFlow +} + +abstract class AbstractUIModel( + initState: StateType +) : UIModel { + protected val _currentState = DefaultMVVMStateFlow(initState) + override val currentState: StateFlow = _currentState.asStateFlow() +} diff --git a/features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/UIView.kt b/features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/UIView.kt new file mode 100644 index 00000000..3e2be96a --- /dev/null +++ b/features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/UIView.kt @@ -0,0 +1,4 @@ +package dev.inmo.postssystem.features.common.common + +interface UIView { +} diff --git a/features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/UIViewModel.kt b/features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/UIViewModel.kt new file mode 100644 index 00000000..306d5260 --- /dev/null +++ b/features/common/client/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/UIViewModel.kt @@ -0,0 +1,15 @@ +package dev.inmo.postssystem.features.common.common + +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +interface UIViewModel { + val currentState: StateFlow +} + +abstract class AbstractUIViewModel : UIViewModel { + protected val _currentState = DefaultMVVMStateFlow(initState()) + override val currentState: StateFlow = _currentState.asStateFlow() + + abstract fun initState(): StateType +} diff --git a/features/common/client/src/main/AndroidManifest.xml b/features/common/client/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b7562922 --- /dev/null +++ b/features/common/client/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/features/common/common/build.gradle b/features/common/common/build.gradle new file mode 100644 index 00000000..d88baba6 --- /dev/null +++ b/features/common/common/build.gradle @@ -0,0 +1,30 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppProjectWithSerializationPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api "dev.inmo:micro_utils.common:$microutils_version" + api "dev.inmo:micro_utils.serialization.typed_serializer:$microutils_version" + api "io.insert-koin:koin-core:$koin_version" + api "com.benasher44:uuid:$uuid_version" + } + } + jvmMain { + dependencies { + api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + } + } + androidMain { + dependencies { + api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + } + } + } +} diff --git a/features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/CommonJson.kt b/features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/CommonJson.kt new file mode 100644 index 00000000..50951f9c --- /dev/null +++ b/features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/CommonJson.kt @@ -0,0 +1,11 @@ +package dev.inmo.postssystem.features.common.common + +import kotlinx.serialization.json.Json + +val DefaultJson = Json { + ignoreUnknownKeys = true + +} + +val Json.default + get() = DefaultJson diff --git a/features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/KoinGetAllDistinct.kt b/features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/KoinGetAllDistinct.kt new file mode 100644 index 00000000..8115a04e --- /dev/null +++ b/features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/KoinGetAllDistinct.kt @@ -0,0 +1,5 @@ +package dev.inmo.postssystem.features.common.common + +import org.koin.core.scope.Scope + +inline fun Scope.getAllDistinct() = getAll().distinct() diff --git a/features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/Typealiases.kt b/features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/Typealiases.kt new file mode 100644 index 00000000..d6980308 --- /dev/null +++ b/features/common/common/src/commonMain/kotlin/dev/inmo/postssystem/features/common/common/Typealiases.kt @@ -0,0 +1,3 @@ +package dev.inmo.postssystem.features.common.common + +typealias Milliseconds = Long diff --git a/features/common/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/common/common/KoinSingleWithBinds.kt b/features/common/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/common/common/KoinSingleWithBinds.kt new file mode 100644 index 00000000..8e9a026b --- /dev/null +++ b/features/common/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/common/common/KoinSingleWithBinds.kt @@ -0,0 +1,16 @@ +package dev.inmo.postssystem.features.common.common + +import org.koin.core.definition.Definition +import org.koin.core.instance.InstanceFactory +import org.koin.core.module.Module +import org.koin.core.qualifier.Qualifier +import org.koin.dsl.binds +import kotlin.reflect.full.allSuperclasses + +inline fun Module.singleWithBinds( + qualifier: Qualifier? = null, + createdAtStart: Boolean = false, + noinline definition: Definition +): Pair> { + return single(qualifier, createdAtStart, definition) binds (T::class.allSuperclasses.toTypedArray()) +} diff --git a/features/common/common/src/main/AndroidManifest.xml b/features/common/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000..bde7f1df --- /dev/null +++ b/features/common/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/publishing/exposed/build.gradle b/features/common/server/build.gradle similarity index 53% rename from publishing/exposed/build.gradle rename to features/common/server/build.gradle index 24ab9cd6..dc631750 100644 --- a/publishing/exposed/build.gradle +++ b/features/common/server/build.gradle @@ -9,17 +9,16 @@ kotlin { sourceSets { commonMain { dependencies { - implementation kotlin('stdlib') + api project(":postssystem.features.common.common") api "dev.inmo:micro_utils.repos.exposed:$microutils_version" - - api project(":postssystem.publishing.api") + api "dev.inmo:micro_utils.repos.ktor.server:$microutils_version" + api "dev.inmo:micro_utils.ktor.server:$microutils_version" } } jvmMain { dependencies { - implementation "org.xerial:sqlite-jdbc:$test_sqlite_version" - implementation "org.jetbrains.kotlin:kotlin-test" - implementation "org.jetbrains.kotlin:kotlin-test-junit" + api "io.ktor:ktor-auth:$ktor_version" + api "ch.qos.logback:logback-classic:$logback_version" } } } diff --git a/features/common/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/common/server/sessions/ApplicationAuthenticationConfigurator.kt b/features/common/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/common/server/sessions/ApplicationAuthenticationConfigurator.kt new file mode 100644 index 00000000..c4eaf084 --- /dev/null +++ b/features/common/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/common/server/sessions/ApplicationAuthenticationConfigurator.kt @@ -0,0 +1,18 @@ +package dev.inmo.postssystem.features.common.server.sessions + +import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator +import io.ktor.application.Application +import io.ktor.auth.Authentication +import io.ktor.auth.authentication + +class ApplicationAuthenticationConfigurator( + private val elements: List +) : KtorApplicationConfigurator { + fun interface Element { operator fun Authentication.Configuration.invoke() } + + override fun Application.configure() { + authentication { + elements.forEach { it.apply { invoke() } } + } + } +} diff --git a/core/ktor/common/build.gradle b/features/files/client/build.gradle similarity index 62% rename from core/ktor/common/build.gradle rename to features/files/client/build.gradle index 7ca05c0e..06f00298 100644 --- a/core/ktor/common/build.gradle +++ b/features/files/client/build.gradle @@ -10,11 +10,8 @@ kotlin { sourceSets { commonMain { dependencies { - implementation kotlin('stdlib') - - api "dev.inmo:micro_utils.ktor.common:$microutils_version" - - api project(":postssystem.core.api") + api project(":postssystem.features.files.common") + api project(":postssystem.features.common.client") } } } diff --git a/features/files/client/src/commonMain/kotlin/dev/inmo/postssystem/features/files/client/ClientFilesStorage.kt b/features/files/client/src/commonMain/kotlin/dev/inmo/postssystem/features/files/client/ClientFilesStorage.kt new file mode 100644 index 00000000..e41fc789 --- /dev/null +++ b/features/files/client/src/commonMain/kotlin/dev/inmo/postssystem/features/files/client/ClientFilesStorage.kt @@ -0,0 +1,48 @@ +package dev.inmo.postssystem.features.files.client + +import dev.inmo.postssystem.features.files.common.* +import dev.inmo.postssystem.features.files.common.storage.FilesStorage +import dev.inmo.micro_utils.ktor.client.UnifiedRequester +import dev.inmo.micro_utils.ktor.common.buildStandardUrl +import dev.inmo.micro_utils.repos.ReadCRUDRepo +import dev.inmo.micro_utils.repos.ktor.client.crud.KtorReadStandardCrudRepo +import io.ktor.client.HttpClient +import io.ktor.client.request.post +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.readBytes +import kotlinx.serialization.BinaryFormat +import kotlinx.serialization.builtins.nullable + +class ClientFilesStorage( + baseUrl: String, + private val client: HttpClient, + private val serialFormat: BinaryFormat +) : FilesStorage, ReadCRUDRepo by KtorReadStandardCrudRepo( + buildStandardUrl(baseUrl, filesRootPathPart), + UnifiedRequester(client, serialFormat), + MetaFileInfoStorageWrapper.serializer(), + MetaFileInfoStorageWrapper.serializer().nullable, + FileId.serializer() +) { + private val unifiedRequester = UnifiedRequester(client, serialFormat) + private val fullFilesPath = buildStandardUrl(baseUrl, filesRootPathPart) + private val fullFilesGetBytesPath = buildStandardUrl( + fullFilesPath, + filesGetFilesPathPart + ) + + override suspend fun getBytes(id: FileId): ByteArray = client.post(fullFilesGetBytesPath) { + body = serialFormat.encodeToByteArray(FileId.serializer(), id) + }.readBytes() + + override suspend fun getFullFileInfo( + id: FileId + ): FullFileInfoStorageWrapper? = unifiedRequester.uniget( + buildStandardUrl( + fullFilesPath, + filesGetFullFileInfoPathPart, + filesFileIdParameter to unifiedRequester.encodeUrlQueryValue(FileId.serializer(), id) + ), + FullFileInfoStorageWrapper.serializer().nullable + ) +} diff --git a/features/files/client/src/main/AndroidManifest.xml b/features/files/client/src/main/AndroidManifest.xml new file mode 100644 index 00000000..20740703 --- /dev/null +++ b/features/files/client/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/core/api/build.gradle b/features/files/common/build.gradle similarity index 64% rename from core/api/build.gradle rename to features/files/common/build.gradle index 1d52a85b..df71df3b 100644 --- a/core/api/build.gradle +++ b/features/files/common/build.gradle @@ -10,13 +10,9 @@ kotlin { sourceSets { commonMain { dependencies { - api kotlin('reflect') - - api "com.soywiz.korlibs.klock:klock:$klockVersion" - api "dev.inmo:micro_utils.common:$microutils_version" - api "dev.inmo:micro_utils.coroutines:$microutils_version" - api "dev.inmo:micro_utils.repos.common:$microutils_version" + api project(":postssystem.features.common.common") api "dev.inmo:micro_utils.mime_types:$microutils_version" + api "dev.inmo:micro_utils.repos.common:$microutils_version" } } } diff --git a/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/Constants.kt b/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/Constants.kt new file mode 100644 index 00000000..e103af8a --- /dev/null +++ b/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/Constants.kt @@ -0,0 +1,6 @@ +package dev.inmo.postssystem.features.files.common + +const val filesRootPathPart = "files" +const val filesGetFilesPathPart = "getFiles" +const val filesGetFullFileInfoPathPart = "getFullFileInfo" +const val filesFileIdParameter = "fileId" diff --git a/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/FileInfo.kt b/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/FileInfo.kt new file mode 100644 index 00000000..9834820d --- /dev/null +++ b/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/FileInfo.kt @@ -0,0 +1,35 @@ +package dev.inmo.postssystem.features.files.common + +import dev.inmo.micro_utils.common.* +import dev.inmo.micro_utils.mime_types.MimeType +import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable + +@Serializable(FileInfoSerializer::class) +sealed interface FileInfo { + val name: FileName + val mimeType: MimeType + + companion object { + fun serializer(): KSerializer = FileInfoSerializer + } +} + +object FileInfoSerializer : KSerializer by TypedSerializer( + "meta" to MetaFileInfo.serializer(), + "full" to FullFileInfo.serializer(), +) + +@Serializable +data class MetaFileInfo(override val name: FileName, override val mimeType: MimeType) : FileInfo + +@Serializable +data class FullFileInfo( + override val name: FileName, + override val mimeType: MimeType, + @Serializable(ByteArrayAllocatorSerializer::class) + val byteArrayAllocator: ByteArrayAllocator +) : FileInfo + +fun FullFileInfo.toMetaFileInfo() = MetaFileInfo(name, mimeType) diff --git a/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/FileInfoWrapper.kt b/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/FileInfoWrapper.kt new file mode 100644 index 00000000..e07d93d1 --- /dev/null +++ b/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/FileInfoWrapper.kt @@ -0,0 +1,21 @@ +package dev.inmo.postssystem.features.files.common + +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmInline + + +@Serializable +@JvmInline +value class FileId(val string: String) { + override fun toString(): String = string.toString() +} + +@Serializable +sealed class FileInfoStorageWrapper { + abstract val id: FileId + abstract val fileInfo: FileInfo +} +@Serializable +data class MetaFileInfoStorageWrapper(override val id: FileId, override val fileInfo: MetaFileInfo) : FileInfoStorageWrapper() +@Serializable +data class FullFileInfoStorageWrapper(override val id: FileId, override val fileInfo: FullFileInfo) : FileInfoStorageWrapper() diff --git a/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/storage/FilesStorage.kt b/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/storage/FilesStorage.kt new file mode 100644 index 00000000..90bf5360 --- /dev/null +++ b/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/storage/FilesStorage.kt @@ -0,0 +1,9 @@ +package dev.inmo.postssystem.features.files.common.storage + +import dev.inmo.postssystem.features.files.common.* +import dev.inmo.micro_utils.repos.ReadCRUDRepo + +interface FilesStorage : ReadCRUDRepo { + suspend fun getBytes(id: FileId): ByteArray + suspend fun getFullFileInfo(id: FileId): FullFileInfoStorageWrapper? +} diff --git a/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/storage/FullFilesStorage.kt b/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/storage/FullFilesStorage.kt new file mode 100644 index 00000000..6ddc513c --- /dev/null +++ b/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/storage/FullFilesStorage.kt @@ -0,0 +1,8 @@ +package dev.inmo.postssystem.features.files.common.storage + +interface FullFilesStorage : FilesStorage, WriteFilesStorage + +class DefaultFullFilesStorage( + filesStorage: FilesStorage, + writeFilesStorage: WriteFilesStorage +) : FullFilesStorage, FilesStorage by filesStorage, WriteFilesStorage by writeFilesStorage diff --git a/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/storage/WriteFilesStorage.kt b/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/storage/WriteFilesStorage.kt new file mode 100644 index 00000000..9c5ad61d --- /dev/null +++ b/features/files/common/src/commonMain/kotlin/dev/inmo/postssystem/features/files/common/storage/WriteFilesStorage.kt @@ -0,0 +1,6 @@ +package dev.inmo.postssystem.features.files.common.storage + +import dev.inmo.postssystem.features.files.common.* +import dev.inmo.micro_utils.repos.WriteCRUDRepo + +interface WriteFilesStorage : WriteCRUDRepo diff --git a/features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/DiskFilesStorage.kt b/features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/DiskFilesStorage.kt new file mode 100644 index 00000000..c2fe9d5f --- /dev/null +++ b/features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/DiskFilesStorage.kt @@ -0,0 +1,63 @@ +package dev.inmo.postssystem.features.files.common + +import dev.inmo.postssystem.features.files.common.storage.FilesStorage +import dev.inmo.micro_utils.pagination.* +import dev.inmo.micro_utils.repos.ReadKeyValueRepo +import java.io.File + +class DiskFilesStorage( + private val filesFolder: File, + private val metasKeyValueRepo: ReadKeyValueRepo +) : FilesStorage { + private val FileId.file + get() = File(filesFolder, string) + + init { + if (!filesFolder.exists()) { + filesFolder.mkdirs() + } else { + require(filesFolder.isDirectory) { "$filesFolder must be a directory" } + } + } + + private suspend fun FileId.meta(): MetaFileInfoStorageWrapper? { + return MetaFileInfoStorageWrapper( + this, + metasKeyValueRepo.get(this) ?: return null + ) + } + + override suspend fun getBytes(id: FileId): ByteArray = id.file.readBytes() + + override suspend fun getFullFileInfo(id: FileId): FullFileInfoStorageWrapper? = getById( + id + ) ?.let { + FullFileInfoStorageWrapper( + id, + FullFileInfo( + it.fileInfo.name, + it.fileInfo.mimeType + ) { + id.file.readBytes() + } + ) + } + + override suspend fun contains(id: FileId): Boolean = metasKeyValueRepo.contains(id) + + override suspend fun count(): Long = metasKeyValueRepo.count() + + override suspend fun getById(id: FileId): MetaFileInfoStorageWrapper? = id.meta() + + override suspend fun getByPagination(pagination: Pagination): PaginationResult { + val keys = metasKeyValueRepo.keys(pagination) + return keys.changeResults( + keys.results.mapNotNull { + MetaFileInfoStorageWrapper( + it, + metasKeyValueRepo.get(it) ?: return@mapNotNull null + ) + } + ) + } +} diff --git a/features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/MetasKeyValueRepo.kt b/features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/MetasKeyValueRepo.kt new file mode 100644 index 00000000..6fff50b6 --- /dev/null +++ b/features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/MetasKeyValueRepo.kt @@ -0,0 +1,15 @@ +package dev.inmo.postssystem.features.files.common + +import dev.inmo.micro_utils.repos.KeyValueRepo +import dev.inmo.micro_utils.repos.mappers.withMapper +import kotlinx.serialization.StringFormat + +fun MetasKeyValueRepo( + serialFormat: StringFormat, + originalRepo: KeyValueRepo +) = originalRepo.withMapper( + { string }, + { serialFormat.encodeToString(MetaFileInfo.serializer(), this) }, + { FileId(this) }, + { serialFormat.decodeFromString(MetaFileInfo.serializer(), this) } +) diff --git a/features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/WriteDistFilesStorage.kt b/features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/WriteDistFilesStorage.kt new file mode 100644 index 00000000..d70645b4 --- /dev/null +++ b/features/files/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/common/WriteDistFilesStorage.kt @@ -0,0 +1,69 @@ +package dev.inmo.postssystem.features.files.common + +import com.benasher44.uuid.uuid4 +import dev.inmo.postssystem.features.files.common.storage.WriteFilesStorage +import dev.inmo.micro_utils.repos.* +import kotlinx.coroutines.flow.* +import java.io.File + +class WriteDistFilesStorage( + private val filesFolder: File, + private val metasKeyValueRepo: WriteKeyValueRepo +) : WriteFilesStorage { + private val FileId.file + get() = File(filesFolder, string) + + private val _deletedObjectsIdsFlow = MutableSharedFlow() + private val _newObjectsFlow = MutableSharedFlow() + private val _updatedObjectsFlow = MutableSharedFlow() + override val deletedObjectsIdsFlow: Flow = _deletedObjectsIdsFlow.asSharedFlow() + override val newObjectsFlow: Flow = _newObjectsFlow.asSharedFlow() + override val updatedObjectsFlow: Flow = _updatedObjectsFlow.asSharedFlow() + + init { + if (!filesFolder.exists()) { + filesFolder.mkdirs() + } else { + require(filesFolder.isDirectory) { "$filesFolder must be a directory" } + } + } + + override suspend fun create(values: List): List = values.map { + var newId: FileId + var file: File + do { + newId = FileId(uuid4().toString()) + file = newId.file + } while (file.exists()) + metasKeyValueRepo.set(newId, it.toMetaFileInfo()) + file.writeBytes(it.byteArrayAllocator()) + FullFileInfoStorageWrapper(newId, it) + } + + override suspend fun deleteById(ids: List) { + ids.forEach { + if (it.file.delete()) { + metasKeyValueRepo.unset(it) + _deletedObjectsIdsFlow.emit(it) + } + } + } + + override suspend fun update( + id: FileId, + value: FullFileInfo + ): FullFileInfoStorageWrapper? = id.file.takeIf { it.exists() } ?.writeBytes(value.byteArrayAllocator()) ?.let { + val result = FullFileInfoStorageWrapper(id, value.copy()) + + metasKeyValueRepo.set(id, value.toMetaFileInfo()) + _updatedObjectsFlow.emit(result) + + result + } + + override suspend fun update( + values: List> + ): List = values.mapNotNull { (id, file) -> + update(id, file) + } +} diff --git a/features/files/common/src/main/AndroidManifest.xml b/features/files/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000..39fefc5e --- /dev/null +++ b/features/files/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/features/files/server/build.gradle b/features/files/server/build.gradle new file mode 100644 index 00000000..06a79375 --- /dev/null +++ b/features/files/server/build.gradle @@ -0,0 +1,17 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" +} + +apply from: "$mppJavaProjectPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.files.common") + api project(":postssystem.features.common.server") + } + } + } +} diff --git a/features/files/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/server/FilesRoutingConfigurator.kt b/features/files/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/server/FilesRoutingConfigurator.kt new file mode 100644 index 00000000..76cd54ca --- /dev/null +++ b/features/files/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/files/server/FilesRoutingConfigurator.kt @@ -0,0 +1,57 @@ +package dev.inmo.postssystem.features.files.server + +import dev.inmo.postssystem.features.files.common.* +import dev.inmo.postssystem.features.files.common.storage.* +import dev.inmo.micro_utils.ktor.server.* +import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator +import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes +import dev.inmo.micro_utils.repos.ktor.server.crud.configureWriteStandardCrudRepoRoutes +import io.ktor.application.call +import io.ktor.auth.authenticate +import io.ktor.response.respondBytes +import io.ktor.routing.* +import kotlinx.serialization.builtins.nullable + +class FilesRoutingConfigurator( + private val filesStorage: FilesStorage, + private val writeFilesStorage: WriteFilesStorage? +) : ApplicationRoutingConfigurator.Element { + constructor(fullFilesStorage: FullFilesStorage) : this(fullFilesStorage, fullFilesStorage) + + override fun Route.invoke() { + authenticate { + route(filesRootPathPart) { + configureReadStandardCrudRepoRoutes( + filesStorage, + MetaFileInfoStorageWrapper.serializer(), + MetaFileInfoStorageWrapper.serializer().nullable, + FileId.serializer() + ) + writeFilesStorage ?.let { + configureWriteStandardCrudRepoRoutes( + writeFilesStorage, + FullFileInfoStorageWrapper.serializer(), + FullFileInfoStorageWrapper.serializer().nullable, + FullFileInfo.serializer(), + FileId.serializer() + ) + } + post(filesGetFilesPathPart) { + call.respondBytes( + filesStorage.getBytes( + call.uniload(FileId.serializer()) + ) + ) + } + get(filesGetFullFileInfoPathPart) { + call.unianswer( + FullFileInfoStorageWrapper.serializer().nullable, + filesStorage.getFullFileInfo( + call.decodeUrlQueryValueOrSendError(filesFileIdParameter, FileId.serializer()) ?: return@get + ) + ) + } + } + } + } +} diff --git a/features/roles/client/build.gradle b/features/roles/client/build.gradle new file mode 100644 index 00000000..dcc31f41 --- /dev/null +++ b/features/roles/client/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppProjectWithSerializationPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.roles.common") + api project(":postssystem.features.common.client") + } + } + } +} diff --git a/features/roles/client/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/client/ClientUsersRolesStorage.kt b/features/roles/client/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/client/ClientUsersRolesStorage.kt new file mode 100644 index 00000000..e1531e4f --- /dev/null +++ b/features/roles/client/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/client/ClientUsersRolesStorage.kt @@ -0,0 +1,17 @@ +package dev.inmo.postssystem.features.roles.client + +import dev.inmo.postssystem.features.roles.common.* +import dev.inmo.micro_utils.ktor.client.UnifiedRequester +import kotlinx.serialization.KSerializer + +class ClientUsersRolesStorage( + private val baseUrl: String, + private val unifiedRequester: UnifiedRequester, + private val serializer: KSerializer +) : UsersRolesStorage, + ReadUsersRolesStorage by ReadClientUsersRolesStorage( + baseUrl, unifiedRequester, serializer + ), + WriteUsersRolesStorage by WriteClientUsersRolesStorage( + baseUrl, unifiedRequester, serializer + ) diff --git a/features/roles/client/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/client/ReadClientUsersRolesStorage.kt b/features/roles/client/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/client/ReadClientUsersRolesStorage.kt new file mode 100644 index 00000000..696f833e --- /dev/null +++ b/features/roles/client/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/client/ReadClientUsersRolesStorage.kt @@ -0,0 +1,70 @@ +package dev.inmo.postssystem.features.roles.client + +import dev.inmo.postssystem.features.roles.common.* +import dev.inmo.postssystem.features.users.common.UserId +import dev.inmo.micro_utils.ktor.client.UnifiedRequester +import dev.inmo.micro_utils.ktor.common.buildStandardUrl +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.serializer + +class ReadClientUsersRolesStorage( + private val baseUrl: String, + private val unifiedRequester: UnifiedRequester, + private val serializer: KSerializer +) : ReadUsersRolesStorage { + private val userRolesSerializer = ListSerializer(serializer) + + private val userRolesFullUrl = buildStandardUrl( + baseUrl, + usersRolesRootPathPart + ) + + override suspend fun getUsers( + userRole: T + ): List = unifiedRequester.uniget( + buildStandardUrl( + userRolesFullUrl, + usersRolesGetUsersPathPart, + usersRolesUserRoleQueryParameterName to unifiedRequester.encodeUrlQueryValue(serializer, userRole) + ), + UsersIdsSerializer + ) + + override suspend fun getRoles( + userId: UserId + ): List = unifiedRequester.uniget( + buildStandardUrl( + userRolesFullUrl, + usersRolesGetRolesPathPart, + usersRolesUserIdQueryParameterName to unifiedRequester.encodeUrlQueryValue(UserId.serializer(), userId) + ), + userRolesSerializer + ) + + override suspend fun contains( + userId: UserId, + userRole: T + ): Boolean = unifiedRequester.uniget( + buildStandardUrl( + userRolesFullUrl, + usersRolesContainsPathPart, + usersRolesUserIdQueryParameterName to unifiedRequester.encodeUrlQueryValue(UserId.serializer(), userId), + usersRolesUserRoleQueryParameterName to unifiedRequester.encodeUrlQueryValue(serializer, userRole) + ), + Boolean.serializer() + ) + + override suspend fun containsAny( + userId: UserId, + userRoles: List + ): Boolean = unifiedRequester.uniget( + buildStandardUrl( + userRolesFullUrl, + usersRolesContainsAnyPathPart, + usersRolesUserIdQueryParameterName to unifiedRequester.encodeUrlQueryValue(UserId.serializer(), userId), + usersRolesUserRoleQueryParameterName to unifiedRequester.encodeUrlQueryValue(userRolesSerializer, userRoles) + ), + Boolean.serializer() + ) +} diff --git a/features/roles/client/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/client/WriteClientUsersRolesStorage.kt b/features/roles/client/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/client/WriteClientUsersRolesStorage.kt new file mode 100644 index 00000000..781988cc --- /dev/null +++ b/features/roles/client/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/client/WriteClientUsersRolesStorage.kt @@ -0,0 +1,52 @@ +package dev.inmo.postssystem.features.roles.client + +import dev.inmo.postssystem.features.roles.common.* +import dev.inmo.postssystem.features.users.common.UserId +import dev.inmo.micro_utils.ktor.client.UnifiedRequester +import dev.inmo.micro_utils.ktor.common.buildStandardUrl +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer + +class WriteClientUsersRolesStorage( + private val baseUrl: String, + private val unifiedRequester: UnifiedRequester, + private val serializer: KSerializer +) : WriteUsersRolesStorage { + private val wrapperSerializer = UserRolesStorageIncludeExcludeWrapper.serializer( + serializer + ) + private val userRolesFullUrl = buildStandardUrl( + baseUrl, + usersRolesRootPathPart + ) + private val includeFullUrl = buildStandardUrl( + userRolesFullUrl, + usersRolesIncludePathPart + ) + private val excludeFullUrl = buildStandardUrl( + userRolesFullUrl, + usersRolesExcludePathPart + ) + + override suspend fun include( + userId: UserId, + userRole: T + ): Boolean = unifiedRequester.unipost( + includeFullUrl, + wrapperSerializer to UserRolesStorageIncludeExcludeWrapper( + userId, userRole + ), + Boolean.serializer() + ) + + override suspend fun exclude( + userId: UserId, + userRole: T + ): Boolean = unifiedRequester.unipost( + excludeFullUrl, + wrapperSerializer to UserRolesStorageIncludeExcludeWrapper( + userId, userRole + ), + Boolean.serializer() + ) +} diff --git a/features/roles/client/src/main/AndroidManifest.xml b/features/roles/client/src/main/AndroidManifest.xml new file mode 100644 index 00000000..2cc8c4f7 --- /dev/null +++ b/features/roles/client/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/features/roles/common/build.gradle b/features/roles/common/build.gradle new file mode 100644 index 00000000..95e0dd8c --- /dev/null +++ b/features/roles/common/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppProjectWithSerializationPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.common.common") + api project(":postssystem.features.users.common") + } + } + } +} diff --git a/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/Constants.kt b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/Constants.kt new file mode 100644 index 00000000..5bbece7c --- /dev/null +++ b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/Constants.kt @@ -0,0 +1,26 @@ +package dev.inmo.postssystem.features.roles.common + +import dev.inmo.postssystem.features.users.common.UserId +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer + +const val usersRolesRootPathPart = "roles" + +val UsersIdsSerializer = ListSerializer(UserId.serializer()) + +const val usersRolesUserRoleQueryParameterName = "userRole" +const val usersRolesUserIdQueryParameterName = "userId" + +const val usersRolesGetUsersPathPart = "getUsersByRole" +const val usersRolesGetRolesPathPart = "getUserRoles" +const val usersRolesContainsPathPart = "contains" +const val usersRolesContainsAnyPathPart = "containsAny" + +const val usersRolesIncludePathPart = "include" +const val usersRolesExcludePathPart = "exclude" + +@Serializable +data class UserRolesStorageIncludeExcludeWrapper( + val userId: UserId, + val userRole: T +) diff --git a/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/UserRole.kt b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/UserRole.kt new file mode 100644 index 00000000..1526f7e3 --- /dev/null +++ b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/UserRole.kt @@ -0,0 +1,81 @@ +package dev.inmo.postssystem.features.roles.common + +import kotlinx.serialization.* +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.* +import kotlinx.serialization.json.* + +@Serializable(UserRoleSerializer::class) +interface UserRole { // temporarily made as class while interfaces are bugged + companion object { + fun serializer() = UserRoleSerializer + } +} + +@Serializable +data class UnknownUserRole(val originalJson: JsonElement) : UserRole + +@Serializer(UserRole::class) +object UserRoleSerializer : KSerializer { + private val userRoleFormat = Json { ignoreUnknownKeys = true } + private const val keyField = "key" + private const val valueField = "value" + private val serializers = mutableMapOf>() + override val descriptor: SerialDescriptor = String.serializer().descriptor + + @InternalSerializationApi + override fun deserialize(decoder: Decoder): UserRole { + return if (decoder is JsonDecoder) { + val originalJson = decoder.decodeJsonElement().jsonObject + val type = originalJson[keyField]?.jsonPrimitive ?.content + return if (type == null || !serializers.containsKey(type)) { + UnknownUserRole(originalJson) + } else { + userRoleFormat.decodeFromJsonElement( + serializers.getValue(type), + originalJson[valueField] ?: buildJsonObject { } + ) + } + } else { + val encoded = decoder.decodeString() + userRoleFormat.decodeFromString(this, encoded) + } + } + + @InternalSerializationApi + private fun T.toJson(): JsonElement { + return userRoleFormat.encodeToJsonElement(this::class.serializer() as KSerializer, this) + } + + @InternalSerializationApi + override fun serialize(encoder: Encoder, value: UserRole) { + if (encoder is JsonEncoder) { + if (value is UnknownUserRole) { + encoder.encodeJsonElement(value.originalJson) + } else { + val valueSerializer = value::class.serializer() + val type = serializers.keys.first { serializers[it] == valueSerializer } + encoder.encodeJsonElement( + buildJsonObject { + put(keyField, type) + put(valueField, value.toJson()) + } + ) + } + } else { + encoder.encodeString( + userRoleFormat.encodeToString(this, value) + ) + } + } + + fun includeSerializer( + type: String, + kSerializer: KSerializer + ) { serializers[type] = kSerializer } + + fun excludeSerializer(type: String) { + serializers.remove(type) + } +} diff --git a/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/UserRolesSerializer.kt b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/UserRolesSerializer.kt new file mode 100644 index 00000000..0d758b68 --- /dev/null +++ b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/UserRolesSerializer.kt @@ -0,0 +1,5 @@ +package dev.inmo.postssystem.features.roles.common + +import kotlinx.serialization.builtins.ListSerializer + +val UserRolesSerializer = ListSerializer(UserRole.serializer()) diff --git a/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/UsersRolesStorage.kt b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/UsersRolesStorage.kt new file mode 100644 index 00000000..2751872a --- /dev/null +++ b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/UsersRolesStorage.kt @@ -0,0 +1,16 @@ +package dev.inmo.postssystem.features.roles.common + +import dev.inmo.postssystem.features.users.common.UserId + +interface ReadUsersRolesStorage { + suspend fun getUsers(userRole: T): List + suspend fun getRoles(userId: UserId): List + suspend fun contains(userId: UserId, userRole: T): Boolean + suspend fun containsAny(userId: UserId, userRoles: List): Boolean +} +interface WriteUsersRolesStorage { + suspend fun include(userId: UserId, userRole: T): Boolean + suspend fun exclude(userId: UserId, userRole: T): Boolean +} + +interface UsersRolesStorage : ReadUsersRolesStorage, WriteUsersRolesStorage diff --git a/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/KeyValueOriginalRepoTypealias.kt b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/KeyValueOriginalRepoTypealias.kt new file mode 100644 index 00000000..d5aaead8 --- /dev/null +++ b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/KeyValueOriginalRepoTypealias.kt @@ -0,0 +1,5 @@ +package dev.inmo.postssystem.features.roles.common.keyvalue + +import dev.inmo.micro_utils.repos.KeyValuesRepo + +typealias KeyValuesUsersRolesOriginalRepo = KeyValuesRepo diff --git a/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/KeyValueUsersRolesStorage.kt b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/KeyValueUsersRolesStorage.kt new file mode 100644 index 00000000..83bff41e --- /dev/null +++ b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/KeyValueUsersRolesStorage.kt @@ -0,0 +1,13 @@ +package dev.inmo.postssystem.features.roles.common.keyvalue + +import dev.inmo.postssystem.features.roles.common.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.StringFormat + +open class KeyValueUsersRolesStorage( + private val keyValuesRepo: KeyValuesUsersRolesOriginalRepo, + private val serializer: KSerializer, + private val format: StringFormat = ReadKeyValueUsersRolesStorage.defaultJson +) : UsersRolesStorage, + ReadUsersRolesStorage by ReadKeyValueUsersRolesStorage(keyValuesRepo, serializer, format), + WriteUsersRolesStorage by WriteKeyValueUsersRolesStorage(keyValuesRepo, serializer, format) diff --git a/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/ReadKeyValueUsersRolesStorage.kt b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/ReadKeyValueUsersRolesStorage.kt new file mode 100644 index 00000000..4d4fd21d --- /dev/null +++ b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/ReadKeyValueUsersRolesStorage.kt @@ -0,0 +1,59 @@ +package dev.inmo.postssystem.features.roles.common.keyvalue + +import dev.inmo.postssystem.features.common.common.default +import dev.inmo.postssystem.features.roles.common.ReadUsersRolesStorage +import dev.inmo.postssystem.features.roles.common.UserRole +import dev.inmo.postssystem.features.users.common.UserId +import dev.inmo.micro_utils.pagination.changeResults +import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging +import dev.inmo.micro_utils.repos.ReadKeyValuesRepo +import kotlinx.serialization.KSerializer +import kotlinx.serialization.StringFormat +import kotlinx.serialization.json.Json + +open class ReadKeyValueUsersRolesStorage( + private val keyValuesRepo: ReadKeyValuesRepo, + private val serializer: KSerializer, + private val format: StringFormat = defaultJson +) : ReadUsersRolesStorage { + override suspend fun getUsers(userRole: T): List { + val serialized = format.encodeToString(serializer, userRole) + + return keyValuesRepo.getAllByWithNextPaging { + keys(serialized, it).let { paginationResult -> + paginationResult.changeResults( + paginationResult.results.map { UserId(it) } + ) + } + } + } + + override suspend fun getRoles(userId: UserId): List { + return keyValuesRepo.getAllByWithNextPaging { + get(userId.long, it).let { paginationResult -> + paginationResult.changeResults( + paginationResult.results.map { serialized -> + format.decodeFromString(serializer, serialized) + } + ) + } + } + } + + override suspend fun contains(userId: UserId, userRole: T): Boolean { + val serialized = format.encodeToString(serializer, userRole) + + return keyValuesRepo.contains(userId.long, serialized) + } + + override suspend fun containsAny(userId: UserId, userRoles: List): Boolean { + return userRoles.any { + contains(userId, it) + } + } + + companion object { + internal val defaultJson = Json.default + } +} + diff --git a/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/WriteKeyValueUsersRolesStorage.kt b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/WriteKeyValueUsersRolesStorage.kt new file mode 100644 index 00000000..59c4aeb3 --- /dev/null +++ b/features/roles/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/common/keyvalue/WriteKeyValueUsersRolesStorage.kt @@ -0,0 +1,34 @@ +package dev.inmo.postssystem.features.roles.common.keyvalue + +import dev.inmo.postssystem.features.roles.common.UserRole +import dev.inmo.postssystem.features.roles.common.WriteUsersRolesStorage +import dev.inmo.postssystem.features.users.common.UserId +import dev.inmo.micro_utils.repos.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.StringFormat + +open class WriteKeyValueUsersRolesStorage( + private val keyValuesRepo: WriteKeyValuesRepo, + private val serializer: KSerializer, + private val format: StringFormat = ReadKeyValueUsersRolesStorage.defaultJson +) : WriteUsersRolesStorage { + override suspend fun include(userId: UserId, userRole: T): Boolean { + return runCatching { + keyValuesRepo.add( + userId.long, + format.encodeToString(serializer, userRole) + ) + true + }.getOrElse { false } + } + + override suspend fun exclude(userId: UserId, userRole: T): Boolean { + return runCatching { + keyValuesRepo.remove( + userId.long, + format.encodeToString(serializer, userRole) + ) + true + }.getOrElse { false } + } +} diff --git a/features/roles/common/src/main/AndroidManifest.xml b/features/roles/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000..aa7bc86e --- /dev/null +++ b/features/roles/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/publishing/ktor/client/build.gradle b/features/roles/manager/client/build.gradle similarity index 59% rename from publishing/ktor/client/build.gradle rename to features/roles/manager/client/build.gradle index 967b7c80..da44d278 100644 --- a/publishing/ktor/client/build.gradle +++ b/features/roles/manager/client/build.gradle @@ -10,11 +10,9 @@ kotlin { sourceSets { commonMain { dependencies { - implementation kotlin('stdlib') - - api "dev.inmo:micro_utils.ktor.client:$microutils_version" - - api project(":postssystem.publishing.ktor.common") + api project(":postssystem.features.roles.manager.common") + api project(":postssystem.features.common.client") + api project(":postssystem.features.roles.client") } } } diff --git a/features/roles/manager/client/src/main/AndroidManifest.xml b/features/roles/manager/client/src/main/AndroidManifest.xml new file mode 100644 index 00000000..e88a91a9 --- /dev/null +++ b/features/roles/manager/client/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/features/roles/manager/common/build.gradle b/features/roles/manager/common/build.gradle new file mode 100644 index 00000000..921ceb14 --- /dev/null +++ b/features/roles/manager/common/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppProjectWithSerializationPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.common.common") + api project(":postssystem.features.roles.common") + } + } + } +} diff --git a/features/roles/manager/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/manager/common/RolesManagerRole.kt b/features/roles/manager/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/manager/common/RolesManagerRole.kt new file mode 100644 index 00000000..8a9b6669 --- /dev/null +++ b/features/roles/manager/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/manager/common/RolesManagerRole.kt @@ -0,0 +1,32 @@ +package dev.inmo.postssystem.features.roles.manager.common + +import dev.inmo.postssystem.features.roles.common.UserRole +import dev.inmo.postssystem.features.roles.common.UserRoleSerializer +import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer +import kotlinx.serialization.Serializable + +@Serializable(RolesManagerRoleSerializer::class) +interface RolesManagerRole : UserRole { + companion object { + fun serializer() = RolesManagerRoleSerializer + } +} + +@Serializable +object GeneralRolesManagerRole : RolesManagerRole { + override fun toString(): String = "GeneralRolesManagerRole" +} + +private const val KEY = "roles_manager" + +object RolesManagerRoleSerializer : TypedSerializer( + RolesManagerRole::class, + mapOf( + "${KEY}_general" to GeneralRolesManagerRole.serializer() + ) +) { + init { + UserRoleSerializer.includeSerializer(KEY, RolesManagerRoleSerializer) + serializers.forEach { (k, v) -> UserRoleSerializer.includeSerializer(k, v) } + } +} diff --git a/features/roles/manager/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/manager/common/RolesManagerRoleStorage.kt b/features/roles/manager/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/manager/common/RolesManagerRoleStorage.kt new file mode 100644 index 00000000..b8e26d90 --- /dev/null +++ b/features/roles/manager/common/src/commonMain/kotlin/dev/inmo/postssystem/features/roles/manager/common/RolesManagerRoleStorage.kt @@ -0,0 +1,16 @@ +package dev.inmo.postssystem.features.roles.manager.common + +import dev.inmo.postssystem.features.common.common.default +import dev.inmo.postssystem.features.roles.common.UsersRolesStorage +import dev.inmo.postssystem.features.roles.common.keyvalue.* +import kotlinx.serialization.StringFormat +import kotlinx.serialization.json.Json + +class RolesManagerRoleStorage( + keyValuesRepo: KeyValuesUsersRolesOriginalRepo, + format: StringFormat = Json.default +) : UsersRolesStorage, KeyValueUsersRolesStorage( + keyValuesRepo, + RolesManagerRole.serializer(), + format +) diff --git a/features/roles/manager/common/src/main/AndroidManifest.xml b/features/roles/manager/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000..863b7a69 --- /dev/null +++ b/features/roles/manager/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/features/roles/manager/server/build.gradle b/features/roles/manager/server/build.gradle new file mode 100644 index 00000000..005f94a9 --- /dev/null +++ b/features/roles/manager/server/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" +} + +apply from: "$mppJavaProjectPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.roles.manager.common") + api project(":postssystem.features.common.server") + api project(":postssystem.features.roles.server") + } + } + } +} diff --git a/features/roles/manager/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/manager/server/RolesManagerRolesChecker.kt b/features/roles/manager/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/manager/server/RolesManagerRolesChecker.kt new file mode 100644 index 00000000..daffb0ce --- /dev/null +++ b/features/roles/manager/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/manager/server/RolesManagerRolesChecker.kt @@ -0,0 +1,18 @@ +package dev.inmo.postssystem.features.roles.manager.server + +import dev.inmo.postssystem.features.roles.common.ReadUsersRolesStorage +import dev.inmo.postssystem.features.roles.common.UserRole +import dev.inmo.postssystem.features.roles.manager.common.GeneralRolesManagerRole +import dev.inmo.postssystem.features.roles.server.RolesChecker +import dev.inmo.postssystem.features.users.common.User +import io.ktor.application.ApplicationCall + +object RolesManagerRolesChecker : RolesChecker { + override val key: String + get() = "RolesManagerRolesChecker" + + override suspend fun ApplicationCall.invoke( + usersRolesStorage: ReadUsersRolesStorage, + user: User + ): Boolean = usersRolesStorage.contains(user.id, GeneralRolesManagerRole) +} diff --git a/features/roles/manager/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/manager/server/RolesManagerUsersRolesStorageServerRoutesConfigurator.kt b/features/roles/manager/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/manager/server/RolesManagerUsersRolesStorageServerRoutesConfigurator.kt new file mode 100644 index 00000000..7a9f4481 --- /dev/null +++ b/features/roles/manager/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/manager/server/RolesManagerUsersRolesStorageServerRoutesConfigurator.kt @@ -0,0 +1,15 @@ +package dev.inmo.postssystem.features.roles.manager.server + +import dev.inmo.postssystem.features.roles.common.UsersRolesStorage +import dev.inmo.postssystem.features.roles.manager.common.RolesManagerRole +import dev.inmo.postssystem.features.roles.manager.common.RolesManagerRoleSerializer +import dev.inmo.postssystem.features.roles.server.UsersRolesStorageWriteServerRoutesConfigurator +import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator + +class RolesManagerUsersRolesStorageServerRoutesConfigurator( + storage: UsersRolesStorage +) : ApplicationRoutingConfigurator.Element by UsersRolesStorageWriteServerRoutesConfigurator( + storage, + RolesManagerRoleSerializer, + RolesManagerRolesChecker.key +) diff --git a/features/roles/server/build.gradle b/features/roles/server/build.gradle new file mode 100644 index 00000000..1479cc33 --- /dev/null +++ b/features/roles/server/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" +} + +apply from: "$mppJavaProjectPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.roles.common") + api project(":postssystem.features.common.server") + api project(":postssystem.features.auth.server") + } + } + } +} diff --git a/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/RolesChecker.kt b/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/RolesChecker.kt new file mode 100644 index 00000000..6e1bd406 --- /dev/null +++ b/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/RolesChecker.kt @@ -0,0 +1,29 @@ +package dev.inmo.postssystem.features.roles.server + +import dev.inmo.postssystem.features.roles.common.* +import dev.inmo.postssystem.features.users.common.User +import io.ktor.application.ApplicationCall + +interface RolesChecker { + val key: String + + suspend operator fun ApplicationCall.invoke( + usersRolesStorage: ReadUsersRolesStorage, + user: User + ): Boolean + + companion object { + fun default( + key: String, + role: T + ): RolesChecker = object : RolesChecker { + override val key: String + get() = key + + override suspend fun ApplicationCall.invoke( + usersRolesStorage: ReadUsersRolesStorage, + user: User + ): Boolean = usersRolesStorage.contains(user.id, role) + } + } +} diff --git a/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesAggregator.kt b/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesAggregator.kt new file mode 100644 index 00000000..8843c789 --- /dev/null +++ b/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesAggregator.kt @@ -0,0 +1,39 @@ +package dev.inmo.postssystem.features.roles.server + +import dev.inmo.postssystem.features.roles.common.UserRole +import dev.inmo.postssystem.features.roles.common.UsersRolesStorage +import dev.inmo.postssystem.features.users.common.UserId + +class UsersRolesAggregator( + private val otherStorages: List> +) : UsersRolesStorage { + private val otherStoragesByClass = otherStorages.associateBy { it.kclass } + + override suspend fun getUsers(userRole: UserRole): List { + return otherStoragesByClass[userRole::class] ?.getUsers(userRole) ?: emptyList() + } + + override suspend fun getRoles(userId: UserId): List = otherStorages.flatMap { it.getRoles(userId) } + + override suspend fun contains(userId: UserId, userRole: UserRole): Boolean { + return otherStoragesByClass[userRole::class] ?.contains(userId, userRole) ?: false + } + + override suspend fun containsAny(userId: UserId, userRoles: List): Boolean { + return userRoles.any { + contains(userId, it) + } + } + + override suspend fun include( + userId: UserId, + userRole: UserRole + ): Boolean = otherStoragesByClass[userRole::class] ?.include(userId, userRole) ?: false + + override suspend fun exclude( + userId: UserId, + userRole: UserRole + ): Boolean { + return otherStoragesByClass[userRole::class] ?.exclude(userId, userRole) ?: false + } +} diff --git a/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesAuthenticationConfigurator.kt b/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesAuthenticationConfigurator.kt new file mode 100644 index 00000000..48e4347b --- /dev/null +++ b/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesAuthenticationConfigurator.kt @@ -0,0 +1,40 @@ +package dev.inmo.postssystem.features.roles.server + +import dev.inmo.postssystem.features.auth.common.AuthToken +import dev.inmo.postssystem.features.auth.server.principal +import dev.inmo.postssystem.features.auth.server.tokens.AuthTokensService +import dev.inmo.postssystem.features.common.server.sessions.ApplicationAuthenticationConfigurator +import dev.inmo.postssystem.features.roles.common.UserRole +import dev.inmo.postssystem.features.roles.common.UsersRolesStorage +import io.ktor.application.call +import io.ktor.auth.Authentication +import io.ktor.auth.session +import io.ktor.http.HttpStatusCode +import io.ktor.response.respond + +class UsersRolesAuthenticationConfigurator( + private val usersRolesStorage: UsersRolesStorage, + private val authTokensService: AuthTokensService, + private val rolesCheckers: List> +) : ApplicationAuthenticationConfigurator.Element { + override fun Authentication.Configuration.invoke() { + rolesCheckers.forEach { checker -> + session(checker.key) { + validate { + val result = authTokensService.getUserPrincipal(it) + if (result.isSuccess) { + val user = result.getOrThrow().principal() + if (checker.run { invoke(usersRolesStorage, user.user) }) { + user + } else { + null + } + } else { + null + } + } + challenge { call.respond(HttpStatusCode.Unauthorized) } + } + } + } +} diff --git a/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesStorageHolder.kt b/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesStorageHolder.kt new file mode 100644 index 00000000..dbd0e878 --- /dev/null +++ b/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesStorageHolder.kt @@ -0,0 +1,42 @@ +package dev.inmo.postssystem.features.roles.server + +import dev.inmo.postssystem.features.roles.common.UserRole +import dev.inmo.postssystem.features.roles.common.UsersRolesStorage +import dev.inmo.postssystem.features.users.common.UserId +import dev.inmo.micro_utils.common.* +import kotlin.reflect.KClass + +data class UsersRolesStorageHolder( + val kclass: KClass, + val storage: UsersRolesStorage +) { + private suspend fun doIfRelevant( + userRole: UserRole, + block: suspend (T) -> R + ): Optional = if (kclass.isInstance(userRole)) { + block(userRole as T).optional + } else { + Optional.absent() + } + + suspend fun getUsers(userRole: UserRole): List? = doIfRelevant(userRole) { + storage.getUsers(it) + }.dataOrNull() + + suspend fun getRoles(userId: UserId): List = storage.getRoles(userId) + + suspend fun contains(userId: UserId, userRole: UserRole): Boolean? = doIfRelevant(userRole) { + storage.contains(userId, it) + }.dataOrNull() + + suspend fun include( + userId: UserId, + userRole: UserRole + ): Boolean? = doIfRelevant(userRole) { + storage.include(userId, it) + }.dataOrNull() + + suspend fun exclude(userId: UserId, userRole: UserRole): Boolean? = doIfRelevant(userRole) { + storage.exclude(userId, it) + }.dataOrNull() +} diff --git a/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesStorageReadServerRoutesConfigurator.kt b/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesStorageReadServerRoutesConfigurator.kt new file mode 100644 index 00000000..4b7ff250 --- /dev/null +++ b/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesStorageReadServerRoutesConfigurator.kt @@ -0,0 +1,73 @@ +package dev.inmo.postssystem.features.roles.server + +import dev.inmo.postssystem.features.roles.common.* +import dev.inmo.postssystem.features.users.common.UserId +import dev.inmo.micro_utils.ktor.server.* +import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator +import io.ktor.application.call +import io.ktor.auth.authenticate +import io.ktor.routing.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.serializer + +class UsersRolesStorageReadServerRoutesConfigurator( + private val storage: ReadUsersRolesStorage, + private val serializer: KSerializer +) : ApplicationRoutingConfigurator.Element { + private val userRolesSerializer = ListSerializer(serializer) + override fun Route.invoke() { + authenticate { + route(usersRolesRootPathPart) { + get(usersRolesGetUsersPathPart) { + val userRole = call.decodeUrlQueryValueOrSendError(usersRolesUserRoleQueryParameterName, serializer) + ?: return@get + call.unianswer( + UsersIdsSerializer, + storage.getUsers(userRole) + ) + } + + get(usersRolesGetRolesPathPart) { + val userId = + call.decodeUrlQueryValueOrSendError(usersRolesUserIdQueryParameterName, UserId.serializer()) + ?: return@get + call.unianswer( + userRolesSerializer, + storage.getRoles(userId) + ) + } + + get(usersRolesContainsPathPart) { + val userId = call.decodeUrlQueryValueOrSendError( + usersRolesUserIdQueryParameterName, + UserId.serializer() + ) ?: return@get + val userRole = call.decodeUrlQueryValueOrSendError( + usersRolesUserRoleQueryParameterName, + serializer + ) ?: return@get + call.unianswer( + Boolean.serializer(), + storage.contains(userId, userRole) + ) + } + + get(usersRolesContainsAnyPathPart) { + val userId = call.decodeUrlQueryValueOrSendError( + usersRolesUserIdQueryParameterName, + UserId.serializer() + ) ?: return@get + val userRoles = call.decodeUrlQueryValueOrSendError( + usersRolesUserRoleQueryParameterName, + userRolesSerializer + ) ?: return@get + call.unianswer( + Boolean.serializer(), + storage.containsAny(userId, userRoles) + ) + } + } + } + } +} diff --git a/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesStorageWriteServerRoutesConfigurator.kt b/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesStorageWriteServerRoutesConfigurator.kt new file mode 100644 index 00000000..b9e14417 --- /dev/null +++ b/features/roles/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/roles/server/UsersRolesStorageWriteServerRoutesConfigurator.kt @@ -0,0 +1,51 @@ +package dev.inmo.postssystem.features.roles.server + +import dev.inmo.postssystem.features.roles.common.* +import dev.inmo.micro_utils.ktor.server.* +import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator +import io.ktor.application.call +import io.ktor.auth.authenticate +import io.ktor.routing.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer + +class UsersRolesStorageWriteServerRoutesConfigurator( + private val storage: WriteUsersRolesStorage, + private val serializer: KSerializer, + private val includeAuthKey: String, + private val excludeAuthKey: String = includeAuthKey +) : ApplicationRoutingConfigurator.Element { + override fun Route.invoke() { + route(usersRolesRootPathPart) { + val wrapperSerializer = UserRolesStorageIncludeExcludeWrapper.serializer( + serializer + ) + authenticate(includeAuthKey) { + post(usersRolesIncludePathPart) { + val wrapper = call.uniload(wrapperSerializer) + + call.unianswer( + Boolean.serializer(), + storage.include( + wrapper.userId, + wrapper.userRole + ) + ) + } + } + authenticate(excludeAuthKey) { + post(usersRolesExcludePathPart) { + val wrapper = call.uniload(wrapperSerializer) + + call.unianswer( + Boolean.serializer(), + storage.exclude( + wrapper.userId, + wrapper.userRole + ) + ) + } + } + } + } +} diff --git a/features/status/client/build.gradle b/features/status/client/build.gradle new file mode 100644 index 00000000..02cb86de --- /dev/null +++ b/features/status/client/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppProjectWithSerializationPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.status.common") + api project(":postssystem.features.common.client") + } + } + } +} diff --git a/features/status/client/src/commonMain/kotlin/dev/inmo/postssystem/features/status/client/StatusFeatureClient.kt b/features/status/client/src/commonMain/kotlin/dev/inmo/postssystem/features/status/client/StatusFeatureClient.kt new file mode 100644 index 00000000..04aad62a --- /dev/null +++ b/features/status/client/src/commonMain/kotlin/dev/inmo/postssystem/features/status/client/StatusFeatureClient.kt @@ -0,0 +1,27 @@ +package dev.inmo.postssystem.features.status.client + +import dev.inmo.postssystem.features.status.common.statusAuthorisedPathPart +import dev.inmo.postssystem.features.status.common.statusRootPart +import dev.inmo.micro_utils.ktor.client.UnifiedRequester +import dev.inmo.micro_utils.ktor.common.buildStandardUrl +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.http.HttpStatusCode + +class StatusFeatureClient( + baseUrl: String, + private val client: HttpClient +) { + private val fullStatusUrl = buildStandardUrl( + baseUrl, + statusRootPart + ) + private val fullAuthorisedStatusUrl = buildStandardUrl( + fullStatusUrl, + statusAuthorisedPathPart + ) + + suspend fun checkServerStatus() = client.get(fullStatusUrl).status == HttpStatusCode.OK + suspend fun checkServerStatusWithAuth() = client.get(fullAuthorisedStatusUrl).status == HttpStatusCode.OK +} diff --git a/features/status/client/src/main/AndroidManifest.xml b/features/status/client/src/main/AndroidManifest.xml new file mode 100644 index 00000000..64ae9c83 --- /dev/null +++ b/features/status/client/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/publishing/api/build.gradle b/features/status/common/build.gradle similarity index 74% rename from publishing/api/build.gradle rename to features/status/common/build.gradle index a046f8bd..d4a562e5 100644 --- a/publishing/api/build.gradle +++ b/features/status/common/build.gradle @@ -10,11 +10,8 @@ kotlin { sourceSets { commonMain { dependencies { - implementation kotlin('stdlib') - - api project(":postssystem.core.api") + api project(":postssystem.features.common.common") } } } } - diff --git a/features/status/common/src/commonMain/kotlin/dev/inmo/postssystem/features/status/common/Constants.kt b/features/status/common/src/commonMain/kotlin/dev/inmo/postssystem/features/status/common/Constants.kt new file mode 100644 index 00000000..12d3450c --- /dev/null +++ b/features/status/common/src/commonMain/kotlin/dev/inmo/postssystem/features/status/common/Constants.kt @@ -0,0 +1,4 @@ +package dev.inmo.postssystem.features.status.common + +const val statusRootPart = "status" +const val statusAuthorisedPathPart = "auth" diff --git a/features/status/common/src/main/AndroidManifest.xml b/features/status/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000..32fae894 --- /dev/null +++ b/features/status/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/features/status/server/build.gradle b/features/status/server/build.gradle new file mode 100644 index 00000000..19917f81 --- /dev/null +++ b/features/status/server/build.gradle @@ -0,0 +1,17 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" +} + +apply from: "$mppJavaProjectPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.status.common") + api project(":postssystem.features.common.server") + } + } + } +} diff --git a/features/status/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/status/server/StatusRoutingConfigurator.kt b/features/status/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/status/server/StatusRoutingConfigurator.kt new file mode 100644 index 00000000..083402cd --- /dev/null +++ b/features/status/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/status/server/StatusRoutingConfigurator.kt @@ -0,0 +1,26 @@ +package dev.inmo.postssystem.features.status.server + +import dev.inmo.postssystem.features.status.common.statusAuthorisedPathPart +import dev.inmo.postssystem.features.status.common.statusRootPart +import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator +import io.ktor.application.call +import io.ktor.auth.authenticate +import io.ktor.http.HttpStatusCode +import io.ktor.response.respond +import io.ktor.routing.* + +object StatusRoutingConfigurator : ApplicationRoutingConfigurator.Element { + override fun Route.invoke() { + route(statusRootPart) { + get { + call.respond(HttpStatusCode.OK) + } + + authenticate { + get(statusAuthorisedPathPart) { + call.respond(HttpStatusCode.OK) + } + } + } + } +} diff --git a/features/template/client/build.gradle b/features/template/client/build.gradle new file mode 100644 index 00000000..07e61531 --- /dev/null +++ b/features/template/client/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppProjectWithSerializationPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.template.common") + api project(":postssystem.features.common.client") + } + } + } +} diff --git a/features/template/client/src/main/AndroidManifest.xml b/features/template/client/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f591b318 --- /dev/null +++ b/features/template/client/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/features/template/common/build.gradle b/features/template/common/build.gradle new file mode 100644 index 00000000..d4a562e5 --- /dev/null +++ b/features/template/common/build.gradle @@ -0,0 +1,17 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppProjectWithSerializationPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.common.common") + } + } + } +} diff --git a/features/template/common/src/main/AndroidManifest.xml b/features/template/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000..e074950c --- /dev/null +++ b/features/template/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/features/template/server/build.gradle b/features/template/server/build.gradle new file mode 100644 index 00000000..16812db8 --- /dev/null +++ b/features/template/server/build.gradle @@ -0,0 +1,17 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" +} + +apply from: "$mppJavaProjectPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.template.common") + api project(":postssystem.features.common.server") + } + } + } +} diff --git a/features/users/client/build.gradle b/features/users/client/build.gradle new file mode 100644 index 00000000..32f19276 --- /dev/null +++ b/features/users/client/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppProjectWithSerializationPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.users.common") + api project(":postssystem.features.common.client") + } + } + } +} diff --git a/features/users/client/src/commonMain/kotlin/dev/inmo/postssystem/features/users/client/UsersStorageKtorClient.kt b/features/users/client/src/commonMain/kotlin/dev/inmo/postssystem/features/users/client/UsersStorageKtorClient.kt new file mode 100644 index 00000000..21a4fffc --- /dev/null +++ b/features/users/client/src/commonMain/kotlin/dev/inmo/postssystem/features/users/client/UsersStorageKtorClient.kt @@ -0,0 +1,19 @@ +package dev.inmo.postssystem.features.users.client + +import dev.inmo.postssystem.features.users.common.* +import dev.inmo.micro_utils.ktor.client.UnifiedRequester +import dev.inmo.micro_utils.ktor.common.buildStandardUrl +import dev.inmo.micro_utils.repos.ReadCRUDRepo +import dev.inmo.micro_utils.repos.ktor.client.crud.KtorReadStandardCrudRepo +import kotlinx.serialization.builtins.nullable + +class UsersStorageKtorClient( + baseUrl: String, + unifiedRequester: UnifiedRequester +) : ReadUsersStorage, ReadCRUDRepo by KtorReadStandardCrudRepo( + buildStandardUrl(baseUrl, usersServerPathPart), + unifiedRequester, + User.serializer(), + User.serializer().nullable, + UserId.serializer() +) diff --git a/features/users/client/src/main/AndroidManifest.xml b/features/users/client/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f27161fd --- /dev/null +++ b/features/users/client/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/features/users/common/build.gradle b/features/users/common/build.gradle new file mode 100644 index 00000000..398ec6ca --- /dev/null +++ b/features/users/common/build.gradle @@ -0,0 +1,27 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppProjectWithSerializationPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.common.common") + api "dev.inmo:micro_utils.repos.common:$microutils_version" + } + } + jvmMain { + dependencies { + api "dev.inmo:micro_utils.repos.exposed:$microutils_version" + } + } + } +} + +android { + disableIncludingJvmCodeInAndroidPart() +} diff --git a/features/users/common/src/commonMain/kotlin/dev/inmo/postssystem/features/users/common/Routes.kt b/features/users/common/src/commonMain/kotlin/dev/inmo/postssystem/features/users/common/Routes.kt new file mode 100644 index 00000000..ab71c677 --- /dev/null +++ b/features/users/common/src/commonMain/kotlin/dev/inmo/postssystem/features/users/common/Routes.kt @@ -0,0 +1,3 @@ +package dev.inmo.postssystem.features.users.common + +const val usersServerPathPart = "users" diff --git a/features/users/common/src/commonMain/kotlin/dev/inmo/postssystem/features/users/common/User.kt b/features/users/common/src/commonMain/kotlin/dev/inmo/postssystem/features/users/common/User.kt new file mode 100644 index 00000000..644a64ae --- /dev/null +++ b/features/users/common/src/commonMain/kotlin/dev/inmo/postssystem/features/users/common/User.kt @@ -0,0 +1,46 @@ +package dev.inmo.postssystem.features.users.common + +import kotlinx.serialization.* +import kotlin.jvm.JvmInline + +@Serializable +@JvmInline +value class UserId(val long: Long) { + override fun toString(): String = long.toString() +} +val Long.userId: UserId + get() = UserId(this) + +@Serializable +@JvmInline +value class Username(val string: String) { + override fun toString(): String = string +} +val String.username: Username + get() = Username(this) + +sealed interface NewUser { + val firstName: String + val lastName: String + val username: Username +} + +@Serializable +sealed class User : NewUser { + abstract val id: UserId +} + +@Serializable +data class DefaultUser( + override val id: UserId, + override val firstName: String, + override val lastName: String, + override val username: Username, +) : User() + +@Serializable +data class DefaultNewUser( + override val firstName: String, + override val lastName: String, + override val username: Username, +) : NewUser diff --git a/features/users/common/src/commonMain/kotlin/dev/inmo/postssystem/features/users/common/UsersStorage.kt b/features/users/common/src/commonMain/kotlin/dev/inmo/postssystem/features/users/common/UsersStorage.kt new file mode 100644 index 00000000..ade2ba7a --- /dev/null +++ b/features/users/common/src/commonMain/kotlin/dev/inmo/postssystem/features/users/common/UsersStorage.kt @@ -0,0 +1,7 @@ +package dev.inmo.postssystem.features.users.common + +import dev.inmo.micro_utils.repos.* + +interface ReadUsersStorage : ReadCRUDRepo +interface WriteUsersStorage : WriteCRUDRepo +interface UsersStorage : ReadUsersStorage, WriteUsersStorage, CRUDRepo diff --git a/features/users/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/users/common/ExposedUsersStorage.kt b/features/users/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/users/common/ExposedUsersStorage.kt new file mode 100644 index 00000000..bdd42932 --- /dev/null +++ b/features/users/common/src/jvmMain/kotlin/dev/inmo/postssystem/features/users/common/ExposedUsersStorage.kt @@ -0,0 +1,51 @@ +package dev.inmo.postssystem.features.users.common + +import dev.inmo.micro_utils.repos.exposed.AbstractExposedCRUDRepo +import dev.inmo.micro_utils.repos.exposed.initTable +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.statements.InsertStatement +import org.jetbrains.exposed.sql.statements.UpdateStatement + +class ExposedUsersStorage(override val database: Database) : UsersStorage, AbstractExposedCRUDRepo( + tableName = "Users" +) { + val userIdColumn = long("userid").autoIncrement() + val usernameColumn = text("username") + private val firstNameColumn = text("firstName") + private val lastNameColumn = text("lastName") + + override val primaryKey: PrimaryKey = PrimaryKey(userIdColumn) + + override val selectByIds: SqlExpressionBuilder.(List) -> Op = { userIdColumn.inList(it.map { it.long }) } + override val selectById: SqlExpressionBuilder.(UserId) -> Op = { userIdColumn.eq(it.long) } + override val ResultRow.asObject: User + get() = DefaultUser( + get(userIdColumn).userId, + get(firstNameColumn), + get(lastNameColumn), + get(usernameColumn).username, + ) + + init { + initTable() + } + + override fun insert(value: NewUser, it: InsertStatement) { + it[usernameColumn] = value.username.string + it[firstNameColumn] = value.firstName + it[lastNameColumn] = value.lastName + } + + override fun update(id: UserId, value: NewUser, it: UpdateStatement) { + it[usernameColumn] = value.username.string + it[firstNameColumn] = value.firstName + it[lastNameColumn] = value.lastName + } + + override fun InsertStatement.asObject(value: NewUser): User = DefaultUser( + get(userIdColumn).userId, + get(firstNameColumn), + get(lastNameColumn), + get(usernameColumn).username, + ) +} diff --git a/features/users/common/src/main/AndroidManifest.xml b/features/users/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0a697ab6 --- /dev/null +++ b/features/users/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/features/users/server/build.gradle b/features/users/server/build.gradle new file mode 100644 index 00000000..286aa52c --- /dev/null +++ b/features/users/server/build.gradle @@ -0,0 +1,23 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" +} + +apply from: "$mppJavaProjectPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":postssystem.features.users.common") + api project(":postssystem.features.common.server") + } + } + jvmMain { + dependencies { + api "io.ktor:ktor-auth:$ktor_version" + api "io.ktor:ktor-server-sessions:$ktor_version" + } + } + } +} diff --git a/features/users/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/users/server/UsersStorageServerRoutesConfigurator.kt b/features/users/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/users/server/UsersStorageServerRoutesConfigurator.kt new file mode 100644 index 00000000..6edac1bd --- /dev/null +++ b/features/users/server/src/jvmMain/kotlin/dev/inmo/postssystem/features/users/server/UsersStorageServerRoutesConfigurator.kt @@ -0,0 +1,26 @@ +package dev.inmo.postssystem.features.users.server + +import dev.inmo.postssystem.features.users.common.* +import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator +import dev.inmo.micro_utils.repos.ktor.server.crud.configureReadStandardCrudRepoRoutes +import io.ktor.auth.authenticate +import io.ktor.routing.Route +import io.ktor.routing.route +import kotlinx.serialization.builtins.nullable + +class UsersStorageServerRoutesConfigurator( + private val usersStorage: ReadUsersStorage +) : ApplicationRoutingConfigurator.Element { + override fun Route.invoke() { + authenticate { + route(usersServerPathPart) { + configureReadStandardCrudRepoRoutes( + usersStorage, + User.serializer(), + User.serializer().nullable, + UserId.serializer() + ) + } + } + } +} diff --git a/gradle.properties b/gradle.properties index c0e95d17..0574ddd7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,49 +1,50 @@ kotlin.code.style=official org.gradle.parallel=true +org.gradle.jvmargs=-Xmx4g kotlin.js.generate.externals=true kotlin.incremental=true kotlin.incremental.js=true android.useAndroidX=true android.enableJetifier=true -org.gradle.jvmargs=-Xmx2048m -kotlin_version=1.4.31 -kotlin_coroutines_version=1.4.2 -kotlin_serialisation_core_version=1.1.0 +# Common -ktor_version=1.5.2 -klockVersion=2.0.6 -uuidVersion=0.2.3 +kotlin_version=1.5.31 +kotlin_serialisation_core_version=1.3.1 -exposed_version=0.29.1 -test_sqlite_version=3.32.3.2 +koin_version=3.1.2 +microutils_version=0.8.4 +ktor_version=1.6.5 +logback_version=1.2.6 +uuid_version=0.3.1 +klock_version=2.4.8 -microutils_version=0.4.27 +# Server -javax_activation_version=1.1.1 +kotlin_exposed_version=0.36.1 +psql_version=42.3.0 -github_release_plugin_version=2.2.12 +# JS + +kotlinx_html_version=0.7.3 +materialDesignLite=1.3.0 # ANDROID -core_ktx_version=1.3.2 -androidx_recycler_version=1.1.0 -appcompat_version=1.2.0 - android_minSdkVersion=21 -android_compileSdkVersion=30 -android_buildToolsVersion=30.0.2 -dexcount_version=2.0.0 +android_compileSdkVersion=31 +android_buildToolsVersion=31.0.0 +dexcount_version=3.0.0 junit_version=4.12 test_ext_junit_version=1.1.2 espresso_core=3.3.0 # Dokka -dokka_version=1.4.20 +dokka_version=1.5.31 # Project data -version=0.6.0 -android_code_version=1 group=dev.inmo +version=0.0.1 +android_code_version=1 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 9ab0a835..e708b1c0 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e68e77fb..1d690b12 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip zipStoreBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip diff --git a/gradlew b/gradlew index cccdd3d5..4f906e0c 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/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 @@ -28,7 +44,7 @@ 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="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -66,6 +82,7 @@ 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 @@ -109,10 +126,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# 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 @@ -138,19 +156,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + 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" ;; + 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 @@ -159,14 +177,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +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" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index e95643d6..107acd32 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,84 +1,89 @@ -@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 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= - -@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 init - -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 init - -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 - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -: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 %CMD_LINE_ARGS% - -: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 +@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/mimes_generator/mime_generator.py b/mimes_generator/mime_generator.py deleted file mode 100644 index ec9916a9..00000000 --- a/mimes_generator/mime_generator.py +++ /dev/null @@ -1,52 +0,0 @@ -import requests -from bs4 import BeautifulSoup -import pandas as pd -import itertools - -def fix_name(category, raw_name): - splitted = raw_name.replace('-', '+').replace('.', '+').replace(',', '+').split('+') - out1 = "" - for s in splitted: - out1 += s.capitalize() - - result = "" - if out1[0].isdigit(): - result += category[0].capitalize() - result += out1 - else: - result += out1 - return result - -if __name__ == '__main__': - df = pd.read_html(open('table.html', 'r')) - mimes = [] - for row in df[0].iterrows(): - mime = row[1][1] - mime_category = mime.split('/', 1)[0] - mime_name = mime.split('/', 1)[1] - mimes.append({ - 'mime_category': mime_category, - 'mime_name': mime_name, - }) - - # codegen - - mimes.sort(key=lambda x: x['mime_category']) - grouped = itertools.groupby(mimes, lambda x: x['mime_category']) - code = '' - code2 = 'internal val knownMimeTypes: Set = setOf(\n' - code2 += ' KnownMimeTypes.Any,\n' - for key, group in grouped: - group_name = key.capitalize() - code += '@Serializable(MimeTypeSerializer::class)\nsealed class %s(raw: String) : MimeType, KnownMimeTypes(raw) {\n' % group_name - code += ' @Serializable(MimeTypeSerializer::class)\n object Any: %s ("%s/*")\n' % (group_name, key) - for mime in group: - name = fix_name(mime['mime_category'], mime['mime_name']) - code += ' @Serializable(MimeTypeSerializer::class)\n object %s: %s ("%s/%s")\n' % (name, group_name, mime['mime_category'], mime['mime_name']) - code2 += ' KnownMimeTypes.%s.%s,\n' % (group_name, name) - code += '}\n\n' - code2 += ')\n' - with open('out1.txt', 'w') as file: - file.write(code) - with open('out2.txt', 'w') as file: - file.write(code2) diff --git a/mimes_generator/table.html b/mimes_generator/table.html deleted file mode 100644 index 607a20f5..00000000 --- a/mimes_generator/table.html +++ /dev/null @@ -1,4157 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameMIME Type / Internet Media TypeFile ExtensionMore Details
Andrew Toolkitapplication/andrew-insetN/AIANA - Andrew Inset
Applixwareapplication/applixware.awVistasource
Atom Syndication Formatapplication/atom+xml.atom, .xmlRFC 4287
Atom Publishing Protocolapplication/atomcat+xml.atomcatRFC 5023
Atom Publishing Protocol Service Documentapplication/atomsvc+xml.atomsvcRFC 5023
Voice Browser Call Controlapplication/ccxml+xml,.ccxmlVoice Browser Call Control: CCXML Version 1.0
Cloud Data Management Interface (CDMI) - Capabilityapplication/cdmi-capability.cdmiaRFC 6208
Cloud Data Management Interface (CDMI) - Contaimerapplication/cdmi-container.cdmicRFC 6209
Cloud Data Management Interface (CDMI) - Domainapplication/cdmi-domain.cdmidRFC 6210
Cloud Data Management Interface (CDMI) - Objectapplication/cdmi-object.cdmioRFC 6211
Cloud Data Management Interface (CDMI) - Queueapplication/cdmi-queue.cdmiqRFC 6212
CU-SeeMeapplication/cu-seeme.cuWhite Pine
Web Distributed Authoring and Versioningapplication/davmount+xml.davmountRFC 4918
Data Structure for the Security Suitability of Cryptographic Algorithmsapplication/dssc+der.dsscRFC 5698
Data Structure for the Security Suitability of Cryptographic Algorithmsapplication/dssc+xml.xdsscRFC 5698
ECMAScriptapplication/ecmascript.esECMA-357
Extensible MultiModal Annotationapplication/emma+xml.emmaEMMA: Extensible MultiModal Annotation markup language
Electronic Publicationapplication/epub+zip.epubWikipedia: EPUB
Efficient XML Interchangeapplication/exi.exiEfficient XML Interchange (EXI) Best Practices
Portable Font Resourceapplication/font-tdpfr.pfrRFC 3073
Hyperstudioapplication/hyperstudio.stkIANA - Hyperstudio
Internet Protocol Flow Information Exportapplication/ipfix.ipfixRFC 3917
Java Archiveapplication/java-archive.jarWikipedia: JAR file format
Java Serialized Objectapplication/java-serialized-object.serJava Serialization API
Java Bytecode Fileapplication/java-vm.classWikipedia: Java Bytecode
JavaScriptapplication/javascript.jsJavaScript
JavaScript Object Notation (JSON)application/json.jsonWikipedia: JSON
Macintosh BinHex 4.0application/mac-binhex40.hqxMacMIME
Compact Proapplication/mac-compactpro.cptCompact Pro
Metadata Authority Description Schemaapplication/mads+xml.madsRFC 6207
MARC Formatsapplication/marc.mrcRFC 2220
MARC21 XML Schemaapplication/marcxml+xml.mrcxRFC 6207
Mathematica Notebooksapplication/mathematica.maIANA - Mathematica
Mathematical Markup Languageapplication/mathml+xml.mathmlW3C Math Home
Mbox database filesapplication/mbox.mboxRFC 4155
Media Server Control Markup Languageapplication/mediaservercontrol+xml.mscmlRFC 5022
Metalinkapplication/metalink4+xml.meta4Wikipedia: Metalink
Metadata Encoding and Transmission Standardapplication/mets+xml.metsRFC 6207
Metadata Object Description Schemaapplication/mods+xml.modsRFC 6207
MPEG-21application/mp21.m21Wikipedia: MPEG-21
MPEG4application/mp4.mp4RFC 4337
Microsoft Wordapplication/msword.docWikipedia: Microsoft Word
Material Exchange Formatapplication/mxf.mxfRFC 4539
Binary Dataapplication/octet-stream.bin
Office Document Architectureapplication/oda.odaRFC 2161
Open eBook Publication Structureapplication/oebps-package+xml.opfWikipedia: Open eBook
Oggapplication/ogg.ogxWikipedia: Ogg
Microsoft OneNoteapplication/onenote.onetocMS OneNote 2010
XML Patch Frameworkapplication/patch-ops-error+xml.xerRFC 5261
Adobe Portable Document Formatapplication/pdf.pdfAdobe PDF
Pretty Good Privacyapplication/pgp-encrypted.pgpRFC 2015
Pretty Good Privacy - Signatureapplication/pgp-signature.pgpRFC 2015
PICSRulesapplication/pics-rules.prfW3C PICSRules
PKCS #10 - Certification Request Standardapplication/pkcs10.p10RFC 2986
PKCS #7 - Cryptographic Message Syntax Standardapplication/pkcs7-mime.p7mRFC 2315
PKCS #7 - Cryptographic Message Syntax Standardapplication/pkcs7-signature.p7sRFC 2315
PKCS #8 - Private-Key Information Syntax Standardapplication/pkcs8.p8RFC 5208
Attribute Certificateapplication/pkix-attr-cert.acRFC 5877
Internet Public Key Infrastructure - Certificateapplication/pkix-cert.cerRFC 2585
Internet Public Key Infrastructure - Certificate Revocation Listsapplication/pkix-crl.crlRFC 2585
Internet Public Key Infrastructure - Certification Pathapplication/pkix-pkipath.pkipathRFC 2585
Internet Public Key Infrastructure - Certificate Management Protocoleapplication/pkixcmp.pkiRFC 2585
Pronunciation Lexicon Specificationapplication/pls+xml.plsRFC 4267
PostScriptapplication/postscript.aiWikipedia: PostScript
CU-Writerapplication/prs.cww.cww
Portable Symmetric Key Containerapplication/pskc+xml.pskcxmlRFC 6030
Resource Description Frameworkapplication/rdf+xml.rdfRFC 3870
IMS Networksapplication/reginfo+xml.rif
Relax NG Compact Syntaxapplication/relax-ng-compact-syntax.rncRelax NG
XML Resource Listsapplication/resource-lists+xml.rlRFC 4826
XML Resource Lists Diffapplication/resource-lists-diff+xml.rldRFC 4826
XML Resource Listsapplication/rls-services+xml.rsRFC 4826
Really Simple Discoveryapplication/rsd+xml.rsdWikipedia: Really Simple Discovery
RSS - Really Simple Syndicationapplication/rss+xml.rss, .xmlWikipedia: RSS
Rich Text Formatapplication/rtf.rtfWikipedia: Rich Text Format
Systems Biology Markup Languageapplication/sbml+xml.sbmlRFC 3823
Server-Based Certificate Validation Protocol - Validation Requestapplication/scvp-cv-request.scqRFC 5055
Server-Based Certificate Validation Protocol - Validation Responseapplication/scvp-cv-response.scsRFC 5055
Server-Based Certificate Validation Protocol - Validation Policies - Requestapplication/scvp-vp-request.spqRFC 5055
Server-Based Certificate Validation Protocol - Validation Policies - Responseapplication/scvp-vp-response.sppRFC 5055
Session Description Protocolapplication/sdp.sdpRFC 2327
Secure Electronic Transaction - Paymentapplication/set-payment-initiation.setpayIANA: SET Payment
Secure Electronic Transaction - Registrationapplication/set-registration-initiation.setregIANA: SET Registration
S Hexdump Formatapplication/shf+xml.shfRFC 4194
Synchronized Multimedia Integration Languageapplication/smil+xml.smiRFC 4536
SPARQL - Queryapplication/sparql-query.rqW3C SPARQL
SPARQL - Resultsapplication/sparql-results+xml.srxW3C SPARQL
Speech Recognition Grammar Specificationapplication/srgs.gramW3C Speech Grammar
Speech Recognition Grammar Specification - XMLapplication/srgs+xml.grxmlW3C Speech Grammar
Search/Retrieve via URL Response Formatapplication/sru+xml.sruRFC 6207
Speech Synthesis Markup Languageapplication/ssml+xml.ssmlW3C Speech Synthesis
Text Encoding and Interchangeapplication/tei+xml.teiRFC 6129
Sharing Transaction Fraud Dataapplication/thraud+xml.tfiRFC 5941
Time Stamped Data Envelopeapplication/timestamped-data.tsdRFC 5955
3rd Generation Partnership Project - Pic Largeapplication/vnd.3gpp.pic-bw-large.plb3GPP
3rd Generation Partnership Project - Pic Smallapplication/vnd.3gpp.pic-bw-small.psb3GPP
3rd Generation Partnership Project - Pic Varapplication/vnd.3gpp.pic-bw-var.pvb3GPP
3rd Generation Partnership Project - Transaction Capabilities Application Partapplication/vnd.3gpp2.tcap.tcap3GPP
3M Post It Notesapplication/vnd.3m.post-it-notes.pwnIANA: 3M Post It Notes
Simply Accountingapplication/vnd.accpac.simply.aso.asoIANA: Simply Accounting
Simply Accounting - Data Importapplication/vnd.accpac.simply.imp.impIANA: Simply Accounting
ACU Cobolapplication/vnd.acucobol.acuIANA: ACU Cobol
ACU Cobolapplication/vnd.acucorp.atcIANA: ACU Cobol
Adobe AIR Applicationapplication/vnd.adobe.air-application-installer-package+zip.airBuilding AIR Applications
Adobe Flex Projectapplication/vnd.adobe.fxp.fxpIANA: Adobe Flex Project
Adobe XML Data Packageapplication/vnd.adobe.xdp+xml.xdpWikipedia: XML Data Package
Adobe XML Forms Data Formatapplication/vnd.adobe.xfdf.xfdfWikipedia: XML Portable Document Format
Ahead AIR Applicationapplication/vnd.ahead.space.aheadIANA: Ahead AIR Application
AirZip FileSECUREapplication/vnd.airzip.filesecure.azf.azfIANA: AirZip
AirZip FileSECUREapplication/vnd.airzip.filesecure.azs.azsIANA: AirZip
Amazon Kindle eBook formatapplication/vnd.amazon.ebook.azwKindle Direct Publishing
Active Content Compressionapplication/vnd.americandynamics.acc.accIANA: Active Content Compression
AmigaDEapplication/vnd.amiga.ami.amiIANA: Amiga
Android Package Archiveapplication/vnd.android.package-archive.apkWikipedia: APK File Format
ANSER-WEB Terminal Client - Certificate Issueapplication/vnd.anser-web-certificate-issue-initiation.ciiIANA: ANSWER-WEB
ANSER-WEB Terminal Client - Web Funds Transferapplication/vnd.anser-web-funds-transfer-initiation.ftiIANA: ANSWER-WEB
Antix Game Playerapplication/vnd.antix.game-component.atxIANA: Antix Game Component
Apple Installer Packageapplication/vnd.apple.installer+xml.mpkgIANA: Apple Installer
Multimedia Playlist Unicodeapplication/vnd.apple.mpegurl.m3u8Wikipedia: M3U
Arista Networks Software Imageapplication/vnd.aristanetworks.swi.swiIANA: Arista Networks Software Image
Audiographapplication/vnd.audiograph.aepIANA: Audiograph
Blueice Research Multipassapplication/vnd.blueice.multipass.mpmIANA: Multipass
BMI Drawing Data Interchangeapplication/vnd.bmi.bmiIANA: BMI
BusinessObjectsapplication/vnd.businessobjects.repIANA: BusinessObjects
CambridgeSoft Chem Drawapplication/vnd.chemdraw+xml.cdxmlIANA: Chem Draw
Karaoke on Chipnuts Chipsetsapplication/vnd.chipnuts.karaoke-mmd.mmdIANA: Chipnuts Karaoke
Interactive Geometry Software Cinderellaapplication/vnd.cinderella.cdyIANA: Cinderella
Claymore Data Filesapplication/vnd.claymore.claIANA: Claymore
RetroPlatform Playerapplication/vnd.cloanto.rp9.rp9IANA: RetroPlatform Player
Clonk Gameapplication/vnd.clonk.c4group.c4gIANA: Clonk
ClueTrust CartoMobile - Configapplication/vnd.cluetrust.cartomobile-config.c11amcIANA: CartoMobile
ClueTrust CartoMobile - Config Packageapplication/vnd.cluetrust.cartomobile-config-pkg.c11amzIANA: CartoMobile
Sixth Floor Media - CommonSpaceapplication/vnd.commonspace.cspIANA: CommonSpace
CIM Databaseapplication/vnd.contact.cmsg.cdbcmsgIANA: CIM Database
CosmoCallerapplication/vnd.cosmocaller.cmcIANA: CosmoCaller
CrickSoftware - Clickerapplication/vnd.crick.clicker.clkxIANA: Clicker
CrickSoftware - Clicker - Keyboardapplication/vnd.crick.clicker.keyboard.clkkIANA: Clicker
CrickSoftware - Clicker - Paletteapplication/vnd.crick.clicker.palette.clkpIANA: Clicker
CrickSoftware - Clicker - Templateapplication/vnd.crick.clicker.template.clktIANA: Clicker
CrickSoftware - Clicker - Wordbankapplication/vnd.crick.clicker.wordbank.clkwIANA: Clicker
Critical Tools - PERT Chart EXPERTapplication/vnd.criticaltools.wbs+xml.wbsIANA: Critical Tools
PosMLapplication/vnd.ctc-posml.pmlIANA: PosML
Adobe PostScript Printer Description File Formatapplication/vnd.cups-ppd.ppdIANA: Cups
CURL Appletapplication/vnd.curl.car.carIANA: CURL Applet
CURL Appletapplication/vnd.curl.pcurl.pcurlIANA: CURL Applet
RemoteDocs R-Viewerapplication/vnd.data-vision.rdz.rdzIANA: Data-Vision
FCS Express Layout Linkapplication/vnd.denovo.fcselayout-link.fe_launchIANA: FCS Express Layout Link
New Moon Liftoff/DNAapplication/vnd.dna.dnaIANA: New Moon Liftoff/DNA
Dolby Meridian Lossless Packingapplication/vnd.dolby.mlp.mlpIANA: Dolby Meridian Lossless Packing
DPGraphapplication/vnd.dpgraph.dpgIANA: DPGraph
DreamFactoryapplication/vnd.dreamfactory.dfacIANA: DreamFactory
Digital Video Broadcastingapplication/vnd.dvb.ait.aitIANA: Digital Video Broadcasting
Digital Video Broadcastingapplication/vnd.dvb.service.svcIANA: Digital Video Broadcasting
DynaGeoapplication/vnd.dynageo.geoIANA: DynaGeo
EcoWin Chartapplication/vnd.ecowin.chart.magIANA: EcoWin Chart
Enliven Viewerapplication/vnd.enliven.nmlIANA: Enliven Viewer
QUASS Stream Playerapplication/vnd.epson.esf.esfIANA: QUASS Stream Player
QUASS Stream Playerapplication/vnd.epson.msf.msfIANA: QUASS Stream Player
QuickAnime Playerapplication/vnd.epson.quickanime.qamIANA: QuickAnime Player
SimpleAnimeLite Playerapplication/vnd.epson.salt.sltIANA: SimpleAnimeLite Player
QUASS Stream Playerapplication/vnd.epson.ssf.ssfIANA: QUASS Stream Player
MICROSEC e-Szign¢application/vnd.eszigno3+xml.es3IANA: MICROSEC e-Szign¢
EZPix Secure Photo Albumapplication/vnd.ezpix-album.ez2IANA: EZPix Secure Photo Album
EZPix Secure Photo Albumapplication/vnd.ezpix-package.ez3IANA: EZPix Secure Photo Album
Forms Data Formatapplication/vnd.fdf.fdfIANA: Forms Data Format
Digital Siesmograph Networks - SEED Datafilesapplication/vnd.fdsn.seed.seedIANA: SEED
NpGraphItapplication/vnd.flographit.gphIANA: FloGraphIt
FluxTime Clipapplication/vnd.fluxtime.clip.ftcIANA: FluxTime Clip
FrameMaker Normal Formatapplication/vnd.framemaker.fmIANA: FrameMaker
Frogans Playerapplication/vnd.frogans.fnc.fncIANA: Frogans Player
Frogans Playerapplication/vnd.frogans.ltf.ltfIANA: Frogans Player
Friendly Software Corporationapplication/vnd.fsc.weblaunch.fscIANA: Friendly Software Corporation
Fujitsu Oasysapplication/vnd.fujitsu.oasys.oasIANA: Fujitsu Oasys
Fujitsu Oasysapplication/vnd.fujitsu.oasys2.oa2IANA: Fujitsu Oasys
Fujitsu Oasysapplication/vnd.fujitsu.oasys3.oa3IANA: Fujitsu Oasys
Fujitsu Oasysapplication/vnd.fujitsu.oasysgp.fg5IANA: Fujitsu Oasys
Fujitsu Oasysapplication/vnd.fujitsu.oasysprs.bh2IANA: Fujitsu Oasys
Fujitsu - Xerox 2D CAD Dataapplication/vnd.fujixerox.ddd.dddIANA: Fujitsu DDD
Fujitsu - Xerox DocuWorksapplication/vnd.fujixerox.docuworks.xdwIANA: Docuworks
Fujitsu - Xerox DocuWorks Binderapplication/vnd.fujixerox.docuworks.binder.xbdIANA: Docuworks Binder
FuzzySheetapplication/vnd.fuzzysheet.fzsIANA: FuzySheet
Genomatix Tuxedo Frameworkapplication/vnd.genomatix.tuxedo.txdIANA: Genomatix Tuxedo Framework
GeoGebraapplication/vnd.geogebra.file.ggbIANA: GeoGebra
GeoGebraapplication/vnd.geogebra.tool.ggtIANA: GeoGebra
GeoMetry Explorerapplication/vnd.geometry-explorer.gexIANA: GeoMetry Explorer
GEONExT and JSXGraphapplication/vnd.geonext.gxtIANA: GEONExT and JSXGraph
GeoplanWapplication/vnd.geoplan.g2wIANA: GeoplanW
GeospacWapplication/vnd.geospace.g3wIANA: GeospacW
GameMaker ActiveXapplication/vnd.gmx.gmxIANA: GameMaker ActiveX
Google Earth - KMLapplication/vnd.google-earth.kml+xml.kmlIANA: Google Earth
Google Earth - Zipped KMLapplication/vnd.google-earth.kmz.kmzIANA: Google Earth
GrafEqapplication/vnd.grafeq.gqfIANA: GrafEq
Groove - Accountapplication/vnd.groove-account.gacIANA: Groove
Groove - Helpapplication/vnd.groove-help.ghfIANA: Groove
Groove - Identity Messageapplication/vnd.groove-identity-message.gimIANA: Groove
Groove - Injectorapplication/vnd.groove-injector.grvIANA: Groove
Groove - Tool Messageapplication/vnd.groove-tool-message.gtmIANA: Groove
Groove - Tool Templateapplication/vnd.groove-tool-template.tplIANA: Groove
Groove - Vcardapplication/vnd.groove-vcard.vcgIANA: Groove
Hypertext Application Languageapplication/vnd.hal+xml.halIANA: HAL
ZVUE Media Managerapplication/vnd.handheld-entertainment+xml.zmmIANA: ZVUE Media Manager
Homebanking Computer Interface (HBCI)application/vnd.hbci.hbciIANA: HBCI
Archipelago Lesson Playerapplication/vnd.hhe.lesson-player.lesIANA: Archipelago Lesson Player
HP-GL/2 and HP RTLapplication/vnd.hp-hpgl.hpglIANA: HP-GL/2 and HP RTL
Hewlett Packard Instant Deliveryapplication/vnd.hp-hpid.hpidIANA: Hewlett Packard Instant Delivery
Hewlett-Packard's WebPrintSmartapplication/vnd.hp-hps.hpsIANA: Hewlett-Packard's WebPrintSmart
HP Indigo Digital Press - Job Layout Languateapplication/vnd.hp-jlyt.jltIANA: HP Job Layout Language
HP Printer Command Languageapplication/vnd.hp-pcl.pclIANA: HP Printer Command Language
PCL 6 Enhanced (Formely PCL XL)application/vnd.hp-pclxl.pclxlIANA: HP PCL XL
Hydrostatix Master Suiteapplication/vnd.hydrostatix.sof-data.sfd-hdstxIANA: Hydrostatix Master Suite
3D Crossword Pluginapplication/vnd.hzn-3d-crossword.x3dIANA: 3D Crossword Plugin
MiniPayapplication/vnd.ibm.minipay.mpyIANA: MiniPay
MO:DCA-Papplication/vnd.ibm.modcap.afpIANA: MO:DCA-P
IBM DB2 Rights Managerapplication/vnd.ibm.rights-management.irmIANA: IBM DB2 Rights Manager
IBM Electronic Media Management System - Secure Containerapplication/vnd.ibm.secure-container.scIANA: EMMS
ICC profileapplication/vnd.iccprofile.iccIANA: ICC profile
igLoaderapplication/vnd.igloader.iglIANA: igLoader
ImmerVision PURE Playersapplication/vnd.immervision-ivp.ivpIANA: ImmerVision PURE Players
ImmerVision PURE Playersapplication/vnd.immervision-ivu.ivuIANA: ImmerVision PURE Players
IOCOM Visimeetapplication/vnd.insors.igm.igmIANA: IOCOM Visimeet
Intercon FormNetapplication/vnd.intercon.formnet.xpwIANA: Intercon FormNet
Interactive Geometry Softwareapplication/vnd.intergeo.i2gIANA: Interactive Geometry Software
Open Financial Exchangeapplication/vnd.intu.qbo.qboIANA: Open Financial Exchange
Quickenapplication/vnd.intu.qfx.qfxIANA: Quicken
IP Unplugged Roaming Clientapplication/vnd.ipunplugged.rcprofile.rcprofileIANA: IP Unplugged Roaming Client
iRepository / Lucidoc Editorapplication/vnd.irepository.package+xml.irpIANA: iRepository / Lucidoc Editor
Express by Infoseekapplication/vnd.is-xpr.xprIANA: Express by Infoseek
International Society for Advancement of Cytometryapplication/vnd.isac.fcs.fcsIANA: International Society for Advancement of Cytometry
Lightspeed Audio Labapplication/vnd.jam.jamIANA: Lightspeed Audio Lab
Mobile Information Device Profileapplication/vnd.jcp.javame.midlet-rms.rmsIANA: Mobile Information Device Profile
RhymBoxapplication/vnd.jisp.jispIANA: RhymBox
Joda Archiveapplication/vnd.joost.joda-archive.jodaIANA: Joda Archive
Kahootzapplication/vnd.kahootz.ktzIANA: Kahootz
KDE KOffice Office Suite - Karbonapplication/vnd.kde.karbon.karbonIANA: KDE KOffice Office Suite
KDE KOffice Office Suite - KChartapplication/vnd.kde.kchart.chrtIANA: KDE KOffice Office Suite
KDE KOffice Office Suite - Kformulaapplication/vnd.kde.kformula.kfoIANA: KDE KOffice Office Suite
KDE KOffice Office Suite - Kivioapplication/vnd.kde.kivio.flwIANA: KDE KOffice Office Suite
KDE KOffice Office Suite - Kontourapplication/vnd.kde.kontour.konIANA: KDE KOffice Office Suite
KDE KOffice Office Suite - Kpresenterapplication/vnd.kde.kpresenter.kprIANA: KDE KOffice Office Suite
KDE KOffice Office Suite - Kspreadapplication/vnd.kde.kspread.kspIANA: KDE KOffice Office Suite
KDE KOffice Office Suite - Kwordapplication/vnd.kde.kword.kwdIANA: KDE KOffice Office Suite
Kenamea Appapplication/vnd.kenameaapp.htkeIANA: Kenamea App
Kidspirationapplication/vnd.kidspiration.kiaIANA: Kidspiration
Kinar Applicationsapplication/vnd.kinar.kneIANA: Kina Applications
SSEYO Koan Play Fileapplication/vnd.koan.skpIANA: SSEYO Koan Play File
Kodak Storyshareapplication/vnd.kodak-descriptor.sseIANA: Kodak Storyshare
Laser App Enterpriseapplication/vnd.las.las+xml.lasxmlIANA: Laser App Enterprise
Life Balance - Desktop Editionapplication/vnd.llamagraphics.life-balance.desktop.lbdIANA: Life Balance
Life Balance - Exchange Formatapplication/vnd.llamagraphics.life-balance.exchange+xml.lbeIANA: Life Balance
Lotus 1-2-3application/vnd.lotus-1-2-3.123IANA: Lotus 1-2-3
Lotus Approachapplication/vnd.lotus-approach.aprIANA: Lotus Approach
Lotus Freelanceapplication/vnd.lotus-freelance.preIANA: Lotus Freelance
Lotus Notesapplication/vnd.lotus-notes.nsfIANA: Lotus Notes
Lotus Organizerapplication/vnd.lotus-organizer.orgIANA: Lotus Organizer
Lotus Screencamapplication/vnd.lotus-screencam.scmIANA: Lotus Screencam
Lotus Wordproapplication/vnd.lotus-wordpro.lwpIANA: Lotus Wordpro
MacPorts Port Systemapplication/vnd.macports.portpkg.portpkgIANA: MacPorts Port System
Micro CADAM Helix D&Dapplication/vnd.mcd.mcdIANA: Micro CADAM Helix D&D
MedCalcapplication/vnd.medcalcdata.mc1IANA: MedCalc
MediaRemoteapplication/vnd.mediastation.cdkey.cdkeyIANA: MediaRemote
Medical Waveform Encoding Formatapplication/vnd.mfer.mwfIANA: Medical Waveform Encoding Format
Melody Format for Mobile Platformapplication/vnd.mfmp.mfmIANA: Melody Format for Mobile Platform
Micrografxapplication/vnd.micrografx.flo.floIANA: Micrografx
Micrografx iGrafx Professionalapplication/vnd.micrografx.igx.igxIANA: Micrografx
FrameMaker Interchange Formatapplication/vnd.mif.mifIANA: FrameMaker Interchange Format
Mobius Management Systems - UniversalArchiveapplication/vnd.mobius.daf.dafIANA: Mobius Management Systems
Mobius Management Systems - Distribution Databaseapplication/vnd.mobius.dis.disIANA: Mobius Management Systems
Mobius Management Systems - Basket fileapplication/vnd.mobius.mbk.mbkIANA: Mobius Management Systems
Mobius Management Systems - Query Fileapplication/vnd.mobius.mqy.mqyIANA: Mobius Management Systems
Mobius Management Systems - Script Languageapplication/vnd.mobius.msl.mslIANA: Mobius Management Systems
Mobius Management Systems - Policy Definition Language Fileapplication/vnd.mobius.plc.plcIANA: Mobius Management Systems
Mobius Management Systems - Topic Index Fileapplication/vnd.mobius.txf.txfIANA: Mobius Management Systems
Mophun VMapplication/vnd.mophun.application.mpnIANA: Mophun VM
Mophun Certificateapplication/vnd.mophun.certificate.mpcIANA: Mophun Certificate
XUL - XML User Interface Languageapplication/vnd.mozilla.xul+xml.xulIANA: XUL
Microsoft Artgalryapplication/vnd.ms-artgalry.cilIANA: MS Artgalry
Microsoft Cabinet Fileapplication/vnd.ms-cab-compressed.cabIANA: MS Cabinet File
Microsoft Excelapplication/vnd.ms-excel.xlsIANA: MS Excel
Microsoft Excel - Add-In Fileapplication/vnd.ms-excel.addin.macroenabled.12.xlamIANA: MS Excel
Microsoft Excel - Binary Workbookapplication/vnd.ms-excel.sheet.binary.macroenabled.12.xlsbIANA: MS Excel
Microsoft Excel - Macro-Enabled Workbookapplication/vnd.ms-excel.sheet.macroenabled.12.xlsmIANA: MS Excel
Microsoft Excel - Macro-Enabled Template Fileapplication/vnd.ms-excel.template.macroenabled.12.xltmIANA: MS Excel
Microsoft Embedded OpenTypeapplication/vnd.ms-fontobject.eotIANA: MS Embedded OpenType
Microsoft Html Help Fileapplication/vnd.ms-htmlhelp.chmIANA:MS Html Help File
Microsoft Class Serverapplication/vnd.ms-ims.imsIANA: MS Class Server
Microsoft Learning Resource Moduleapplication/vnd.ms-lrm.lrmIANA: MS Learning Resource Module
Microsoft Office System Release Themeapplication/vnd.ms-officetheme.thmxIANA: MS Office System Release Theme
Microsoft Trust UI Provider - Security Catalogapplication/vnd.ms-pki.seccat.catIANA: MS Trust UI Provider
Microsoft Trust UI Provider - Certificate Trust Linkapplication/vnd.ms-pki.stl.stlIANA: MS Trust UI Provider
Microsoft PowerPointapplication/vnd.ms-powerpoint.pptIANA: MS PowerPoint
Microsoft PowerPoint - Add-in fileapplication/vnd.ms-powerpoint.addin.macroenabled.12.ppamIANA: MS PowerPoint
Microsoft PowerPoint - Macro-Enabled Presentation Fileapplication/vnd.ms-powerpoint.presentation.macroenabled.12.pptmIANA: MS PowerPoint
Microsoft PowerPoint - Macro-Enabled Open XML Slideapplication/vnd.ms-powerpoint.slide.macroenabled.12.sldmIANA: MS PowerPoint
Microsoft PowerPoint - Macro-Enabled Slide Show Fileapplication/vnd.ms-powerpoint.slideshow.macroenabled.12.ppsmIANA: MS PowerPoint
Microsoft PowerPoint - Macro-Enabled Template Fileapplication/vnd.ms-powerpoint.template.macroenabled.12.potmIANA: MS PowerPoint
Microsoft Projectapplication/vnd.ms-project.mppIANA: MS PowerPoint
Microsoft Word - Macro-Enabled Documentapplication/vnd.ms-word.document.macroenabled.12.docmIANA: MS Word
Microsoft Word - Macro-Enabled Templateapplication/vnd.ms-word.template.macroenabled.12.dotmIANA: MS Word
Microsoft Worksapplication/vnd.ms-works.wpsIANA: MS Works
Microsoft Windows Media Player Playlistapplication/vnd.ms-wpl.wplIANA: MS Windows Media Player Playlist
Microsoft XML Paper Specificationapplication/vnd.ms-xpsdocument.xpsIANA: MS XML Paper Specification
3GPP MSEQ Fileapplication/vnd.mseq.mseqIANA: 3GPP MSEQ File
MUsical Score Interpreted Code Invented for the ASCII designation of Notationapplication/vnd.musician.musIANA: MUSICIAN
Muvee Automatic Video Editingapplication/vnd.muvee.style.mstyIANA: Muvee
neuroLanguageapplication/vnd.neurolanguage.nlu.nluIANA: neuroLanguage
NobleNet Directoryapplication/vnd.noblenet-directory.nndIANA: NobleNet Directory
NobleNet Sealerapplication/vnd.noblenet-sealer.nnsIANA: NobleNet Sealer
NobleNet Webapplication/vnd.noblenet-web.nnwIANA: NobleNet Web
N-Gage Game Dataapplication/vnd.nokia.n-gage.data.ngdatIANA: N-Gage Game Data
N-Gage Game Installerapplication/vnd.nokia.n-gage.symbian.install.n-gageIANA: N-Gage Game Installer
Nokia Radio Application - Presetapplication/vnd.nokia.radio-preset.rpstIANA: Nokia Radio Application
Nokia Radio Application - Presetapplication/vnd.nokia.radio-presets.rpssIANA: Nokia Radio Application
Novadigm's RADIA and EDM productsapplication/vnd.novadigm.edm.edmIANA: Novadigm's RADIA and EDM products
Novadigm's RADIA and EDM productsapplication/vnd.novadigm.edx.edxIANA: Novadigm's RADIA and EDM products
Novadigm's RADIA and EDM productsapplication/vnd.novadigm.ext.extIANA: Novadigm's RADIA and EDM products
OpenDocument Chartapplication/vnd.oasis.opendocument.chart.odcIANA: OpenDocument Chart
OpenDocument Chart Templateapplication/vnd.oasis.opendocument.chart-template.otcIANA: OpenDocument Chart Template
OpenDocument Databaseapplication/vnd.oasis.opendocument.database.odbIANA: OpenDocument Database
OpenDocument Formulaapplication/vnd.oasis.opendocument.formula.odfIANA: OpenDocument Formula
OpenDocument Formula Templateapplication/vnd.oasis.opendocument.formula-template.odftIANA: OpenDocument Formula Template
OpenDocument Graphicsapplication/vnd.oasis.opendocument.graphics.odgIANA: OpenDocument Graphics
OpenDocument Graphics Templateapplication/vnd.oasis.opendocument.graphics-template.otgIANA: OpenDocument Graphics Template
OpenDocument Imageapplication/vnd.oasis.opendocument.image.odiIANA: OpenDocument Image
OpenDocument Image Templateapplication/vnd.oasis.opendocument.image-template.otiIANA: OpenDocument Image Template
OpenDocument Presentationapplication/vnd.oasis.opendocument.presentation.odpIANA: OpenDocument Presentation
OpenDocument Presentation Templateapplication/vnd.oasis.opendocument.presentation-template.otpIANA: OpenDocument Presentation Template
OpenDocument Spreadsheetapplication/vnd.oasis.opendocument.spreadsheet.odsIANA: OpenDocument Spreadsheet
OpenDocument Spreadsheet Templateapplication/vnd.oasis.opendocument.spreadsheet-template.otsIANA: OpenDocument Spreadsheet Template
OpenDocument Textapplication/vnd.oasis.opendocument.text.odtIANA: OpenDocument Text
OpenDocument Text Masterapplication/vnd.oasis.opendocument.text-master.odmIANA: OpenDocument Text Master
OpenDocument Text Templateapplication/vnd.oasis.opendocument.text-template.ottIANA: OpenDocument Text Template
Open Document Text Webapplication/vnd.oasis.opendocument.text-web.othIANA: OpenDocument Text Web
Sugar Linux Application Bundleapplication/vnd.olpc-sugar.xoIANA: Sugar Linux App Bundle
OMA Download Agentsapplication/vnd.oma.dd2+xml.dd2IANA: OMA Download Agents
Open Office Extensionapplication/vnd.openofficeorg.extension.oxtIANA: Open Office Extension
Microsoft Office - OOXML - Presentationapplication/vnd.openxmlformats-officedocument.presentationml.presentation.pptxIANA: OOXML - Presentation
Microsoft Office - OOXML - Presentation (Slide)application/vnd.openxmlformats-officedocument.presentationml.slide.sldxIANA: OOXML - Presentation
Microsoft Office - OOXML - Presentation (Slideshow)application/vnd.openxmlformats-officedocument.presentationml.slideshow.ppsxIANA: OOXML - Presentation
Microsoft Office - OOXML - Presentation Templateapplication/vnd.openxmlformats-officedocument.presentationml.template.potxIANA: OOXML - Presentation Template
Microsoft Office - OOXML - Spreadsheetapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet.xlsxIANA: OOXML - Spreadsheet
Microsoft Office - OOXML - Spreadsheet Templateapplication/vnd.openxmlformats-officedocument.spreadsheetml.template.xltxIANA: OOXML - Spreadsheet Template
Microsoft Office - OOXML - Word Documentapplication/vnd.openxmlformats-officedocument.wordprocessingml.document.docxIANA: OOXML - Word Document
Microsoft Office - OOXML - Word Document Templateapplication/vnd.openxmlformats-officedocument.wordprocessingml.template.dotxIANA: OOXML - Word Document Template
MapGuide DBXMLapplication/vnd.osgeo.mapguide.package.mgpIANA: MapGuide DBXML
OSGi Deployment Packageapplication/vnd.osgi.dp.dpIANA: OSGi Deployment Package
PalmOS Dataapplication/vnd.palm.pdbIANA: PalmOS Data
PawaaFILEapplication/vnd.pawaafile.pawIANA: PawaaFILE
Proprietary P&G Standard Reporting Systemapplication/vnd.pg.format.strIANA: Proprietary P&G Standard Reporting System
Proprietary P&G Standard Reporting Systemapplication/vnd.pg.osasli.ei6IANA: Proprietary P&G Standard Reporting System
Pcsel eFIF Fileapplication/vnd.picsel.efifIANA: Picsel eFIF File
Qualcomm's Plaza Mobile Internetapplication/vnd.pmi.widget.wgIANA: Qualcomm's Plaza Mobile Internet
PocketLearn Viewersapplication/vnd.pocketlearn.plfIANA: PocketLearn Viewers
PowerBuilderapplication/vnd.powerbuilder6.pbdIANA: PowerBuilder
Preview Systems ZipLock/VBoxapplication/vnd.previewsystems.box.boxIANA: Preview Systems ZipLock/Vbox
EFI Proteusapplication/vnd.proteus.magazine.mgzIANA: EFI Proteus
PubliShare Objectsapplication/vnd.publishare-delta-tree.qpsIANA: PubliShare Objects
Princeton Video Imageapplication/vnd.pvi.ptid1.ptidIANA: Princeton Video Image
QuarkXpressapplication/vnd.quark.quarkxpress.qxdIANA: QuarkXPress
RealVNCapplication/vnd.realvnc.bed.bedIANA: RealVNC
Recordare Applicationsapplication/vnd.recordare.musicxml.mxlIANA: Recordare Apps
Recordare Applicationsapplication/vnd.recordare.musicxml+xml.musicxmlIANA: Recordare Apps
CryptoNoteapplication/vnd.rig.cryptonote.cryptonoteIANA: CryptoNote
Blackberry COD Fileapplication/vnd.rim.cod.cod
RealMediaapplication/vnd.rn-realmedia.rm
ROUTE 66 Location Based Servicesapplication/vnd.route66.link66+xml.link66IANA: ROUTE 66
SailingTrackerapplication/vnd.sailingtracker.track.stIANA: SailingTracker
SeeMailapplication/vnd.seemail.seeIANA: SeeMail
Secured eMailapplication/vnd.sema.semaIANA: Secured eMail
Secured eMailapplication/vnd.semd.semdIANA: Secured eMail
Secured eMailapplication/vnd.semf.semfIANA: Secured eMail
Shana Informed Fillerapplication/vnd.shana.informed.formdata.ifmIANA: Shana Informed Filler
Shana Informed Fillerapplication/vnd.shana.informed.formtemplate.itpIANA: Shana Informed Filler
Shana Informed Fillerapplication/vnd.shana.informed.interchange.iifIANA: Shana Informed Filler
Shana Informed Fillerapplication/vnd.shana.informed.package.ipkIANA: Shana Informed Filler
SimTech MindMapperapplication/vnd.simtech-mindmapper.twdIANA: SimTech MindMapper
SMAF Fileapplication/vnd.smaf.mmfIANA: SMAF File
SMART Technologies Appsapplication/vnd.smart.teacher.teacherIANA: SMART Technologies Apps
SudokuMagicapplication/vnd.solent.sdkm+xml.sdkmIANA: SudokuMagic
TIBCO Spotfireapplication/vnd.spotfire.dxp.dxpIANA: TIBCO Spotfire
TIBCO Spotfireapplication/vnd.spotfire.sfs.sfsIANA: TIBCO Spotfire
StarOffice - Calcapplication/vnd.stardivision.calc.sdc
StarOffice - Drawapplication/vnd.stardivision.draw.sda
StarOffice - Impressapplication/vnd.stardivision.impress.sdd
StarOffice - Mathapplication/vnd.stardivision.math.smf
StarOffice - Writerapplication/vnd.stardivision.writer.sdw
StarOffice - Writer (Global)application/vnd.stardivision.writer-global.sgl
StepManiaapplication/vnd.stepmania.stepchart.smIANA: StepMania
OpenOffice - Calc (Spreadsheet)application/vnd.sun.xml.calc.sxcWikipedia: OpenOffice
OpenOffice - Calc Template (Spreadsheet)application/vnd.sun.xml.calc.template.stcWikipedia: OpenOffice
OpenOffice - Draw (Graphics)application/vnd.sun.xml.draw.sxdWikipedia: OpenOffice
OpenOffice - Draw Template (Graphics)application/vnd.sun.xml.draw.template.stdWikipedia: OpenOffice
OpenOffice - Impress (Presentation)application/vnd.sun.xml.impress.sxiWikipedia: OpenOffice
OpenOffice - Impress Template (Presentation)application/vnd.sun.xml.impress.template.stiWikipedia: OpenOffice
OpenOffice - Math (Formula)application/vnd.sun.xml.math.sxmWikipedia: OpenOffice
OpenOffice - Writer (Text - HTML)application/vnd.sun.xml.writer.sxwWikipedia: OpenOffice
OpenOffice - Writer (Text - HTML)application/vnd.sun.xml.writer.global.sxgWikipedia: OpenOffice
OpenOffice - Writer Template (Text - HTML)application/vnd.sun.xml.writer.template.stwWikipedia: OpenOffice
ScheduleUsapplication/vnd.sus-calendar.susIANA: ScheduleUs
SourceView Documentapplication/vnd.svd.svdIANA: SourceView Document
Symbian Install Packageapplication/vnd.symbian.install.sisIANA: Symbian Install
SyncMLapplication/vnd.syncml+xml.xsmIANA: SyncML
SyncML - Device Managementapplication/vnd.syncml.dm+wbxml.bdmIANA: SyncML
SyncML - Device Managementapplication/vnd.syncml.dm+xml.xdmIANA: SyncML
Tao Intentapplication/vnd.tao.intent-module-archive.taoIANA: Tao Intent
MobileTVapplication/vnd.tmobile-livetv.tmoIANA: MobileTV
TRI Systems Configapplication/vnd.trid.tpt.tptIANA: TRI Systems
Triscape Map Explorerapplication/vnd.triscape.mxs.mxsIANA: Triscape Map Explorer
True BASICapplication/vnd.trueapp.traIANA: True BASIC
Universal Forms Description Languageapplication/vnd.ufdl.ufdIANA: Universal Forms Description Language
User Interface Quartz - Theme (Symbian)application/vnd.uiq.theme.utzIANA: User Interface Quartz
UMAJINapplication/vnd.umajin.umjIANA: UMAJIN
Unity 3dapplication/vnd.unity.unitywebIANA: Unity 3d
Unique Object Markup Languageapplication/vnd.uoml+xml.uomlIANA: UOML
VirtualCatalogapplication/vnd.vcx.vcxIANA: VirtualCatalog
Microsoft Visioapplication/vnd.visio.vsdIANA: Visio
Microsoft Visio 2013application/vnd.visio2013.vsdxIANA: Visio
Visionaryapplication/vnd.visionary.visIANA: Visionary
Viewport+application/vnd.vsf.vsfIANA: Viewport+
WAP Binary XML (WBXML)application/vnd.wap.wbxml.wbxmlIANA: WBXML
Compiled Wireless Markup Language (WMLC)application/vnd.wap.wmlc.wmlcIANA: WMLC
WMLScriptapplication/vnd.wap.wmlscriptc.wmlscIANA: WMLScript
WebTurboapplication/vnd.webturbo.wtbIANA: WebTurbo
Mathematica Notebook Playerapplication/vnd.wolfram.player.nbpIANA: Mathematica Notebook Player
Wordperfectapplication/vnd.wordperfect.wpdIANA: Wordperfect
SundaHus WQapplication/vnd.wqd.wqdIANA: SundaHus WQ
Worldtalkapplication/vnd.wt.stf.stfIANA: Worldtalk
CorelXARAapplication/vnd.xara.xarIANA: CorelXARA
Extensible Forms Description Languageapplication/vnd.xfdl.xfdlIANA: Extensible Forms Description Language
HV Voice Dictionaryapplication/vnd.yamaha.hv-dic.hvdIANA: HV Voice Dictionary
HV Scriptapplication/vnd.yamaha.hv-script.hvsIANA: HV Script
HV Voice Parameterapplication/vnd.yamaha.hv-voice.hvpIANA: HV Voice Parameter
Open Score Formatapplication/vnd.yamaha.openscoreformat.osfIANA: Open Score Format
OSFPVGapplication/vnd.yamaha.openscoreformat.osfpvg+xml.osfpvgIANA: OSFPVG
SMAF Audioapplication/vnd.yamaha.smaf-audio.safIANA: SMAF Audio
SMAF Phraseapplication/vnd.yamaha.smaf-phrase.spfIANA: SMAF Phrase
CustomMenuapplication/vnd.yellowriver-custom-menu.cmpIANA: CustomMenu
Z.U.L. Geometryapplication/vnd.zul.zirIANA: Z.U.L.
Zzazz Deckapplication/vnd.zzazz.deck+xml.zazIANA: Zzazz
VoiceXMLapplication/voicexml+xml.vxmlRFC 4267
Widget Packaging and XML Configurationapplication/widget.wgtW3C Widget Packaging and XML Configuration
WinHelpapplication/winhlp.hlpWikipedia: WinHelp
WSDL - Web Services Description Languageapplication/wsdl+xml.wsdlW3C Web Service Description Language
Web Services Policyapplication/wspolicy+xml.wspolicyW3C Web Services Policy
7-Zipapplication/x-7z-compressed.7zWikipedia: 7-Zip
AbiWordapplication/x-abiword.abwWikipedia: AbiWord
Ace Archiveapplication/x-ace-compressed.aceWikipedia: ACE
Adobe (Macropedia) Authorware - Binary Fileapplication/x-authorware-bin.aabWikipedia: Authorware
Adobe (Macropedia) Authorware - Mapapplication/x-authorware-map.aamWikipedia: Authorware
Adobe (Macropedia) Authorware - Segment Fileapplication/x-authorware-seg.aasWikipedia: Authorware
Binary CPIO Archiveapplication/x-bcpio.bcpioWikipedia: cpio
BitTorrentapplication/x-bittorrent.torrentWikipedia: BitTorrent
Bzip Archiveapplication/x-bzip.bzWikipedia: Bzip
Bzip2 Archiveapplication/x-bzip2.bz2Wikipedia: Bzip
Video CDapplication/x-cdlink.vcdWikipedia: Video CD
pIRChapplication/x-chat.chatWikipedia: pIRCh
Portable Game Notation (Chess Games)application/x-chess-pgn.pgnWikipedia: Portable Game Notationb
CPIO Archiveapplication/x-cpio.cpioWikipedia: cpio
C Shell Scriptapplication/x-csh.cshWikipedia: C Shell
Debian Packageapplication/x-debian-package.debWikipedia: Debian Package
Adobe Shockwave Playerapplication/x-director.dirWikipedia: Adobe Shockwave Player
Doom Video Gameapplication/x-doom.wadWikipedia: Doom WAD
Navigation Control file for XML (for ePub)application/x-dtbncx+xml.ncxWikipedia: EPUB
Digital Talking Bookapplication/x-dtbook+xml.dtbWikipedia: EPUB
Digital Talking Book - Resource Fileapplication/x-dtbresource+xml.resDigital Talking Book
Device Independent File Format (DVI)application/x-dvi.dviWikipedia: DVI
Glyph Bitmap Distribution Formatapplication/x-font-bdf.bdfWikipedia: Glyph Bitmap Distribution Format
Ghostscript Fontapplication/x-font-ghostscript.gsfWikipedia: Ghostscript
PSF Fontsapplication/x-font-linux-psf.psfPSF Fonts
OpenType Font Fileapplication/x-font-otf.otfOpenType Font File
Portable Compiled Formatapplication/x-font-pcf.pcfWikipedia: Portable Compiled Format
Server Normal Formatapplication/x-font-snf.snfWikipedia: Server Normal Format
TrueType Fontapplication/x-font-ttf.ttfWikipedia: TrueType
PostScript Fontsapplication/x-font-type1.pfaWikipedia: PostScript Fonts
Web Open Font Formatapplication/x-font-woff.woffWikipedia: Web Open Font Format
FutureSplash Animatorapplication/x-futuresplash.splWikipedia: FutureSplash Animator
Gnumericapplication/x-gnumeric.gnumericWikipedia: Gnumeric
GNU Tar Filesapplication/x-gtar.gtarGNU Tar
Hierarchical Data Formatapplication/x-hdf.hdfWikipedia: Hierarchical Data Format
Java Network Launching Protocolapplication/x-java-jnlp-file.jnlpWikipedia: Java Web Start
LaTeXapplication/x-latex.latexWikipedia: LaTeX
Mobipocketapplication/x-mobipocket-ebook.prcWikipedia: Mobipocket
Microsoft ClickOnceapplication/x-ms-application.applicationWikipedia: ClickOnce
Microsoft Windows Media Player Download Packageapplication/x-ms-wmd.wmdWikipedia: Windows Media Player
Microsoft Windows Media Player Skin Packageapplication/x-ms-wmz.wmzWikipedia: Windows Media Player
Microsoft XAML Browser Applicationapplication/x-ms-xbap.xbapWikipedia: XAML Browser
Microsoft Accessapplication/x-msaccess.mdbWikipedia: Microsoft Access
Microsoft Office Binderapplication/x-msbinder.obdWikipedia: Microsoft Shared Tools
Microsoft Information Cardapplication/x-mscardfile.crdWikipedia: Information Card
Microsoft Clipboard Clipapplication/x-msclip.clpWikipedia: Clipboard
Microsoft Applicationapplication/x-msdownload.exeWikipedia: EXE
Microsoft MediaViewapplication/x-msmediaview.mvbWindows Help
Microsoft Windows Metafileapplication/x-msmetafile.wmfWikipedia: Windows Metafile
Microsoft Moneyapplication/x-msmoney.mnyWikipedia: Microsoft Money
Microsoft Publisherapplication/x-mspublisher.pubWikipedia: Microsoft Publisher
Microsoft Schedule+application/x-msschedule.scdWikipedia: Microsoft Schedule Plus
Microsoft Windows Terminal Servicesapplication/x-msterminal.trmWikipedia: Terminal Server
Microsoft Wordpadapplication/x-mswrite.wriWikipedia: Wordpad
Network Common Data Form (NetCDF)application/x-netcdf.ncWikipedia: NetCDF
PKCS #12 - Personal Information Exchange Syntax Standardapplication/x-pkcs12.p12RFC 2986
PKCS #7 - Cryptographic Message Syntax Standard (Certificates)application/x-pkcs7-certificates.p7bRFC 2986
PKCS #7 - Cryptographic Message Syntax Standard (Certificate Request Response)application/x-pkcs7-certreqresp.p7rRFC 2986
RAR Archiveapplication/x-rar-compressed.rarWikipedia: RAR
Bourne Shell Scriptapplication/x-sh.shWikipedia: Bourne Shell
Shell Archiveapplication/x-shar.sharWikipedia: Shell Archie
Adobe Flashapplication/x-shockwave-flash.swfWikipedia: Adobe Flash
Microsoft Silverlightapplication/x-silverlight-app.xapWikipedia: Silverlight
Stuffit Archiveapplication/x-stuffit.sitWikipedia: Stuffit
Stuffit Archiveapplication/x-stuffitx.sitxWikipedia: Stuffit
System V Release 4 CPIO Archiveapplication/x-sv4cpio.sv4cpioWikipedia: pax
System V Release 4 CPIO Checksum Dataapplication/x-sv4crc.sv4crcWikipedia: pax
Tar File (Tape Archive)application/x-tar.tarWikipedia: Tar
Tcl Scriptapplication/x-tcl.tclWikipedia: Tcl
TeXapplication/x-tex.texWikipedia: TeX
TeX Font Metricapplication/x-tex-tfm.tfmWikipedia: TeX Font Metric
GNU Texinfo Documentapplication/x-texinfo.texinfoWikipedia: Texinfo
Ustar (Uniform Standard Tape Archive)application/x-ustar.ustarWikipedia: Ustar
WAIS Sourceapplication/x-wais-source.srcYoLinux
X.509 Certificateapplication/x-x509-ca-cert.derWikipedia: X.509
Xfigapplication/x-xfig.figWikipedia: Xfig
XPInstall - Mozillaapplication/x-xpinstall.xpiWikipedia: XPI
XML Configuration Access Protocol - XCAP Diffapplication/xcap-diff+xml.xdfWikipedia: XCAP
XML Encryption Syntax and Processingapplication/xenc+xml.xencW3C XML Encryption Syntax and Processing
XHTML - The Extensible HyperText Markup Languageapplication/xhtml+xml.xhtmlW3C XHTML
XML - Extensible Markup Languageapplication/xml.xmlW3C XML
Document Type Definitionapplication/xml-dtd.dtdW3C DTD
XML-Binary Optimized Packagingapplication/xop+xml.xopW3C XOP
XML Transformationsapplication/xslt+xml.xsltW3C XSLT
XSPF - XML Shareable Playlist Formatapplication/xspf+xml.xspfXML Shareable Playlist Format
MXMLapplication/xv+xml.mxmlWikipedia: MXML
YANG Data Modeling Languageapplication/yang.yangWikipedia: YANG
YIN (YANG - XML)application/yin+xml.yinWikipedia: YANG
Zip Archiveapplication/zip.zipWikipedia: Zip
Adaptive differential pulse-code modulationaudio/adpcm.adpWikipedia: ADPCM
Sun Audio - Au file formataudio/basic.auWikipedia: Sun audio
MIDI - Musical Instrument Digital Interfaceaudio/midi.midWikipedia: MIDI
MPEG-4 Audioaudio/mp4.mp4aWikipedia: MP4A
MPEG Audioaudio/mpeg.mpgaWikipedia: MPGA
Ogg Audioaudio/ogg.ogaWikipedia: Ogg
DECE Audioaudio/vnd.dece.audio.uvaIANA: Dece Audio
Digital Winds Musicaudio/vnd.digital-winds.eolIANA: Digital Winds
DRA Audioaudio/vnd.dra.draIANA: DRA
DTS Audioaudio/vnd.dts.dtsIANA: DTS
DTS High Definition Audioaudio/vnd.dts.hd.dtshdIANA: DTS HD
Lucent Voiceaudio/vnd.lucent.voice.lvpIANA: Lucent Voice
Microsoft PlayReady Ecosystemaudio/vnd.ms-playready.media.pya.pyaIANA: Microsoft PlayReady Ecosystem
Nuera ECELP 4800audio/vnd.nuera.ecelp4800.ecelp4800IANA: ECELP 4800
Nuera ECELP 7470audio/vnd.nuera.ecelp7470.ecelp7470IANA: ECELP 7470
Nuera ECELP 9600audio/vnd.nuera.ecelp9600.ecelp9600IANA: ECELP 9600
Hit'n'Mixaudio/vnd.rip.ripIANA: Hit'n'Mix
Open Web Media Project - Audioaudio/webm.webaWebM Project
Advanced Audio Coding (AAC)audio/x-aac.aacWikipedia: AAC
Audio Interchange File Formataudio/x-aiff.aifWikipedia: Audio Interchange File Format
M3U (Multimedia Playlist)audio/x-mpegurl.m3uWikipedia: M3U
Microsoft Windows Media Audio Redirectoraudio/x-ms-wax.waxWindows Media Metafiles
Microsoft Windows Media Audioaudio/x-ms-wma.wmaWikipedia: Windows Media Audio
Real Audio Soundaudio/x-pn-realaudio.ramWikipedia: RealPlayer
Real Audio Soundaudio/x-pn-realaudio-plugin.rmpWikipedia: RealPlayer
Waveform Audio File Format (WAV)audio/x-wav.wavWikipedia: WAV
ChemDraw eXchange filechemical/x-cdx.cdxChemDraw eXchange file
Crystallographic Interchange Formatchemical/x-cif.cifCrystallographic Interchange Format
CrystalMaker Data Formatchemical/x-cmdf.cmdfCrystalMaker Data Format
Chemical Markup Languagechemical/x-cml.cmlWikipedia: Chemical Markup Language
Chemical Style Markup Languagechemical/x-csml.csmlWikipedia: Chemical Style Markup Language
XYZ File Formatchemical/x-xyz.xyzWikipedia: XYZ File Format
Bitmap Image Fileimage/bmp.bmpWikipedia: BMP File Format
Computer Graphics Metafileimage/cgm.cgmWikipedia: Computer Graphics Metafile
G3 Fax Imageimage/g3fax.g3Wikipedia: G3 Fax Image
Graphics Interchange Formatimage/gif.gifWikipedia: Graphics Interchange Format
Image Exchange Formatimage/ief.iefRFC 1314
JPEG Imageimage/jpeg.jpeg, .jpgRFC 1314
JPEG Image (Progressive)image/pjpeg.pjpegJPEG image compression FAQ
JPEG Image (Citrix client)image/x-citrix-jpeg.jpeg, .jpgRFC 1314
OpenGL Textures (KTX)image/ktx.ktxKTX File Format
Portable Network Graphics (PNG)image/png.pngRFC 2083
Portable Network Graphics (PNG) (x-token)image/x-png.pngRFC 2083
Portable Network Graphics (PNG) (Citrix client)image/x-citrix-png.pngRFC 2083
BTIFimage/prs.btif.btifIANA: BTIF
Scalable Vector Graphics (SVG)image/svg+xml.svgWikipedia: SVG
Tagged Image File Formatimage/tiff.tiffWikipedia: TIFF
Photoshop Documentimage/vnd.adobe.photoshop.psdWikipedia: Photoshop Document
DECE Graphicimage/vnd.dece.graphic.uviIANA: DECE Graphic
Close Captioning - Subtitleimage/vnd.dvb.subtitle.subWikipedia: Closed Captioning
DjVuimage/vnd.djvu.djvuWikipedia: DjVu
DWG Drawingimage/vnd.dwg.dwgWikipedia: DWG
AutoCAD DXFimage/vnd.dxf.dxfWikipedia: AutoCAD DXF
FastBid Sheetimage/vnd.fastbidsheet.fbsIANA: FastBid Sheet
FlashPiximage/vnd.fpx.fpxIANA: FPX
FAST Search & Transfer ASAimage/vnd.fst.fstIANA: FAST Search & Transfer ASA
EDMICS 2000image/vnd.fujixerox.edmics-mmr.mmrIANA: EDMICS 2000
EDMICS 2000image/vnd.fujixerox.edmics-rlc.rlcIANA: EDMICS 2000
Microsoft Document Imaging Formatimage/vnd.ms-modi.mdiWikipedia: Microsoft Document Image Format
FlashPiximage/vnd.net-fpx.npxIANA: FPX
WAP Bitamp (WBMP)image/vnd.wap.wbmp.wbmpIANA: WBMP
eXtended Image File Format (XIFF)image/vnd.xiff.xifIANA: XIFF
WebP Imageimage/webp.webpWikipedia: WebP
CMU Imageimage/x-cmu-raster.ras
Corel Metafile Exchange (CMX)image/x-cmx.cmxWikipedia: CorelDRAW
FreeHand MXimage/x-freehand.fhWikipedia: Macromedia Freehand
Icon Imageimage/x-icon.icoWikipedia: ICO File Format
PCX Imageimage/x-pcx.pcxWikipedia: PCX
PICT Imageimage/x-pict.picWikipedia: PICT
Portable Anymap Imageimage/x-portable-anymap.pnmWikipedia: Netpbm Format
Portable Bitmap Formatimage/x-portable-bitmap.pbmWikipedia: Netpbm Format
Portable Graymap Formatimage/x-portable-graymap.pgmWikipedia: Netpbm Format
Portable Pixmap Formatimage/x-portable-pixmap.ppmWikipedia: Netpbm Format
Silicon Graphics RGB Bitmapimage/x-rgb.rgbRGB Image Format
X BitMapimage/x-xbitmap.xbmWikipedia: X BitMap
X PixMapimage/x-xpixmap.xpmWikipedia: X PixMap
X Window Dumpimage/x-xwindowdump.xwdWikipedia: X Window Dump
Email Messagemessage/rfc822.emlRFC 2822
Initial Graphics Exchange Specification (IGES)model/iges.igsWikipedia: IGES
Mesh Data Typemodel/mesh.mshRFC 2077
COLLADAmodel/vnd.collada+xml.daeIANA: COLLADA
Autodesk Design Web Format (DWF)model/vnd.dwf.dwfWikipedia: Design Web Format
Geometric Description Language (GDL)model/vnd.gdl.gdlIANA: GDL
Gen-Trix Studiomodel/vnd.gtw.gtwIANA: GTW
Virtue MTSmodel/vnd.mts.mtsIANA: MTS
Virtue VTUmodel/vnd.vtu.vtuIANA: VTU
Virtual Reality Modeling Languagemodel/vrml.wrlWikipedia: VRML
iCalendartext/calendar.icsWikipedia: iCalendar
Cascading Style Sheets (CSS)text/css.cssWikipedia: CSS
Comma-Seperated Valuestext/csv.csvWikipedia: CSV
HyperText Markup Language (HTML)text/html.htmlWikipedia: HTML
Notation3text/n3.n3Wikipedia: Notation3
Text Filetext/plain.txtWikipedia: Text File
PRS Lines Tagtext/prs.lines.tag.dscIANA: PRS Lines Tag
Rich Text Format (RTF)text/richtext.rtxWikipedia: Rich Text Format
Standard Generalized Markup Language (SGML)text/sgml.sgmlWikipedia: SGML
Tab Seperated Valuestext/tab-separated-values.tsvWikipedia: TSV
trofftext/troff.tWikipedia: troff
Turtle (Terse RDF Triple Language)text/turtle.ttlWikipedia: Turtle
URI Resolution Servicestext/uri-list.uriRFC 2483
Curl - Applettext/vnd.curl.curlCurl Applet
Curl - Detached Applettext/vnd.curl.dcurl.dcurlCurl Detached Applet
Curl - Source Codetext/vnd.curl.scurl.scurlCurl Source Code
Curl - Manifest Filetext/vnd.curl.mcurl.mcurlCurl Manifest File
mod_fly / fly.cgitext/vnd.fly.flyIANA: Fly
FLEXSTORtext/vnd.fmi.flexstor.flxIANA: FLEXSTOR
Graphviztext/vnd.graphviz.gvIANA: Graphviz
In3D - 3DMLtext/vnd.in3d.3dml.3dmlIANA: In3D
In3D - 3DMLtext/vnd.in3d.spot.spotIANA: In3D
J2ME App Descriptortext/vnd.sun.j2me.app-descriptor.jadIANA: J2ME App Descriptor
Wireless Markup Language (WML)text/vnd.wap.wml.wmlWikipedia: WML
Wireless Markup Language Script (WMLScript)text/vnd.wap.wmlscript.wmlsWikipedia: WMLScript
Assembler Source Filetext/x-asm.sWikipedia: Assembly
C Source Filetext/x-c.cWikipedia: C Programming Language
Fortran Source Filetext/x-fortran.fWikipedia: Fortran
Pascal Source Filetext/x-pascal.pWikipedia: Pascal
Java Source Filetext/x-java-source,java.javaWikipedia: Java
Setexttext/x-setext.etxWikipedia: Setext
UUEncodetext/x-uuencode.uuWikipedia: UUEncode
vCalendartext/x-vcalendar.vcsWikipedia: vCalendar
vCardtext/x-vcard.vcfWikipedia: vCard
3GPvideo/3gpp.3gpWikipedia: 3GP
3GP2video/3gpp2.3g2Wikipedia: 3G2
H.261video/h261.h261Wikipedia: H.261
H.263video/h263.h263Wikipedia: H.263
H.264video/h264.h264Wikipedia: H.264
JPGVideovideo/jpeg.jpgvRFC 3555
JPEG 2000 Compound Image File Formatvideo/jpm.jpmIANA: JPM
Motion JPEG 2000video/mj2.mj2IANA: MJ2
MPEG-4 Videovideo/mp4.mp4Wikipedia: MP4
MPEG Videovideo/mpeg.mpegWikipedia: MPEG
Ogg Videovideo/ogg.ogvWikipedia: Ogg
Quicktime Videovideo/quicktime.qtWikipedia: Quicktime
DECE High Definition Videovideo/vnd.dece.hd.uvhIANA: DECE HD Video
DECE Mobile Videovideo/vnd.dece.mobile.uvmIANA: DECE Mobile Video
DECE PD Videovideo/vnd.dece.pd.uvpIANA: DECE PD Video
DECE SD Videovideo/vnd.dece.sd.uvsIANA: DECE SD Video
DECE Videovideo/vnd.dece.video.uvvIANA: DECE Video
FAST Search & Transfer ASAvideo/vnd.fvt.fvtIANA: FVT
MPEG Urlvideo/vnd.mpegurl.mxuIANA: MPEG Url
Microsoft PlayReady Ecosystem Videovideo/vnd.ms-playready.media.pyv.pyvIANA: Microsoft PlayReady Ecosystem
DECE MP4video/vnd.uvvu.mp4.uvuIANA: DECE MP4
Vivovideo/vnd.vivo.vivIANA: Vivo
Open Web Media Project - Videovideo/webm.webmWebM Project
Flash Videovideo/x-f4v.f4vWikipedia: Flash Video
FLI/FLC Animation Formatvideo/x-fli.fliFLI/FLC Animation Format
Flash Videovideo/x-flv.flvWikipedia: Flash Video
M4vvideo/x-m4v.m4vWikipedia: M4v
Microsoft Advanced Systems Format (ASF)video/x-ms-asf.asfWikipedia: Advanced Systems Format (ASF)
Microsoft Windows Mediavideo/x-ms-wm.wmWikipedia: Advanced Systems Format (ASF)
Microsoft Windows Media Videovideo/x-ms-wmv.wmvWikipedia: Advanced Systems Format (ASF)
Microsoft Windows Media Audio/Video Playlistvideo/x-ms-wmx.wmxWikipedia: Advanced Systems Format (ASF)
Microsoft Windows Media Video Playlistvideo/x-ms-wvx.wvxWikipedia: Advanced Systems Format (ASF)
Audio Video Interleave (AVI)video/x-msvideo.aviWikipedia: AVI
SGI Movievideo/x-sgi-movie.movieSGI Facts
CoolTalkx-conference/x-cooltalk.iceWikipedia: CoolTalk
BAS Partitur Formattext/plain-bas.parPhonetik BAS
YAML Ain't Markup Language / Yet Another Markup Languagetext/yaml.yamlYAML: YAML Ain't Markup Language
Apple Disk Imageapplication/x-apple-diskimage.dmgApple Disk Image
\ No newline at end of file diff --git a/mppAndroidProject b/mppAndroidProject.gradle similarity index 82% rename from mppAndroidProject rename to mppAndroidProject.gradle index 7f0b8ef6..af688eec 100644 --- a/mppAndroidProject +++ b/mppAndroidProject.gradle @@ -1,7 +1,7 @@ -project.version = "$version" + System.getenv("additional_version") +project.version = "$version" project.group = "$group" -apply from: "$publishGradlePath" +// apply from: "$publishGradlePath" kotlin { android { diff --git a/mppJavaProject.gradle b/mppJavaProject.gradle new file mode 100644 index 00000000..d006f30b --- /dev/null +++ b/mppJavaProject.gradle @@ -0,0 +1,33 @@ +project.version = "$version" +project.group = "$group" + + apply from: "$publishGradlePath" + +kotlin { + jvm().compilations.main { + kotlinOptions { + jvmTarget = "1.8" + targetCompatibility = "1.8" + } + } + + sourceSets { + commonMain { + dependencies { + implementation kotlin('stdlib') + } + } + commonTest { + dependencies { + implementation kotlin('test-common') + implementation kotlin('test-annotations-common') + } + } + + jvmTest { + dependencies { + implementation kotlin('test-junit') + } + } + } +} diff --git a/mppJavaProject b/mppJsProject.gradle similarity index 72% rename from mppJavaProject rename to mppJsProject.gradle index 7ccd11e9..761487a3 100644 --- a/mppJavaProject +++ b/mppJsProject.gradle @@ -1,10 +1,13 @@ -project.version = "$version" + System.getenv("additional_version") +project.version = "$version" project.group = "$group" -apply from: "$publishGradlePath" + apply from: "$publishGradlePath" kotlin { - jvm() + js (IR) { + browser() + nodejs() + } sourceSets { commonMain { @@ -18,9 +21,9 @@ kotlin { implementation kotlin('test-annotations-common') } } - - jvmTest { + jsTest { dependencies { + implementation kotlin('test-js') implementation kotlin('test-junit') } } diff --git a/mppProjectWithSerialization b/mppProjectWithSerialization.gradle similarity index 84% rename from mppProjectWithSerialization rename to mppProjectWithSerialization.gradle index a2d85b64..864b3dff 100644 --- a/mppProjectWithSerialization +++ b/mppProjectWithSerialization.gradle @@ -1,11 +1,16 @@ -project.version = "$version" + System.getenv("additional_version") +project.version = "$version" project.group = "$group" -apply from: "$publishGradlePath" + apply from: "$publishGradlePath" kotlin { - jvm() - js (BOTH) { + jvm().compilations.main { + kotlinOptions { + jvmTarget = "1.8" + targetCompatibility = "1.8" + } + } + js (IR) { browser() nodejs() } diff --git a/pubconf.kpsb b/pubconf.kpsb deleted file mode 100644 index 8c140cca..00000000 --- a/pubconf.kpsb +++ /dev/null @@ -1 +0,0 @@ -{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://opensource.org/licenses/Apache-2.0"}],"mavenConfig":{"name":"${project.name}","description":"","url":"https://git.inmo.dev:8322/PostsSystem/Core","vcsUrl":"ssh://git@git.inmo.dev:8322/PostsSystem/Core.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GitHubPackages","url":"https://maven.pkg.github.com/PostsSystem/Core"}]}} \ No newline at end of file diff --git a/publish.gradle b/publish.gradle index 778ff2c8..78519469 100644 --- a/publish.gradle +++ b/publish.gradle @@ -9,13 +9,13 @@ publishing { artifact javadocsJar pom { - description = "" + description = "${project.name}" name = "${project.name}" - url = "https://git.inmo.dev:8322/PostsSystem/Core" + url = "https://github.com/PostsSystem/core.git" scm { - developerConnection = "scm:git:[fetch=]ssh://git@git.inmo.dev:8322/PostsSystem/Core.git[push=]ssh://git@git.inmo.dev:8322/PostsSystem/Core.git" - url = "ssh://git@git.inmo.dev:8322/PostsSystem/Core.git" + developerConnection = "scm:git:[fetch=]git@github.com:PostsSystem/core.git[push=]git@github.com:PostsSystem/core.git" + url = "git@github.com:PostsSystem/core.git" } developers { @@ -45,12 +45,14 @@ publishing { } } repositories { - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/PostsSystem/Core") - credentials { - username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER') - password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD') + if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/PostsSystem/core") + credentials { + username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER') + password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD') + } } } } diff --git a/publish.kpsb b/publish.kpsb new file mode 100644 index 00000000..b1c7593c --- /dev/null +++ b/publish.kpsb @@ -0,0 +1 @@ +{"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://opensource.org/licenses/Apache-2.0"}],"mavenConfig":{"name":"${project.name}","description":"${project.name}","url":"https://github.com/PostsSystem/core.git","vcsUrl":"git@github.com:PostsSystem/core.git","developers":[{"id":"InsanusMokrassar","name":"Aleksei Ovsiannikov","eMail":"ovsyannikov.alexey95@gmail.com"},{"id":"000Sanya","name":"Syrov Aleksandr","eMail":"000sanya.000sanya@gmail.com"}],"repositories":[{"name":"GitHubPackages","url":"https://maven.pkg.github.com/PostsSystem/core"}]}} \ No newline at end of file diff --git a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/PostKeyGenerator.kt b/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/PostKeyGenerator.kt deleted file mode 100644 index f47b3976..00000000 --- a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/PostKeyGenerator.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.inmo.postssystem.core.publishing - -import dev.inmo.postssystem.core.post.PostId - -typealias PostKeyGenerator = suspend (PostId, TriggerId) -> TriggerControlKey diff --git a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/PublishingKeyReceiver.kt b/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/PublishingKeyReceiver.kt deleted file mode 100644 index 762d8d0a..00000000 --- a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/PublishingKeyReceiver.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.inmo.postssystem.core.publishing - -import dev.inmo.postssystem.core.post.PostId - -typealias PublishingKeyReceiverGetter = suspend (TriggerId) -> PublishingKeyReceiver? - -interface PublishingKeyReceiver : Trigger { - suspend fun acceptKey(postId: PostId, publishingKey: TriggerControlKey) -} diff --git a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/PublishingKeysRegistrar.kt b/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/PublishingKeysRegistrar.kt deleted file mode 100644 index 587ed906..00000000 --- a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/PublishingKeysRegistrar.kt +++ /dev/null @@ -1,67 +0,0 @@ -package dev.inmo.postssystem.core.publishing - -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.publishing.repos.PublishingKeysRepo -import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow - -typealias TriggerControlKey = String - -interface ReadPublishingRegistrar { - suspend fun getPostIdByTriggerControlKey( - key: TriggerControlKey - ): PostId? -} - -interface WritePublishingRegistrar { - val unregisteredKeysFlow: Flow - - suspend fun registerTriggerForPost( - key: TriggerControlKey, - postId: PostId - ): Boolean - suspend fun unregisterTriggerForPost( - postId: PostId - ): Boolean -} - -interface PublishingKeysRegistrar : ReadPublishingRegistrar, WritePublishingRegistrar - -class BusinessPublishingKeysRegistrar( - private val repo: PublishingKeysRepo -) : PublishingKeysRegistrar { - private val unregisteredKeysChannel: BroadcastChannel = BroadcastChannel(Channel.BUFFERED) - override val unregisteredKeysFlow: Flow = unregisteredKeysChannel.asFlow() - - override suspend fun getPostIdByTriggerControlKey( - key: TriggerControlKey - ): PostId? = repo.getPostIdByTriggerControlKey(key) - - override suspend fun registerTriggerForPost( - key: TriggerControlKey, - postId: PostId - ): Boolean = repo.getTriggerControlKeyByPostId( - postId - ).let { previousKey -> - repo.setPostTriggerControlKey(postId, key).also { inserted -> - if (inserted && previousKey != null) { - unregisteredKeysChannel.send(previousKey) - } - } - } - - override suspend fun unregisterTriggerForPost( - postId: PostId - ): Boolean = repo.getTriggerControlKeyByPostId( - postId - ).let { previousKey -> - repo.unsetPostTriggerControlKey(postId).also { - if (it && previousKey != null) { - unregisteredKeysChannel.send(previousKey) - } - } - } -} - diff --git a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/PublishingService.kt b/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/PublishingService.kt deleted file mode 100644 index ee05ae04..00000000 --- a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/PublishingService.kt +++ /dev/null @@ -1,17 +0,0 @@ -package dev.inmo.postssystem.core.publishing - -import dev.inmo.postssystem.core.post.repo.PostsRepo -import dev.inmo.postssystem.core.publishing.repos.PublishedPostsWriteRepo -import dev.inmo.postssystem.core.publishing.repos.PublishingKeysRepo - -class PublishingService( - private val postsRepo: PostsRepo, - private val publishedPostsRepo: PublishedPostsWriteRepo, - private val keysRepo: PublishingKeysRepo -) : PublishingKeysRegistrar by BusinessPublishingKeysRegistrar( - keysRepo -), PublishingTrigger by BusinessPublishingTrigger( - postsRepo, - publishedPostsRepo, - keysRepo -) diff --git a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/PublishingTrigger.kt b/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/PublishingTrigger.kt deleted file mode 100644 index 37c5d191..00000000 --- a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/PublishingTrigger.kt +++ /dev/null @@ -1,37 +0,0 @@ -package dev.inmo.postssystem.core.publishing - -import dev.inmo.micro_utils.repos.create -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.post.RegisteredPost -import dev.inmo.postssystem.core.post.repo.PostsRepo -import dev.inmo.postssystem.core.publishing.repos.* -import kotlinx.coroutines.flow.* -import kotlinx.serialization.Serializable - -interface PublishingTrigger { - val postingTriggeredFlow: SharedFlow - - suspend fun triggerPosting( - triggerControlKey: TriggerControlKey - ): PostId? -} - -class BusinessPublishingTrigger( - private val postsRepo: PostsRepo, - private val publishedPostsRepo: PublishedPostsWriteRepo, - private val publishingKeysRepo: PublishingKeysRepo -) : PublishingTrigger { - private val _postingTriggeredFlow: MutableSharedFlow = MutableSharedFlow() - override val postingTriggeredFlow: SharedFlow = _postingTriggeredFlow.asSharedFlow() - - override suspend fun triggerPosting(triggerControlKey: TriggerControlKey): PostId? { - val postId = publishingKeysRepo.getPostIdByTriggerControlKey(triggerControlKey) ?: return null - publishingKeysRepo.unsetPostTriggerControlKey(postId) - - return postsRepo.getPostById(postId) ?.let { post -> - publishedPostsRepo.create(post).firstOrNull() ?.also { - _postingTriggeredFlow.emit(it) - } - } ?.id - } -} diff --git a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/Trigger.kt b/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/Trigger.kt deleted file mode 100644 index 75abceaf..00000000 --- a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/Trigger.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.inmo.postssystem.core.publishing - -typealias TriggerId = String - -interface Trigger { - val id: TriggerId -} \ No newline at end of file diff --git a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/repos/PublishedPostsRepo.kt b/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/repos/PublishedPostsRepo.kt deleted file mode 100644 index 3a6cc762..00000000 --- a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/repos/PublishedPostsRepo.kt +++ /dev/null @@ -1,43 +0,0 @@ -package dev.inmo.postssystem.core.publishing.repos - -import com.soywiz.klock.DateTime -import dev.inmo.micro_utils.pagination.* -import dev.inmo.micro_utils.repos.* -import dev.inmo.postssystem.core.UnixMillis -import dev.inmo.postssystem.core.content.Content -import dev.inmo.postssystem.core.content.RegisteredContent -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.post.RegisteredPost -import dev.inmo.postssystem.core.post.repo.PostsRepo -import kotlinx.coroutines.flow.Flow -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient - -typealias PublishedPostId = String -@Serializable -data class PublishedPost( - val id: PublishedPostId, - val post: RegisteredPost, - val content: List, - private val publicationTime: UnixMillis -) { - @Transient - val publicationDateTime = DateTime(publicationTime) -} - -interface PublishedPostsReadRepo : ReadCRUDRepo { - suspend fun getPostPublishing( - postId: PostId, - pagination: Pagination, - reversed: Boolean = false - ): PaginationResult -} -suspend inline fun PostId.publishedAtLeastOnce(lookIn: PublishedPostsReadRepo): Boolean { - return lookIn.getPostPublishing(this, FirstPagePagination(1)).results.isNotEmpty() -} -suspend inline fun RegisteredPost.publishedAtLeastOnce(lookIn: PublishedPostsReadRepo): Boolean { - return lookIn.getPostPublishing(id, FirstPagePagination(1)).results.isNotEmpty() -} -interface PublishedPostsWriteRepo : WriteCRUDRepo - -interface PublishedPostsRepo : PublishedPostsReadRepo, PublishedPostsWriteRepo, CRUDRepo diff --git a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/repos/PublishingKeysRepo.kt b/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/repos/PublishingKeysRepo.kt deleted file mode 100644 index da508a80..00000000 --- a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/repos/PublishingKeysRepo.kt +++ /dev/null @@ -1,54 +0,0 @@ -package dev.inmo.postssystem.core.publishing.repos - -import dev.inmo.micro_utils.pagination.FirstPagePagination -import dev.inmo.micro_utils.repos.* -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.publishing.TriggerControlKey - -interface ReadPublishingKeysRepo { - suspend fun getPostIdByTriggerControlKey( - key: TriggerControlKey - ): PostId? - suspend fun getTriggerControlKeyByPostId( - postId: PostId - ): TriggerControlKey? -} - -interface WritePublishingKeysRepo { - suspend fun setPostTriggerControlKey( - postId: PostId, - key: TriggerControlKey - ): Boolean - suspend fun unsetPostTriggerControlKey( - postId: PostId - ): Boolean -} - -interface PublishingKeysRepo : ReadPublishingKeysRepo, WritePublishingKeysRepo - -fun PublishingKeysRepo( - keyValueRepo: KeyValueRepo -) = object : PublishingKeysRepo { - override suspend fun getPostIdByTriggerControlKey( - key: TriggerControlKey - ): PostId? = keyValueRepo.keys(key, FirstPagePagination(1)).results.firstOrNull() - - override suspend fun getTriggerControlKeyByPostId( - postId: PostId - ): TriggerControlKey? = keyValueRepo.get(postId) - - override suspend fun setPostTriggerControlKey( - postId: PostId, - key: TriggerControlKey - ): Boolean { - keyValueRepo.set(postId, key) - return true - } - - override suspend fun unsetPostTriggerControlKey( - postId: PostId - ): Boolean { - keyValueRepo.unset(postId) - return true - } -} diff --git a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/repos/TriggersToPostsRepo.kt b/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/repos/TriggersToPostsRepo.kt deleted file mode 100644 index 132eb1b3..00000000 --- a/publishing/api/src/commonMain/kotlin/dev/inmo/postssystem/core/publishing/repos/TriggersToPostsRepo.kt +++ /dev/null @@ -1,37 +0,0 @@ -package dev.inmo.postssystem.core.publishing.repos - -import dev.inmo.micro_utils.repos.* -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.publishing.* - -interface ReadTriggersToPostsRepo : ReadKeyValueRepo -interface WriteTriggersToPostsRepo : WriteKeyValueRepo -interface TriggersToPostsRepo : KeyValueRepo, WriteTriggersToPostsRepo, ReadTriggersToPostsRepo - -fun TriggersToPostsRepo( - keyValueRepo: KeyValueRepo, - generator: PostKeyGenerator, - publishingKeyReceiverGetter: PublishingKeyReceiverGetter, - writePublishingKeysRegistrar: WritePublishingRegistrar -): TriggersToPostsRepo = object : TriggersToPostsRepo, KeyValueRepo by keyValueRepo { - override suspend fun set(toSet: Map) { - keyValueRepo.set( - toSet.filter { (postId, triggerId) -> - val publishingKeyReceiver = publishingKeyReceiverGetter(triggerId) ?: return@filter false - val publishingKey = generator(postId, triggerId) - writePublishingKeysRegistrar.registerTriggerForPost(publishingKey, postId).also { - if (it) { - publishingKeyReceiver.acceptKey(postId, publishingKey) - } - } - } - ) - } - - override suspend fun unset(toUnset: List) { - toUnset.forEach { - writePublishingKeysRegistrar.unregisterTriggerForPost(it) - } - keyValueRepo.unset(toUnset) - } -} diff --git a/publishing/api/src/main/AndroidManifest.xml b/publishing/api/src/main/AndroidManifest.xml deleted file mode 100644 index a923875e..00000000 --- a/publishing/api/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/publishing/exposed/gradle.properties b/publishing/exposed/gradle.properties deleted file mode 100644 index abc24679..00000000 --- a/publishing/exposed/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -test_sqlite_version=3.28.0 diff --git a/publishing/exposed/src/jvmMain/kotlin/dev/inmo/postssystem/core/publishing/exposed/ExposedPublishedPostsRepo.kt b/publishing/exposed/src/jvmMain/kotlin/dev/inmo/postssystem/core/publishing/exposed/ExposedPublishedPostsRepo.kt deleted file mode 100644 index 212d4875..00000000 --- a/publishing/exposed/src/jvmMain/kotlin/dev/inmo/postssystem/core/publishing/exposed/ExposedPublishedPostsRepo.kt +++ /dev/null @@ -1,92 +0,0 @@ -package dev.inmo.postssystem.core.publishing.exposed - -import com.soywiz.klock.DateTime -import dev.inmo.micro_utils.coroutines.launchSynchronously -import dev.inmo.micro_utils.pagination.* -import dev.inmo.micro_utils.pagination.utils.reverse -import dev.inmo.micro_utils.repos.exposed.AbstractExposedCRUDRepo -import dev.inmo.micro_utils.repos.exposed.initTable -import dev.inmo.postssystem.core.content.api.ReadContentRepo -import dev.inmo.postssystem.core.generateId -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.post.RegisteredPost -import dev.inmo.postssystem.core.post.repo.ReadPostsRepo -import dev.inmo.postssystem.core.publishing.repos.* -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.statements.InsertStatement -import org.jetbrains.exposed.sql.statements.UpdateStatement -import org.jetbrains.exposed.sql.transactions.transaction - -class ExposedPublishedPostsRepo( - override val database: Database, - private val postsRepo: ReadPostsRepo, - private val contentRepo: ReadContentRepo, - tableName: String = "PublishedPostsRepo", - private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default) -) : PublishedPostsRepo, - AbstractExposedCRUDRepo( - tableName = tableName - ) { - private val idColumn = text("id") - private val postIdColumn = text("postId") - private val dateTimeColumn = double("dateTime").clientDefault { - DateTime.now().unixMillis - } - - override val selectByIds: SqlExpressionBuilder.(List) -> Op = { idColumn.inList(it) } - override val InsertStatement.asObject: PublishedPost get() = TODO() - - override fun InsertStatement.asObject(value: RegisteredPost): PublishedPost = PublishedPost( - get(idColumn), - value, - scope.launchSynchronously { value.content.mapNotNull { contentRepo.getById(it) } }, - get(dateTimeColumn) - ) - - override val selectById: SqlExpressionBuilder.(PublishedPostId) -> Op = { idColumn.eq(it) } - override val ResultRow.asObject: PublishedPost - get() { - val (post, content) = scope.launchSynchronously { - val post = requireNotNull(postsRepo.getPostById(get(postIdColumn))) {"Post with id \"${get(postIdColumn)}\" not found" } - post to post.content.mapNotNull { contentRepo.getById(it) } - } - return PublishedPost( - get(idColumn), - post, - content, - get(dateTimeColumn) - ) - } - - - init { - initTable() - } - - override fun insert(value: RegisteredPost, it: InsertStatement) { - it[idColumn] = generateId() - it[postIdColumn] = value.id - } - - override fun update(id: PublishedPostId, value: RegisteredPost, it: UpdateStatement) { - it[postIdColumn] = value.id - } - - override suspend fun getPostPublishing( - postId: PostId, - pagination: Pagination, - reversed: Boolean - ): PaginationResult = transaction(database) { - val query = select { postIdColumn.eq(postId) } - query.paginate(pagination, postIdColumn, reversed).map { - it[idColumn] - } to query.count() - }.let { (results, count) -> - results.createPaginationResult( - if (reversed) pagination.reverse(count) else pagination, - count - ) - } -} \ No newline at end of file diff --git a/publishing/exposed/src/jvmMain/kotlin/dev/inmo/postssystem/core/publishing/exposed/ExposedPublishingKeysRepo.kt b/publishing/exposed/src/jvmMain/kotlin/dev/inmo/postssystem/core/publishing/exposed/ExposedPublishingKeysRepo.kt deleted file mode 100644 index 6f800881..00000000 --- a/publishing/exposed/src/jvmMain/kotlin/dev/inmo/postssystem/core/publishing/exposed/ExposedPublishingKeysRepo.kt +++ /dev/null @@ -1,59 +0,0 @@ -package dev.inmo.postssystem.core.publishing.exposed - -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.publishing.TriggerControlKey -import dev.inmo.postssystem.core.publishing.repos.PublishingKeysRepo -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.transactions.transaction - -class ExposedPublishingKeysRepo( - private val database: Database -) : PublishingKeysRepo, Table() { - private val postIdColumn: Column = text("postId") - private val triggerControlKeyColumn: Column = text("triggerControlKey") - override val primaryKey: PrimaryKey = PrimaryKey(postIdColumn, triggerControlKeyColumn) - - init { - transaction( - db = database - ) { - SchemaUtils.createMissingTablesAndColumns(this@ExposedPublishingKeysRepo) - } - } - - override suspend fun getPostIdByTriggerControlKey(key: TriggerControlKey): PostId? = transaction( - db = database - ) { - select { triggerControlKeyColumn.eq(key) }.limit(1).firstOrNull() ?.getOrNull(postIdColumn) - } - - override suspend fun getTriggerControlKeyByPostId(postId: PostId): TriggerControlKey? = transaction( - db = database - ) { - select { postIdColumn.eq(postId) }.limit(1).firstOrNull() ?.getOrNull(triggerControlKeyColumn) - } - - override suspend fun setPostTriggerControlKey(postId: PostId, key: TriggerControlKey): Boolean { - unsetPostTriggerControlKey(postId) - return transaction( - db = database - ) { - insert { - it[postIdColumn] = postId - it[triggerControlKeyColumn] = triggerControlKeyColumn - }.getOrNull(postIdColumn) == postId - } - } - - override suspend fun unsetPostTriggerControlKey(postId: PostId): Boolean = transaction( - db = database - ) { - deleteWhere { - postIdColumn.eq(postId) - } > 0 - } -} - -class DatabasePublishingKeysRepo( - database: Database -): PublishingKeysRepo by ExposedPublishingKeysRepo(database) diff --git a/publishing/ktor/client/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/client/PublishingKeysRepoKtorClient.kt b/publishing/ktor/client/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/client/PublishingKeysRepoKtorClient.kt deleted file mode 100644 index b83a46c9..00000000 --- a/publishing/ktor/client/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/client/PublishingKeysRepoKtorClient.kt +++ /dev/null @@ -1,28 +0,0 @@ -package dev.inmo.postssystem.publishing.ktor.client - -import dev.inmo.postssystem.core.publishing.repos.* -import dev.inmo.postssystem.publishing.ktor.publishingKeysRootRoute -import io.ktor.client.HttpClient -import io.ktor.client.features.websocket.WebSockets - -class PublishingKeysRepoKtorClient private constructor ( - readPublishingKeysClient: ReadPublishingKeysRepoKtorClient, - writePublishingKeysClient: WritePublishingKeysRepoKtorClient -) : PublishingKeysRepo, ReadPublishingKeysRepo by readPublishingKeysClient, WritePublishingKeysRepo by writePublishingKeysClient { - constructor( - baseUrl: String, - subpath: String? = publishingKeysRootRoute, - client: HttpClient = HttpClient { - install(WebSockets) - } - ) : this ( - ReadPublishingKeysRepoKtorClient ( - subpath ?.let { "$baseUrl/$subpath" } ?: baseUrl, - client - ), - WritePublishingKeysRepoKtorClient( - subpath ?.let { "$baseUrl/$subpath" } ?: baseUrl, - client - ) - ) -} \ No newline at end of file diff --git a/publishing/ktor/client/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/client/ReadPublishingKeysRepoKtorClient.kt b/publishing/ktor/client/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/client/ReadPublishingKeysRepoKtorClient.kt deleted file mode 100644 index cf252d55..00000000 --- a/publishing/ktor/client/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/client/ReadPublishingKeysRepoKtorClient.kt +++ /dev/null @@ -1,27 +0,0 @@ -package dev.inmo.postssystem.publishing.ktor.client - -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.publishing.TriggerControlKey -import dev.inmo.postssystem.core.publishing.repos.ReadPublishingKeysRepo -import dev.inmo.postssystem.publishing.ktor.getPostIdByTriggerControlKeyRoute -import dev.inmo.postssystem.publishing.ktor.getTriggerControlKeyByPostIdRoute -import dev.inmo.micro_utils.ktor.client.uniget -import dev.inmo.micro_utils.ktor.common.buildStandardUrl -import io.ktor.client.HttpClient -import kotlinx.serialization.builtins.nullable -import kotlinx.serialization.builtins.serializer - -class ReadPublishingKeysRepoKtorClient ( - private val baseUrl: String, - private val client: HttpClient = HttpClient() -) : ReadPublishingKeysRepo { - override suspend fun getPostIdByTriggerControlKey(key: TriggerControlKey): PostId? = client.uniget( - buildStandardUrl(baseUrl, "$getPostIdByTriggerControlKeyRoute/$key"), - PostId.serializer().nullable - ) - - override suspend fun getTriggerControlKeyByPostId(postId: PostId): TriggerControlKey? = client.uniget( - buildStandardUrl(baseUrl, "$getTriggerControlKeyByPostIdRoute/$postId"), - TriggerControlKey.serializer().nullable - ) -} \ No newline at end of file diff --git a/publishing/ktor/client/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/client/WritePublishingKeysRepoKtorClient.kt b/publishing/ktor/client/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/client/WritePublishingKeysRepoKtorClient.kt deleted file mode 100644 index 83930ed7..00000000 --- a/publishing/ktor/client/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/client/WritePublishingKeysRepoKtorClient.kt +++ /dev/null @@ -1,28 +0,0 @@ -package dev.inmo.postssystem.publishing.ktor.client - -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.publishing.TriggerControlKey -import dev.inmo.postssystem.core.publishing.repos.WritePublishingKeysRepo -import dev.inmo.postssystem.publishing.ktor.* -import dev.inmo.micro_utils.ktor.client.BodyPair -import dev.inmo.micro_utils.ktor.client.unipost -import dev.inmo.micro_utils.ktor.common.buildStandardUrl -import io.ktor.client.HttpClient -import kotlinx.serialization.builtins.serializer - -class WritePublishingKeysRepoKtorClient ( - private val baseUrl: String, - private val client: HttpClient = HttpClient() -) : WritePublishingKeysRepo { - override suspend fun setPostTriggerControlKey(postId: PostId, key: TriggerControlKey): Boolean = client.unipost( - buildStandardUrl(baseUrl, setPostTriggerControlKeyRoute), - BodyPair(SetPostTriggerControlKeyObject.serializer(), SetPostTriggerControlKeyObject(postId, key)), - Boolean.serializer() - ) - - override suspend fun unsetPostTriggerControlKey(postId: PostId): Boolean = client.unipost( - buildStandardUrl(baseUrl, unsetPostTriggerControlKeyRoute), - BodyPair(PostId.serializer(), postId), - Boolean.serializer() - ) -} \ No newline at end of file diff --git a/publishing/ktor/client/src/main/AndroidManifest.xml b/publishing/ktor/client/src/main/AndroidManifest.xml deleted file mode 100644 index 47ff02ac..00000000 --- a/publishing/ktor/client/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/PublishingKeysRoutes.kt b/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/PublishingKeysRoutes.kt deleted file mode 100644 index 25dd57d4..00000000 --- a/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/PublishingKeysRoutes.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.inmo.postssystem.publishing.ktor - -const val publishingKeysRootRoute = "publishingKeys" - -const val getPostIdByTriggerControlKeyRoute = "getPostIdByTriggerControlKey" -const val getTriggerControlKeyByPostIdRoute = "getTriggerControlKeyByPostId" - -const val setPostTriggerControlKeyRoute = "setPostTriggerControlKey" -const val unsetPostTriggerControlKeyRoute = "unsetPostTriggerControlKey" \ No newline at end of file diff --git a/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/PublishingRegistrarRoutes.kt b/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/PublishingRegistrarRoutes.kt deleted file mode 100644 index a81a04c9..00000000 --- a/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/PublishingRegistrarRoutes.kt +++ /dev/null @@ -1,4 +0,0 @@ -package dev.inmo.postssystem.publishing.ktor - -//const val getPostIdByTriggerControlKeyRoute = "getPostIdByTriggerControlKey" -const val registerTriggerForPostRoute = "registerTriggerForPost" diff --git a/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/PublishingTriggerRoutes.kt b/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/PublishingTriggerRoutes.kt deleted file mode 100644 index 20881e68..00000000 --- a/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/PublishingTriggerRoutes.kt +++ /dev/null @@ -1,3 +0,0 @@ -package dev.inmo.postssystem.publishing.ktor - -const val triggerPostingRoute = "triggerPosting" \ No newline at end of file diff --git a/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/Serializers.kt b/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/Serializers.kt deleted file mode 100644 index 728f4dd5..00000000 --- a/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/Serializers.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.inmo.postssystem.publishing.ktor - -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.publishing.TriggerControlKey -import kotlinx.serialization.builtins.SetSerializer -import kotlinx.serialization.builtins.serializer - -val postsIdSerializer = SetSerializer(PostId.serializer()) -val triggerControlKeysSerializer = SetSerializer(TriggerControlKey.serializer()) diff --git a/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/SetPostTriggerControlKeyObject.kt b/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/SetPostTriggerControlKeyObject.kt deleted file mode 100644 index a77e6dc8..00000000 --- a/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/SetPostTriggerControlKeyObject.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.inmo.postssystem.publishing.ktor - -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.publishing.TriggerControlKey -import kotlinx.serialization.Serializable - -@Serializable -data class SetPostTriggerControlKeyObject ( - val postId: PostId, - val key: TriggerControlKey -) \ No newline at end of file diff --git a/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/TriggerSetterConfiguratorRoutes.kt b/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/TriggerSetterConfiguratorRoutes.kt deleted file mode 100644 index 377ed769..00000000 --- a/publishing/ktor/common/src/commonMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/TriggerSetterConfiguratorRoutes.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.insanusmokrassar.postssystem.publishing.ktor - -import dev.inmo.postssystem.core.content.ContentId -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.publishing.TriggerId -import kotlinx.serialization.Serializable - -const val triggersRootRoute = "triggers" -const val setTriggerSubRoute = "set" - -@Serializable -data class TriggerSettingData(val postId: PostId, val triggerId: TriggerId) diff --git a/publishing/ktor/common/src/main/AndroidManifest.xml b/publishing/ktor/common/src/main/AndroidManifest.xml deleted file mode 100644 index d52bf584..00000000 --- a/publishing/ktor/common/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/publishing/ktor/server/build.gradle b/publishing/ktor/server/build.gradle deleted file mode 100644 index 3dae25cf..00000000 --- a/publishing/ktor/server/build.gradle +++ /dev/null @@ -1,26 +0,0 @@ -plugins { - id "org.jetbrains.kotlin.multiplatform" - id "org.jetbrains.kotlin.plugin.serialization" -} - -apply from: "$mppJavaProjectPresetPath" - -kotlin { - sourceSets { - commonMain { - dependencies { - api "dev.inmo:micro_utils.ktor.server:$microutils_version" - - api project(":postssystem.publishing.ktor.common") - } - } - - jvmTest { - dependencies { - implementation "org.xerial:sqlite-jdbc:$test_sqlite_version" - implementation "org.jetbrains.kotlin:kotlin-test" - implementation "org.jetbrains.kotlin:kotlin-test-junit" - } - } - } -} diff --git a/publishing/ktor/server/src/jvmMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/server/publishing_keys_repo/PublishingKeysRepoRoutingConfigurator.kt b/publishing/ktor/server/src/jvmMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/server/publishing_keys_repo/PublishingKeysRepoRoutingConfigurator.kt deleted file mode 100644 index 3202c314..00000000 --- a/publishing/ktor/server/src/jvmMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/server/publishing_keys_repo/PublishingKeysRepoRoutingConfigurator.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.insanusmokrassar.postssystem.publishing.ktor.server.publishing_keys_repo - -import dev.inmo.postssystem.core.publishing.repos.PublishingKeysRepo -import dev.inmo.postssystem.publishing.ktor.publishingKeysRootRoute -import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator -import io.ktor.routing.Route -import io.ktor.routing.route - -private inline fun configurator(proxyTo: PublishingKeysRepo): Route.() -> Unit = { - configureReadPublishingKeysRepoRoutes(proxyTo) - configureWritePublishingKeysRepoRoutes(proxyTo) -} - -fun Route.configurePublishingKeysRepoRoutes ( - proxyTo: PublishingKeysRepo, - rootRoute: String? = publishingKeysRootRoute -) { - rootRoute ?.also { - route(it, configurator(proxyTo)) - } ?: configurator(proxyTo).invoke(this) -} - -class PublishingKeysRepoRoutingConfigurator ( - private val proxyTo: PublishingKeysRepo, - private val rootRoute: String? = publishingKeysRootRoute -) : ApplicationRoutingConfigurator.Element { - override fun Route.invoke() { - configurePublishingKeysRepoRoutes(proxyTo, rootRoute) - } -} \ No newline at end of file diff --git a/publishing/ktor/server/src/jvmMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/server/publishing_keys_repo/ReadPublishingKeysRepoRoutingConfigurator.kt b/publishing/ktor/server/src/jvmMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/server/publishing_keys_repo/ReadPublishingKeysRepoRoutingConfigurator.kt deleted file mode 100644 index 5f90578a..00000000 --- a/publishing/ktor/server/src/jvmMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/server/publishing_keys_repo/ReadPublishingKeysRepoRoutingConfigurator.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.insanusmokrassar.postssystem.publishing.ktor.server.publishing_keys_repo - -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.publishing.TriggerControlKey -import dev.inmo.postssystem.core.publishing.repos.ReadPublishingKeysRepo -import dev.inmo.postssystem.publishing.ktor.getPostIdByTriggerControlKeyRoute -import dev.inmo.postssystem.publishing.ktor.getTriggerControlKeyByPostIdRoute -import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator -import dev.inmo.micro_utils.ktor.server.getParameterOrSendError -import dev.inmo.micro_utils.ktor.server.unianswer -import io.ktor.application.call -import io.ktor.routing.Route -import io.ktor.routing.get -import kotlinx.serialization.builtins.nullable -import kotlinx.serialization.builtins.serializer - -fun Route.configureReadPublishingKeysRepoRoutes ( - proxyTo: ReadPublishingKeysRepo -) { - get("$getPostIdByTriggerControlKeyRoute/{key}") { - val key: TriggerControlKey = call.getParameterOrSendError("key") ?: return@get - - call.unianswer( - PostId.serializer().nullable, - proxyTo.getPostIdByTriggerControlKey(key) - ) - } - - get("$getTriggerControlKeyByPostIdRoute/{postId}") { - val postId: PostId = call.getParameterOrSendError("postId") ?: return@get - - call.unianswer( - TriggerControlKey.serializer().nullable, - proxyTo.getTriggerControlKeyByPostId(postId) - ) - } -} - -class ReadPublishingKeysRepoRoutingConfigurator ( - private val proxyTo: ReadPublishingKeysRepo -) : ApplicationRoutingConfigurator.Element { - override fun Route.invoke() { - configureReadPublishingKeysRepoRoutes(proxyTo) - } -} \ No newline at end of file diff --git a/publishing/ktor/server/src/jvmMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/server/publishing_keys_repo/WritePublishingKeysRepoRoutingConfigurator.kt b/publishing/ktor/server/src/jvmMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/server/publishing_keys_repo/WritePublishingKeysRepoRoutingConfigurator.kt deleted file mode 100644 index b154ee88..00000000 --- a/publishing/ktor/server/src/jvmMain/kotlin/com/insanusmokrassar/postssystem/publishing/ktor/server/publishing_keys_repo/WritePublishingKeysRepoRoutingConfigurator.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.insanusmokrassar.postssystem.publishing.ktor.server.publishing_keys_repo - -import dev.inmo.postssystem.core.post.PostId -import dev.inmo.postssystem.core.publishing.repos.WritePublishingKeysRepo -import dev.inmo.postssystem.publishing.ktor.* -import dev.inmo.micro_utils.ktor.server.unianswer -import dev.inmo.micro_utils.ktor.server.uniload -import io.ktor.application.call -import io.ktor.routing.Route -import io.ktor.routing.post -import kotlinx.serialization.builtins.serializer - -fun Route.configureWritePublishingKeysRepoRoutes ( - proxyTo: WritePublishingKeysRepo -) { - post(setPostTriggerControlKeyRoute) { - val obj = call.uniload(SetPostTriggerControlKeyObject.serializer()) - - call.unianswer( - Boolean.serializer(), - proxyTo.setPostTriggerControlKey(obj.postId, obj.key) - ) - } - - post(unsetPostTriggerControlKeyRoute) { - val postId = call.uniload(PostId.serializer()) - - call.unianswer( - Boolean.serializer(), - proxyTo.unsetPostTriggerControlKey(postId) - ) - } -} - -class WritePublishingKeysRepoRoutingConfigurator ( - private val proxyTo: WritePublishingKeysRepo -) \ No newline at end of file diff --git a/server/Makefile b/server/Makefile new file mode 100644 index 00000000..ae84f56b --- /dev/null +++ b/server/Makefile @@ -0,0 +1,21 @@ +#!make +# include .env +export $(shell sed 's/=.*//' .env) + +.ONESHELL: +.PHONY: clean build upPostgres composeBuild runExample addTestUserAndAuth startTestPostgres + +clean: + ../gradlew clean + +build: + ../gradlew build distTar + +startTestServer: + ../gradlew run --args="test.config.json" + +startTestPostgres: + sudo docker-compose up + +addTestUserAndAuth: + docker-compose exec test_postgres psql test -U test -c "INSERT INTO Users VALUES (-1, 'test', 'test', 'test');INSERT INTO UsersAuthentications VALUES ('test', -1);" diff --git a/server/build.gradle b/server/build.gradle new file mode 100644 index 00000000..51b4017e --- /dev/null +++ b/server/build.gradle @@ -0,0 +1,25 @@ +plugins { + id "org.jetbrains.kotlin.jvm" + id "org.jetbrains.kotlin.plugin.serialization" + id "application" +} + +version = null + +application { + mainClass = 'dev.inmo.postssystem.server.EntranceKt' +} + +dependencies { + api project(":postssystem.features.common.server") + api project(":postssystem.features.status.server") + api project(":postssystem.features.files.server") + api project(":postssystem.features.users.server") + api project(":postssystem.features.auth.server") + api project(":postssystem.features.roles.server") + api project(":postssystem.features.roles.manager.server") + api "io.ktor:ktor-server-netty:$ktor_version" + api "io.ktor:ktor-websockets:$ktor_version" + api "org.jetbrains.exposed:exposed-jdbc:$kotlin_exposed_version" + api "org.postgresql:postgresql:$psql_version" +} diff --git a/server/docker-compose.yml b/server/docker-compose.yml new file mode 100644 index 00000000..179d9baa --- /dev/null +++ b/server/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.4" + +services: + test_postgres: + image: postgres + environment: + POSTGRES_USER: "test" + POSTGRES_PASSWORD: "test" + POSTGRES_DB: "test" + ports: + - "8090:5432" diff --git a/server/src/main/java/dev/inmo/postssystem/server/AuthConfig.kt b/server/src/main/java/dev/inmo/postssystem/server/AuthConfig.kt new file mode 100644 index 00000000..3d299bde --- /dev/null +++ b/server/src/main/java/dev/inmo/postssystem/server/AuthConfig.kt @@ -0,0 +1,10 @@ +package dev.inmo.postssystem.server + +import dev.inmo.postssystem.features.common.common.Milliseconds +import kotlinx.serialization.Serializable +import java.util.concurrent.TimeUnit + +@Serializable +data class AuthConfig( + val sessionAge: Milliseconds = TimeUnit.DAYS.toMillis(1) +) diff --git a/server/src/main/java/dev/inmo/postssystem/server/ClientStaticRoutingConfiguration.kt b/server/src/main/java/dev/inmo/postssystem/server/ClientStaticRoutingConfiguration.kt new file mode 100644 index 00000000..3d24d81c --- /dev/null +++ b/server/src/main/java/dev/inmo/postssystem/server/ClientStaticRoutingConfiguration.kt @@ -0,0 +1,29 @@ +package dev.inmo.postssystem.server + +import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator +import io.ktor.application.call +import io.ktor.http.content.files +import io.ktor.http.content.static +import io.ktor.response.respondRedirect +import io.ktor.routing.Route +import io.ktor.routing.get +import java.io.File + +class ClientStaticRoutingConfiguration( + clientStatic: String? +) : ApplicationRoutingConfigurator.Element { + private val staticFile = clientStatic ?.let { File(clientStatic).takeIf { it.exists() } } + override fun Route.invoke() { + staticFile ?.let { + static("client") { + files(it) + get { + call.respondRedirect("client/index.html") + } + } + get { + call.respondRedirect("client") + } + } + } +} diff --git a/server/src/main/java/dev/inmo/postssystem/server/Config.kt b/server/src/main/java/dev/inmo/postssystem/server/Config.kt new file mode 100644 index 00000000..1bed1c08 --- /dev/null +++ b/server/src/main/java/dev/inmo/postssystem/server/Config.kt @@ -0,0 +1,21 @@ +package dev.inmo.postssystem.server + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.io.File + +@Serializable +data class Config( + val host: String = "0.0.0.0", + val port: Int = 8080, + @SerialName("database") + val databaseConfig: DatabaseConfig = DatabaseConfig(), + @SerialName("auth") + val authConfig: AuthConfig = AuthConfig(), + val clientStatic: String? = null, + val filesFolder: String, + val debugMode: Boolean = false +) { + val filesFolderFile: File + get() = File(filesFolder) +} diff --git a/server/src/main/java/dev/inmo/postssystem/server/DI.kt b/server/src/main/java/dev/inmo/postssystem/server/DI.kt new file mode 100644 index 00000000..3cc506c3 --- /dev/null +++ b/server/src/main/java/dev/inmo/postssystem/server/DI.kt @@ -0,0 +1,169 @@ +package dev.inmo.postssystem.server + +import dev.inmo.postssystem.features.auth.server.AuthenticationRoutingConfigurator +import dev.inmo.postssystem.features.auth.server.SessionAuthenticationConfigurator +import dev.inmo.postssystem.features.auth.server.tokens.* +import dev.inmo.postssystem.features.common.common.getAllDistinct +import dev.inmo.postssystem.features.common.common.singleWithBinds +import dev.inmo.postssystem.features.common.server.sessions.ApplicationAuthenticationConfigurator +import dev.inmo.postssystem.features.files.common.* +import dev.inmo.postssystem.features.files.common.storage.* +import dev.inmo.postssystem.features.files.common.storage.WriteFilesStorage +import dev.inmo.postssystem.features.files.server.FilesRoutingConfigurator +import dev.inmo.postssystem.features.roles.common.* +import dev.inmo.postssystem.features.roles.common.keyvalue.KeyValuesUsersRolesOriginalRepo +import dev.inmo.postssystem.features.roles.manager.common.RolesManagerRole +import dev.inmo.postssystem.features.roles.manager.common.RolesManagerRoleStorage +import dev.inmo.postssystem.features.roles.manager.server.RolesManagerRolesChecker +import dev.inmo.postssystem.features.roles.manager.server.RolesManagerUsersRolesStorageServerRoutesConfigurator +import dev.inmo.postssystem.features.roles.server.* +import dev.inmo.postssystem.features.status.server.StatusRoutingConfigurator +import dev.inmo.postssystem.features.users.common.ExposedUsersStorage +import dev.inmo.postssystem.features.users.server.UsersStorageServerRoutesConfigurator +import dev.inmo.micro_utils.coroutines.LinkedSupervisorScope +import dev.inmo.micro_utils.ktor.server.configurators.* +import dev.inmo.micro_utils.ktor.server.createKtorServer +import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo +import dev.inmo.micro_utils.repos.exposed.onetomany.ExposedOneToManyKeyValueRepo +import io.ktor.application.featureOrNull +import io.ktor.application.log +import io.ktor.routing.Route +import io.ktor.routing.Routing +import io.ktor.server.engine.ApplicationEngine +import io.ktor.server.netty.Netty +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.serialization.StringFormat +import kotlinx.serialization.json.Json +import org.jetbrains.exposed.sql.Database +import org.koin.core.module.Module +import org.koin.core.parameter.ParametersHolder +import org.koin.core.qualifier.StringQualifier +import org.koin.dsl.* +import java.io.File + +private fun Route.print() { + println(this) + children.forEach { + it.print() + } +} + + +fun getDIModule( + vararg args: String, + baseScope: CoroutineScope = CoroutineScope(Dispatchers.IO) +): Module { + val json = Json { + ignoreUnknownKeys = true + } + val config = json.decodeFromString(Config.serializer(), File(args.first()).readText()) + + val originalFilesMetasKeyValueRepoQualifier = StringQualifier("OriginalFilesMetaKV") + val filesMetasKeyValueRepoQualifier = StringQualifier("FilesMetaKV") + val filesFolderQualifier = StringQualifier("filesFolder") + val reviewVersionsQualifier = StringQualifier("reviewVersions") + val releaseVersionsQualifier = StringQualifier("releaseVersions") + val usersRolesKeyValueFactoryQualifier = StringQualifier("usersRolesKeyValueFactory") + + return module { + single { json } + single { get() } + singleWithBinds { config } + singleWithBinds { get().databaseConfig } + singleWithBinds { get().authConfig } + singleWithBinds(filesFolderQualifier) { get().filesFolderFile } + + singleWithBinds { get().database } + singleWithBinds(originalFilesMetasKeyValueRepoQualifier) { + ExposedKeyValueRepo(get(), { text("fileid") }, { text("metaInfo") }, "FileIdsToMetas") + } + singleWithBinds(filesMetasKeyValueRepoQualifier) { + MetasKeyValueRepo( + get(), + get(originalFilesMetasKeyValueRepoQualifier) + ) + } + single { DiskFilesStorage(get(filesFolderQualifier), get(filesMetasKeyValueRepoQualifier)) } + single { WriteDistFilesStorage(get(filesFolderQualifier), get(filesMetasKeyValueRepoQualifier)) } + single { DefaultFullFilesStorage(get(), get()) } + singleWithBinds { ExposedUsersStorage(get()) } + singleWithBinds { exposedUsersAuthenticator(get(), get()) } + + factory(usersRolesKeyValueFactoryQualifier) { (tableName: String) -> + ExposedOneToManyKeyValueRepo(get(), { long("userId") }, { text("role") }, tableName) + } + single { + RolesManagerRoleStorage(get(usersRolesKeyValueFactoryQualifier) { ParametersHolder(mutableListOf("rolesManager")) }) + } + single>(StringQualifier("RolesManagerRoleStorage")) { get() } + singleWithBinds { + UsersRolesStorageHolder( + RolesManagerRole::class, + get() + ) + } + singleWithBinds> { UsersRolesAggregator(getAll()) } + + // Roles checkers + single>(StringQualifier(RolesManagerRolesChecker.key)) { RolesManagerRolesChecker } + + factory { baseScope.LinkedSupervisorScope() } + + // Routing configurators + singleWithBinds { FilesRoutingConfigurator(get(), null) } + singleWithBinds { StatusRoutingConfigurator } + singleWithBinds { UsersStorageServerRoutesConfigurator(get()) } + singleWithBinds { UsersRolesStorageReadServerRoutesConfigurator(get(), UserRoleSerializer) } + singleWithBinds { RolesManagerUsersRolesStorageServerRoutesConfigurator(get()) } + + singleWithBinds { ClientStaticRoutingConfiguration(get().clientStatic) } + singleWithBinds { + UsersRolesAuthenticationConfigurator( + get(), + get(), + getAll() + ) + } + + // Session and auth configurators + singleWithBinds { SessionAuthenticationConfigurator(get().sessionAge) } + singleWithBinds { + DefaultAuthTokensService( + get(), + get(), + get(), + get().sessionAge, + get() + ) + } + singleWithBinds { AuthenticationRoutingConfigurator(get(), get()) } + + if (config.debugMode) { + single(StringQualifier("Tracer")) { ApplicationRoutingConfigurator.Element {(this as Routing).trace { application.log.trace(it.buildText()) } } } + } + + singleWithBinds { ApplicationRoutingConfigurator(getAllDistinct()) } + singleWithBinds { ApplicationCachingHeadersConfigurator(getAllDistinct()) } + singleWithBinds { ApplicationSessionsConfigurator(getAllDistinct()) } + singleWithBinds { StatusPagesConfigurator(getAllDistinct()) } + singleWithBinds { ApplicationAuthenticationConfigurator(getAllDistinct()) } + singleWithBinds { WebSocketsConfigurator } + + factory { + val config = get() + createKtorServer( + Netty, + config.host, + config.port + ) { + getAllDistinct().also(::println).forEach { + it.apply { configure() } + } + if (config.debugMode) { + featureOrNull(Routing) ?.print() + } + } + } + } +} diff --git a/server/src/main/java/dev/inmo/postssystem/server/DatabaseConfig.kt b/server/src/main/java/dev/inmo/postssystem/server/DatabaseConfig.kt new file mode 100644 index 00000000..143c3529 --- /dev/null +++ b/server/src/main/java/dev/inmo/postssystem/server/DatabaseConfig.kt @@ -0,0 +1,26 @@ +package dev.inmo.postssystem.server + +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import org.jetbrains.exposed.sql.Database + +const val defaultDatabaseParamsName = "defaultDatabase" +inline val Map.database: Database? + get() = (get(defaultDatabaseParamsName) as? DatabaseConfig) ?.database + +@Serializable +data class DatabaseConfig( + val url: String = "jdbc:postgresql://localhost:5432/tablet", + val driver: String = org.postgresql.Driver::class.qualifiedName!!, + val username: String = "", + val password: String = "" +) { + @Transient + val database: Database = Database.connect( + url, + driver, + username, + password + ) +} + diff --git a/server/src/main/java/dev/inmo/postssystem/server/Entrance.kt b/server/src/main/java/dev/inmo/postssystem/server/Entrance.kt new file mode 100644 index 00000000..399641e1 --- /dev/null +++ b/server/src/main/java/dev/inmo/postssystem/server/Entrance.kt @@ -0,0 +1,10 @@ +package dev.inmo.postssystem.server + +import io.ktor.server.engine.ApplicationEngine +import org.koin.core.context.startKoin + +fun main(vararg args: String) { + startKoin { + modules(getDIModule(*args)) + }.koin.get().start(wait = true) +} diff --git a/server/src/main/java/dev/inmo/postssystem/server/WebSocketsConfigurator.kt b/server/src/main/java/dev/inmo/postssystem/server/WebSocketsConfigurator.kt new file mode 100644 index 00000000..aa2e270b --- /dev/null +++ b/server/src/main/java/dev/inmo/postssystem/server/WebSocketsConfigurator.kt @@ -0,0 +1,12 @@ +package dev.inmo.postssystem.server + +import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator +import io.ktor.application.Application +import io.ktor.application.install +import io.ktor.websocket.WebSockets + +object WebSocketsConfigurator : KtorApplicationConfigurator { + override fun Application.configure() { + install(WebSockets) + } +} diff --git a/server/test.config.json b/server/test.config.json new file mode 100644 index 00000000..e046a63c --- /dev/null +++ b/server/test.config.json @@ -0,0 +1,10 @@ +{ + "database": { + "url": "jdbc:postgresql://127.0.0.1:8090/test", + "username": "test", + "password": "test" + }, + "clientStatic": "../client/build/distributions", + "filesFolder": "/tmp/files", + "debugMode": true +} diff --git a/settings.gradle b/settings.gradle index bf0b490e..fa6429f7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,21 +1,36 @@ -rootProject.name='postssystem' +rootProject.name = 'postssystem' String[] includes = [ - ':core:api', - ':core:exposed', - ':core:ktor:common', - ':core:ktor:client', - ':core:ktor:server', + ":features:common:common", + ":features:common:client", + ":features:common:server", - ':publishing:api', - ':publishing:exposed', - ':publishing:ktor:common', - ':publishing:ktor:client', - ':publishing:ktor:server', + ":features:files:common", + ":features:files:client", + ":features:files:server", - ':business_cases:post_creating:server', - ':business_cases:post_creating:common', - ':business_cases:post_creating:client', + ":features:status:common", + ":features:status:client", + ":features:status:server", + + ":features:users:common", + ":features:users:client", + ":features:users:server", + + ":features:roles:common", + ":features:roles:client", + ":features:roles:server", + + ":features:roles:manager:common", + ":features:roles:manager:client", + ":features:roles:manager:server", + + ":features:auth:common", + ":features:auth:client", + ":features:auth:server", + + ":server", + ":client", ]