feat: initial commit 🎉
implements a basic styled app with totp login
|
|
@ -0,0 +1,15 @@
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="11" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetDropDown">
|
||||||
|
<targetSelectedWithDropDown>
|
||||||
|
<Target>
|
||||||
|
<type value="QUICK_BOOT_TARGET" />
|
||||||
|
<deviceKey>
|
||||||
|
<Key>
|
||||||
|
<type value="VIRTUAL_DEVICE_PATH" />
|
||||||
|
<value value="C:\Users\sp46a\.android\avd\Pixel_2_API_23.avd" />
|
||||||
|
</Key>
|
||||||
|
</deviceKey>
|
||||||
|
</Target>
|
||||||
|
</targetSelectedWithDropDown>
|
||||||
|
<timeTargetWasSelectedWithDropDown value="2022-11-28T15:52:35.350851100Z" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="testRunner" value="GRADLE" />
|
||||||
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleHome" value="$PROJECT_DIR$/../../../../Gradle" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DesignSurface">
|
||||||
|
<option name="filePathToZoomLevelMap">
|
||||||
|
<map>
|
||||||
|
<entry key="..\:/Users/sp46a/AndroidStudioProjects/Revolt/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" value="0.102" />
|
||||||
|
<entry key="..\:/Users/sp46a/AndroidStudioProjects/Revolt/app/src/main/res/drawable/ic_launcher_background.xml" value="0.102" />
|
||||||
|
<entry key="..\:/Users/sp46a/AndroidStudioProjects/Revolt/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" value="0.14" />
|
||||||
|
<entry key="..\:/Users/sp46a/AndroidStudioProjects/Revolt/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml" value="0.14" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
plugins {
|
||||||
|
id 'com.android.application'
|
||||||
|
id 'org.jetbrains.kotlin.android'
|
||||||
|
id 'org.jetbrains.kotlin.plugin.serialization'
|
||||||
|
id 'com.mikepenz.aboutlibraries.plugin'
|
||||||
|
id 'com.google.dagger.hilt.android'
|
||||||
|
id 'kotlin-kapt'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk 33
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "chat.revolt"
|
||||||
|
minSdk 23
|
||||||
|
targetSdk 33
|
||||||
|
versionCode Integer.parseInt("000_001_000".replaceAll("_", ""), 10)
|
||||||
|
versionName "0.1.0"
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
vectorDrawables {
|
||||||
|
useSupportLibrary true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
compose true
|
||||||
|
}
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion compose_version
|
||||||
|
}
|
||||||
|
packagingOptions {
|
||||||
|
resources {
|
||||||
|
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
namespace 'chat.revolt'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Android/Kotlin Core
|
||||||
|
implementation 'androidx.core:core-ktx:1.9.0'
|
||||||
|
|
||||||
|
// JSON Serialization
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
|
||||||
|
|
||||||
|
// Jetpack Compose
|
||||||
|
implementation "androidx.compose.ui:ui:$compose_libraries_version"
|
||||||
|
implementation 'androidx.compose.material3:material3:1.0.1'
|
||||||
|
implementation "androidx.compose.ui:ui-tooling-preview:$compose_libraries_version"
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
|
||||||
|
implementation 'androidx.activity:activity-compose:1.6.1'
|
||||||
|
|
||||||
|
// Accompanist - Jetpack Compose Extensions
|
||||||
|
implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version"
|
||||||
|
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"
|
||||||
|
|
||||||
|
// KTOR - HTTP+WebSocket Library
|
||||||
|
implementation "io.ktor:ktor-client-core:$ktor_version"
|
||||||
|
implementation "io.ktor:ktor-client-okhttp:$ktor_version"
|
||||||
|
implementation "io.ktor:ktor-client-logging:$ktor_version"
|
||||||
|
implementation "io.ktor:ktor-client-content-negotiation:$ktor_version"
|
||||||
|
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
|
||||||
|
|
||||||
|
// Screen Navigation
|
||||||
|
implementation "androidx.navigation:navigation-compose:$nav_version"
|
||||||
|
|
||||||
|
// Jetpack Compose Tooling
|
||||||
|
debugImplementation "androidx.compose.ui:ui-tooling:$compose_libraries_version"
|
||||||
|
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_libraries_version"
|
||||||
|
|
||||||
|
// Browser opening utility (used for legal links)
|
||||||
|
implementation "androidx.browser:browser:1.4.0"
|
||||||
|
|
||||||
|
// Hilt - Dependency Injection
|
||||||
|
implementation "com.google.dagger:hilt-android:$hilt_version"
|
||||||
|
kapt "com.google.dagger:hilt-compiler:$hilt_version"
|
||||||
|
|
||||||
|
// Coil - Image Loading
|
||||||
|
implementation "io.coil-kt:coil:$coil_version"
|
||||||
|
implementation "io.coil-kt:coil-compose:$coil_version"
|
||||||
|
implementation "io.coil-kt:coil-svg:$coil_version"
|
||||||
|
implementation "io.coil-kt:coil-gif:$coil_version"
|
||||||
|
|
||||||
|
// AboutLibraries - automated OSS library attribution
|
||||||
|
implementation "com.mikepenz:aboutlibraries-compose:$aboutlibraries_version"
|
||||||
|
|
||||||
|
// Jetpack DataStore - persistent storage
|
||||||
|
implementation "androidx.datastore:datastore-preferences:1.1.0-alpha01"
|
||||||
|
implementation "androidx.datastore:datastore:1.1.0-alpha01"
|
||||||
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
correctErrorTypes true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
|
# Keep `Companion` object fields of serializable classes.
|
||||||
|
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
|
||||||
|
-if @kotlinx.serialization.Serializable class **
|
||||||
|
-keepclassmembers class <1> {
|
||||||
|
static <1>$Companion Companion;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep `serializer()` on companion objects (both default and named) of serializable classes.
|
||||||
|
-if @kotlinx.serialization.Serializable class ** {
|
||||||
|
static **$* *;
|
||||||
|
}
|
||||||
|
-keepclassmembers class <2>$<3> {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep `INSTANCE.serializer()` of serializable objects.
|
||||||
|
-if @kotlinx.serialization.Serializable class ** {
|
||||||
|
public static ** INSTANCE;
|
||||||
|
}
|
||||||
|
-keepclassmembers class <1> {
|
||||||
|
public static <1> INSTANCE;
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
|
||||||
|
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
||||||
|
|
||||||
|
# Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`.
|
||||||
|
# If you have any, uncomment and replace classes with those containing named companion objects.
|
||||||
|
#-keepattributes InnerClasses # Needed for `getDeclaredClasses`.
|
||||||
|
#-if @kotlinx.serialization.Serializable class
|
||||||
|
#com.example.myapplication.HasNamedCompanion, # <-- List serializable classes with named companions.
|
||||||
|
#com.example.myapplication.HasNamedCompanion2
|
||||||
|
#{
|
||||||
|
# static **$* *;
|
||||||
|
#}
|
||||||
|
#-keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept.
|
||||||
|
# static <1>$$serializer INSTANCE;
|
||||||
|
#}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:name=".RevoltApplication"
|
||||||
|
android:theme="@style/Theme.Revolt"
|
||||||
|
tools:targetApi="31">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/Theme.Revolt">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package chat.revolt
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import chat.revolt.screens.about.AboutScreen
|
||||||
|
import chat.revolt.screens.about.AttributionScreen
|
||||||
|
import chat.revolt.screens.about.PlaceholderScreen
|
||||||
|
import chat.revolt.screens.chat.HomeScreen
|
||||||
|
import chat.revolt.screens.login.GreeterScreen
|
||||||
|
import chat.revolt.screens.login.LoginScreen
|
||||||
|
import chat.revolt.screens.login.MfaScreen
|
||||||
|
import chat.revolt.ui.theme.RevoltTheme
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContent {
|
||||||
|
RevoltTheme {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
color = MaterialTheme.colorScheme.background
|
||||||
|
) {
|
||||||
|
AppEntrypoint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppEntrypoint() {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = "setup/greeting"
|
||||||
|
) {
|
||||||
|
composable("setup/greeting") { GreeterScreen(navController) }
|
||||||
|
composable("setup/login") { LoginScreen(navController) }
|
||||||
|
composable("setup/mfa/{mfaTicket}/{allowedAuthTypes}") { backStackEntry ->
|
||||||
|
val mfaTicket = backStackEntry.arguments?.getString("mfaTicket") ?: ""
|
||||||
|
val allowedAuthTypes =
|
||||||
|
backStackEntry.arguments?.getString("allowedAuthTypes") ?: ""
|
||||||
|
|
||||||
|
MfaScreen(navController, allowedAuthTypes, mfaTicket)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable("chat/home") { HomeScreen(navController) }
|
||||||
|
|
||||||
|
composable("about") { AboutScreen(navController) }
|
||||||
|
composable("about/oss") { AttributionScreen(navController) }
|
||||||
|
composable("about/placeholder") { PlaceholderScreen(navController) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package chat.revolt
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
|
||||||
|
@HiltAndroidApp
|
||||||
|
class RevoltApplication : Application() {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
package chat.revolt.api
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import chat.revolt.api.routes.user.fetchSelf
|
||||||
|
import chat.revolt.api.schemas.CompleteUser
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.engine.okhttp.*
|
||||||
|
import io.ktor.client.plugins.*
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.client.plugins.logging.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
const val REVOLT_BASE = "https://api.revolt.chat"
|
||||||
|
const val REVOLT_SUPPORT = "https://support.revolt.chat"
|
||||||
|
const val REVOLT_MARKETING = "https://revolt.chat"
|
||||||
|
const val REVOLT_FILES = "https://autumn.revolt.chat"
|
||||||
|
|
||||||
|
private const val BACKEND_IS_STABLE = false
|
||||||
|
|
||||||
|
val Context.revoltKVStorage: DataStore<Preferences> by preferencesDataStore(name = "revolt_kv")
|
||||||
|
|
||||||
|
val RevoltJson = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
val RevoltHttp = HttpClient(OkHttp) {
|
||||||
|
install(DefaultRequest)
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(RevoltJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BACKEND_IS_STABLE) {
|
||||||
|
install(HttpRequestRetry) {
|
||||||
|
retryOnServerErrors(maxRetries = 5)
|
||||||
|
retryOnException(maxRetries = 5)
|
||||||
|
|
||||||
|
modifyRequest { request ->
|
||||||
|
request.headers.append("x-retry-count", retryCount.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
exponentialDelay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
install(Logging) { level = LogLevel.INFO }
|
||||||
|
|
||||||
|
defaultRequest {
|
||||||
|
url(REVOLT_BASE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object RevoltAPI {
|
||||||
|
const val TOKEN_HEADER_NAME = "x-session-token"
|
||||||
|
|
||||||
|
val userCache = mutableMapOf<String, CompleteUser>()
|
||||||
|
|
||||||
|
var selfId: String? = null
|
||||||
|
|
||||||
|
var sessionToken: String = ""
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun setSessionHeader(token: String) {
|
||||||
|
sessionToken = token
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun initialize() {
|
||||||
|
if (sessionToken != "") {
|
||||||
|
fetchSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isLoggedIn(): Boolean {
|
||||||
|
return selfId != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
data class RevoltError(val type: String)
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
package chat.revolt.api.routes.account
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import chat.revolt.api.RevoltError
|
||||||
|
import chat.revolt.api.RevoltHttp
|
||||||
|
import chat.revolt.api.RevoltJson
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LoginNegotiation(
|
||||||
|
val email: String,
|
||||||
|
val password: String,
|
||||||
|
|
||||||
|
@SerialName("friendly_name")
|
||||||
|
val friendlyName: String,
|
||||||
|
val captcha: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LoginMfaAmendmentTotpCode(
|
||||||
|
@SerialName("mfa_ticket")
|
||||||
|
val mfaTicket: String,
|
||||||
|
|
||||||
|
@SerialName("mfa_response")
|
||||||
|
val mfaResponse: MfaResponseTotpCode,
|
||||||
|
|
||||||
|
@SerialName("friendly_name")
|
||||||
|
val friendlyName: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LoginMfaAmendmentRecoveryCode(
|
||||||
|
@SerialName("mfa_ticket")
|
||||||
|
val mfaTicket: String,
|
||||||
|
|
||||||
|
@SerialName("mfa_response")
|
||||||
|
val mfaResponse: MfaResponseRecoveryCode,
|
||||||
|
|
||||||
|
@SerialName("friendly_name")
|
||||||
|
val friendlyName: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MfaResponseRecoveryCode(
|
||||||
|
@SerialName("recovery_code")
|
||||||
|
val recoveryCode: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MfaResponseTotpCode(
|
||||||
|
@SerialName("totp_code")
|
||||||
|
val totpCode: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MfaLoginSpec(
|
||||||
|
val result: String,
|
||||||
|
val ticket: String,
|
||||||
|
|
||||||
|
@SerialName("allowed_methods")
|
||||||
|
val allowedMethods: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MfaCheck(
|
||||||
|
val result: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class WebPushData(
|
||||||
|
val endpoint: String,
|
||||||
|
|
||||||
|
@SerialName("p256dh")
|
||||||
|
val p256diffieHellman: String,
|
||||||
|
val auth: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UserHints(
|
||||||
|
val result: String,
|
||||||
|
|
||||||
|
@SerialName("_id")
|
||||||
|
val id: String,
|
||||||
|
|
||||||
|
@SerialName("user_id")
|
||||||
|
val userId: String,
|
||||||
|
val token: String,
|
||||||
|
val name: String,
|
||||||
|
val subscription: WebPushData? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class EmailPasswordAssessment(
|
||||||
|
val proceedMfa: Boolean = false,
|
||||||
|
val mfaSpec: MfaLoginSpec? = null,
|
||||||
|
val firstUserHints: UserHints? = null,
|
||||||
|
val error: RevoltError? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun negotiateAuthentication(email: String, password: String): EmailPasswordAssessment {
|
||||||
|
val sessionName = friendlySessionName()
|
||||||
|
|
||||||
|
val response: HttpResponse = RevoltHttp.post("/auth/session/login") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(LoginNegotiation(email, password, sessionName, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseContent = response.bodyAsText()
|
||||||
|
Log.d("Revolt", "negotiateAuthentication: $responseContent")
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
val error = RevoltJson.decodeFromString(RevoltError.serializer(), responseContent)
|
||||||
|
return EmailPasswordAssessment(error = error)
|
||||||
|
} catch (e: SerializationException) {
|
||||||
|
// Not an error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status == HttpStatusCode.InternalServerError) {
|
||||||
|
return EmailPasswordAssessment(
|
||||||
|
error = RevoltError(
|
||||||
|
"InternalServerError",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseJson = RevoltJson.decodeFromString(MfaCheck.serializer(), responseContent)
|
||||||
|
|
||||||
|
return when (responseJson.result) {
|
||||||
|
"Success" -> EmailPasswordAssessment(
|
||||||
|
firstUserHints = RevoltJson.decodeFromString(UserHints.serializer(), responseContent)
|
||||||
|
)
|
||||||
|
"MFA" -> EmailPasswordAssessment(
|
||||||
|
proceedMfa = true,
|
||||||
|
mfaSpec = RevoltJson.decodeFromString(MfaLoginSpec.serializer(), responseContent)
|
||||||
|
)
|
||||||
|
else -> throw Exception("Unknown result: ${responseJson.result}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun authenticateWithMfaTotpCode(
|
||||||
|
mfaTicket: String,
|
||||||
|
mfaResponse: MfaResponseTotpCode,
|
||||||
|
): EmailPasswordAssessment {
|
||||||
|
val response: HttpResponse = RevoltHttp.post("/auth/session/login") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(LoginMfaAmendmentTotpCode(mfaTicket, mfaResponse, friendlySessionName()))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val error = RevoltJson.decodeFromString(RevoltError.serializer(), response.bodyAsText())
|
||||||
|
return EmailPasswordAssessment(error = error)
|
||||||
|
} catch (e: SerializationException) {
|
||||||
|
// Not an error
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseContent = response.bodyAsText()
|
||||||
|
Log.d("Revolt", "authenticateWithMfaTotpCode: $responseContent")
|
||||||
|
|
||||||
|
return EmailPasswordAssessment(
|
||||||
|
firstUserHints = RevoltJson.decodeFromString(UserHints.serializer(), responseContent)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun authenticateWithMfaRecoveryCode(
|
||||||
|
mfaTicket: String,
|
||||||
|
mfaResponse: MfaResponseRecoveryCode,
|
||||||
|
): EmailPasswordAssessment {
|
||||||
|
val response: HttpResponse = RevoltHttp.post("/auth/session/login") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(LoginMfaAmendmentRecoveryCode(mfaTicket, mfaResponse, friendlySessionName()))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val error = RevoltJson.decodeFromString(RevoltError.serializer(), response.bodyAsText())
|
||||||
|
return EmailPasswordAssessment(error = error)
|
||||||
|
} catch (e: SerializationException) {
|
||||||
|
// Not an error
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseContent = response.bodyAsText()
|
||||||
|
Log.d("Revolt", "authenticateWithMfaRecoveryCode: $responseContent")
|
||||||
|
|
||||||
|
return EmailPasswordAssessment(
|
||||||
|
firstUserHints = RevoltJson.decodeFromString(UserHints.serializer(), responseContent)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun friendlySessionName(): String {
|
||||||
|
return "Revolt Android on ${Build.MANUFACTURER} ${Build.MODEL}"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package chat.revolt.api.routes.misc
|
||||||
|
|
||||||
|
import chat.revolt.api.RevoltHttp
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Root(
|
||||||
|
val revolt: String,
|
||||||
|
val features: Features,
|
||||||
|
val ws: String,
|
||||||
|
val app: String,
|
||||||
|
val vapid: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Features(
|
||||||
|
val captcha: CAPTCHA,
|
||||||
|
val email: Boolean,
|
||||||
|
|
||||||
|
@SerialName("invite_only")
|
||||||
|
val inviteOnly: Boolean,
|
||||||
|
|
||||||
|
val autumn: Autumn,
|
||||||
|
val january: Autumn,
|
||||||
|
val voso: Voso
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Autumn(
|
||||||
|
val enabled: Boolean,
|
||||||
|
val url: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CAPTCHA(
|
||||||
|
val enabled: Boolean,
|
||||||
|
val key: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Voso(
|
||||||
|
val enabled: Boolean,
|
||||||
|
val url: String,
|
||||||
|
val ws: String
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun getRootRoute(): Root {
|
||||||
|
return RevoltHttp.get("/").body()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package chat.revolt.api.routes.user
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.api.RevoltHttp
|
||||||
|
import chat.revolt.api.RevoltJson
|
||||||
|
import chat.revolt.api.schemas.CompleteUser
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
|
||||||
|
suspend fun fetchSelf(): CompleteUser {
|
||||||
|
Log.d("fetchSelf", "Fetching self and logging object")
|
||||||
|
|
||||||
|
val response = RevoltHttp.get("/users/@me") {
|
||||||
|
headers.append(RevoltAPI.TOKEN_HEADER_NAME, RevoltAPI.sessionToken)
|
||||||
|
}
|
||||||
|
.bodyAsText()
|
||||||
|
|
||||||
|
Log.d("fetchSelf", response)
|
||||||
|
|
||||||
|
val user = RevoltJson.decodeFromString(CompleteUser.serializer(), response)
|
||||||
|
|
||||||
|
RevoltAPI.userCache[user.id!!] = user
|
||||||
|
RevoltAPI.selfId = user.id
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchSelfWithNewToken(token: String): CompleteUser {
|
||||||
|
RevoltAPI.setSessionHeader(token)
|
||||||
|
return fetchSelf()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
package chat.revolt.api.schemas
|
||||||
|
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
import kotlinx.serialization.descriptors.*
|
||||||
|
import kotlinx.serialization.encoding.*
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CompleteUser(
|
||||||
|
@SerialName("_id")
|
||||||
|
val id: String? = null,
|
||||||
|
|
||||||
|
val username: String? = null,
|
||||||
|
val avatar: Avatar? = null,
|
||||||
|
val relations: List<Relation>? = null,
|
||||||
|
val badges: Long? = null,
|
||||||
|
val status: Status? = null,
|
||||||
|
val profile: Profile? = null,
|
||||||
|
val flags: Long? = null,
|
||||||
|
val privileged: Boolean? = null,
|
||||||
|
val bot: Bot? = null,
|
||||||
|
val relationship: String? = null,
|
||||||
|
val online: Boolean? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Avatar(
|
||||||
|
@SerialName("_id")
|
||||||
|
val id: String? = null,
|
||||||
|
|
||||||
|
val tag: String? = null,
|
||||||
|
val filename: String? = null,
|
||||||
|
val metadata: Metadata? = null,
|
||||||
|
|
||||||
|
@SerialName("content_type")
|
||||||
|
val contentType: String? = null,
|
||||||
|
|
||||||
|
val size: Long? = null,
|
||||||
|
val deleted: Boolean? = null,
|
||||||
|
val reported: Boolean? = null,
|
||||||
|
|
||||||
|
@SerialName("message_id")
|
||||||
|
val messageID: String? = null,
|
||||||
|
|
||||||
|
@SerialName("user_id")
|
||||||
|
val userID: String? = null,
|
||||||
|
|
||||||
|
@SerialName("server_id")
|
||||||
|
val serverID: String? = null,
|
||||||
|
|
||||||
|
@SerialName("object_id")
|
||||||
|
val objectID: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Metadata(
|
||||||
|
val type: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Bot(
|
||||||
|
val owner: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Profile(
|
||||||
|
val content: String? = null,
|
||||||
|
val background: Avatar? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Relation(
|
||||||
|
@SerialName("_id")
|
||||||
|
val id: String? = null,
|
||||||
|
|
||||||
|
val status: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Status(
|
||||||
|
val text: String? = null,
|
||||||
|
val presence: String? = null
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package chat.revolt.components.generic
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CollapsibleCard(
|
||||||
|
title: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = modifier.padding(10.dp),
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
) {
|
||||||
|
Column() {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.clickable { expanded = !expanded },
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(5.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Icon(
|
||||||
|
imageVector = if (expanded) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.padding(vertical = 5.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AnimatedVisibility(visible = expanded) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package chat.revolt.components.generic
|
||||||
|
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun FormTextField(
|
||||||
|
value: String,
|
||||||
|
label: String,
|
||||||
|
onChange: (it: String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
password: Boolean = false,
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onChange,
|
||||||
|
singleLine = true,
|
||||||
|
visualTransformation = if (password) PasswordVisualTransformation() else VisualTransformation.None,
|
||||||
|
label = { Text(label) },
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package chat.revolt.components.generic
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import chat.revolt.BuildConfig
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.decode.GifDecoder
|
||||||
|
import coil.decode.ImageDecoderDecoder
|
||||||
|
import coil.decode.SvgDecoder
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RemoteImage(
|
||||||
|
url: String,
|
||||||
|
description: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentScale: ContentScale = ContentScale.Crop
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(url)
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
|
imageLoader = ImageLoader.Builder(LocalContext.current).components {
|
||||||
|
if (Build.VERSION.SDK_INT >= 28) {
|
||||||
|
add(ImageDecoderDecoder.Factory())
|
||||||
|
} else {
|
||||||
|
add(GifDecoder.Factory())
|
||||||
|
}
|
||||||
|
add(SvgDecoder.Factory())
|
||||||
|
}.build(),
|
||||||
|
contentDescription = description,
|
||||||
|
contentScale = contentScale,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun drawableResource(id: Int): String {
|
||||||
|
return "android.resource://" + BuildConfig.APPLICATION_ID + "/" + id
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package chat.revolt.components.generic
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Weblink(text: String, url: String, modifier: Modifier = Modifier) {
|
||||||
|
val ctx = LocalContext.current
|
||||||
|
|
||||||
|
AnyLink(text = text, action = {
|
||||||
|
val customTab = CustomTabsIntent
|
||||||
|
.Builder()
|
||||||
|
.build()
|
||||||
|
customTab.launchUrl(ctx, Uri.parse(url))
|
||||||
|
}, modifier = modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AnyLink(text: String, action: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = Color(0xaaffffff),
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 15.sp
|
||||||
|
),
|
||||||
|
modifier = modifier
|
||||||
|
.padding(horizontal = 2.5.dp, vertical = 3.dp)
|
||||||
|
.clickable(onClick = action)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
package chat.revolt.screens.about
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import chat.revolt.R
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import chat.revolt.BuildConfig
|
||||||
|
import chat.revolt.api.REVOLT_BASE
|
||||||
|
import chat.revolt.api.routes.misc.Root
|
||||||
|
import chat.revolt.api.routes.misc.getRootRoute
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
class AboutViewModel(
|
||||||
|
) : ViewModel() {
|
||||||
|
private val _root = mutableStateOf<Root?>(null)
|
||||||
|
val root: State<Root?>
|
||||||
|
get() = _root
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_root.value = getRootRoute().copy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VersionItem(
|
||||||
|
key: String,
|
||||||
|
value: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(modifier) {
|
||||||
|
Text(
|
||||||
|
text = key,
|
||||||
|
color = Color(0xccffffff),
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 2.5.dp, vertical = 2.5.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
color = Color(0xccffffff),
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Normal
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 2.5.dp, vertical = 2.5.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ComponentVersions(
|
||||||
|
apiVersion: String
|
||||||
|
) {
|
||||||
|
// App Info
|
||||||
|
VersionItem(key = BuildConfig.APPLICATION_ID, value = BuildConfig.VERSION_NAME)
|
||||||
|
|
||||||
|
// API Info
|
||||||
|
VersionItem(key = URI(REVOLT_BASE).host, value = apiVersion)
|
||||||
|
|
||||||
|
// Device Info
|
||||||
|
VersionItem(key = "Runtime SDK", value = Build.VERSION.SDK_INT.toString())
|
||||||
|
VersionItem(
|
||||||
|
key = "Model",
|
||||||
|
value = "${Build.MANUFACTURER} ${
|
||||||
|
Build.DEVICE.replaceFirstChar {
|
||||||
|
if (it.isLowerCase()) it.titlecase() else it.toString()
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AboutScreen(
|
||||||
|
navController: NavController,
|
||||||
|
viewModel: AboutViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
|
||||||
|
if (viewModel.root.value == null) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.loading),
|
||||||
|
color = Color(0xaaffffff),
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Normal
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.about),
|
||||||
|
style = MaterialTheme.typography.displaySmall.copy(
|
||||||
|
fontWeight = FontWeight.Black,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp, vertical = 10.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
ComponentVersions(apiVersion = viewModel.root.value!!.revolt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 30.dp)
|
||||||
|
) {
|
||||||
|
ElevatedButton(
|
||||||
|
onClick = { navController.navigate("about/oss") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = R.string.oss_attribution))
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = { navController.popBackStack() },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = R.string.back))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package chat.revolt.screens.about
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import chat.revolt.R
|
||||||
|
import chat.revolt.ui.theme.DarkColorScheme
|
||||||
|
import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
|
||||||
|
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AttributionScreen(navController: NavController) {
|
||||||
|
Column() {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.oss_attribution),
|
||||||
|
style = MaterialTheme.typography.displaySmall.copy(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Left,
|
||||||
|
fontSize = 24.sp
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 15.dp, vertical = 15.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
LibrariesContainer(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.weight(1f),
|
||||||
|
colors = LibraryDefaults.libraryColors(
|
||||||
|
backgroundColor = DarkColorScheme.background,
|
||||||
|
contentColor = DarkColorScheme.onBackground,
|
||||||
|
badgeBackgroundColor = DarkColorScheme.primary,
|
||||||
|
badgeContentColor = DarkColorScheme.onPrimary
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = { navController.popBackStack() },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 30.dp, top = 5.dp, start = 20.dp, end = 20.dp)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = R.string.back))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package chat.revolt.screens.about
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import chat.revolt.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PlaceholderScreen(
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.comingsoon_heading),
|
||||||
|
style = MaterialTheme.typography.displaySmall.copy(
|
||||||
|
fontSize = 30.sp,
|
||||||
|
fontWeight = FontWeight.Black,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp, vertical = 10.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.comingsoon_body),
|
||||||
|
color = Color(0xaaffffff),
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp, vertical = 10.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 30.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { navController.popBackStack() },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.back))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package chat.revolt.screens.chat
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import chat.revolt.api.REVOLT_FILES
|
||||||
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.components.generic.RemoteImage
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeScreen(navController: NavController) {
|
||||||
|
val user = RevoltAPI.userCache[RevoltAPI.selfId]
|
||||||
|
|
||||||
|
Column() {
|
||||||
|
Text(text = "Logged in as " + user?.username + "!")
|
||||||
|
RemoteImage(
|
||||||
|
url = "$REVOLT_FILES/avatars/${user?.avatar?.id}/user.png",
|
||||||
|
description = "User Avatar",
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(CircleShape)
|
||||||
|
.width(70.dp)
|
||||||
|
.height(70.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
package chat.revolt.screens.login
|
||||||
|
|
||||||
|
import chat.revolt.R
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ElevatedButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.components.generic.RemoteImage
|
||||||
|
import chat.revolt.components.generic.drawableResource
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
class GreeterViewModel() : ViewModel() {
|
||||||
|
private var _skipLogin by mutableStateOf(false)
|
||||||
|
val skipLogin: Boolean
|
||||||
|
get() = _skipLogin
|
||||||
|
|
||||||
|
private var _finishedLoading by mutableStateOf(false)
|
||||||
|
val finishedLoading: Boolean
|
||||||
|
get() = _finishedLoading
|
||||||
|
|
||||||
|
fun setSkipLogin(value: Boolean) {
|
||||||
|
_skipLogin = value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFinishedLoading(value: Boolean) {
|
||||||
|
_finishedLoading = value
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// runBlocking prevents the greeter from showing up at all, if the user is already logged in
|
||||||
|
// (It is normally a bad idea to use runBlocking outside of a coroutine scope)
|
||||||
|
runBlocking {
|
||||||
|
RevoltAPI.initialize()
|
||||||
|
if (RevoltAPI.isLoggedIn()) {
|
||||||
|
_skipLogin = true
|
||||||
|
}
|
||||||
|
setFinishedLoading(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GreeterScreen(navController: NavController, viewModel: GreeterViewModel = viewModel()) {
|
||||||
|
if (viewModel.skipLogin) {
|
||||||
|
navController.navigate("chat/home") {
|
||||||
|
popUpTo("setup/greeting") {
|
||||||
|
inclusive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModel.setSkipLogin(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
RemoteImage(
|
||||||
|
url = drawableResource(R.drawable.revolt_logo_wide),
|
||||||
|
description = "Revolt Logo",
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(60.dp)
|
||||||
|
.padding(bottom = 30.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.login_onboarding_heading),
|
||||||
|
style = MaterialTheme.typography.displaySmall.copy(
|
||||||
|
fontSize = 30.sp,
|
||||||
|
fontWeight = FontWeight.Black,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp, vertical = 10.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.login_onboarding_body),
|
||||||
|
color = Color(0xaaffffff),
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp, vertical = 10.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 30.dp)
|
||||||
|
) {
|
||||||
|
ElevatedButton(
|
||||||
|
onClick = { navController.navigate("about") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.about))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { navController.navigate("setup/login") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.login))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
package chat.revolt.screens.login
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ElevatedButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import chat.revolt.R
|
||||||
|
import chat.revolt.api.REVOLT_SUPPORT
|
||||||
|
import chat.revolt.api.routes.account.EmailPasswordAssessment
|
||||||
|
import chat.revolt.api.routes.account.negotiateAuthentication
|
||||||
|
import chat.revolt.components.generic.AnyLink
|
||||||
|
import chat.revolt.components.generic.FormTextField
|
||||||
|
import chat.revolt.components.generic.Weblink
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class LoginViewModel() : ViewModel() {
|
||||||
|
private var _email by mutableStateOf("")
|
||||||
|
val email: String
|
||||||
|
get() = _email
|
||||||
|
|
||||||
|
private var _password by mutableStateOf("")
|
||||||
|
val password: String
|
||||||
|
get() = _password
|
||||||
|
|
||||||
|
private var _error by mutableStateOf<String?>(null)
|
||||||
|
val error: String?
|
||||||
|
get() = _error
|
||||||
|
|
||||||
|
private var _navigateToMfa by mutableStateOf(false)
|
||||||
|
val navigateToMfa: Boolean
|
||||||
|
get() = _navigateToMfa
|
||||||
|
|
||||||
|
private var _mfaResponse by mutableStateOf<EmailPasswordAssessment?>(null)
|
||||||
|
val mfaResponse: EmailPasswordAssessment?
|
||||||
|
get() = _mfaResponse
|
||||||
|
|
||||||
|
fun doLogin() {
|
||||||
|
_error = null
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = negotiateAuthentication(_email, _password)
|
||||||
|
if (response.error != null) {
|
||||||
|
_error = response.error.type
|
||||||
|
} else {
|
||||||
|
Log.d("Login", "Checking for MFA")
|
||||||
|
if (response.proceedMfa) {
|
||||||
|
Log.d("Login", "MFA required. Navigating to MFA screen")
|
||||||
|
_mfaResponse = response
|
||||||
|
_navigateToMfa = true
|
||||||
|
} else {
|
||||||
|
Log.d(
|
||||||
|
"Login",
|
||||||
|
"No MFA required. Login is complete! We have a session token: ${response.firstUserHints!!.token}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mfaComplete() {
|
||||||
|
_navigateToMfa = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEmail(email: String) {
|
||||||
|
_email = email
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPassword(password: String) {
|
||||||
|
_password = password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginScreen(
|
||||||
|
navController: NavController,
|
||||||
|
viewModel: LoginViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
if (viewModel.navigateToMfa) {
|
||||||
|
navController.navigate(
|
||||||
|
"setup/mfa/${viewModel.mfaResponse!!.mfaSpec!!.ticket}/${
|
||||||
|
viewModel.mfaResponse!!.mfaSpec!!.allowedMethods.joinToString(
|
||||||
|
","
|
||||||
|
)
|
||||||
|
}"
|
||||||
|
)
|
||||||
|
viewModel.mfaComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.login_heading),
|
||||||
|
style = MaterialTheme.typography.displaySmall.copy(
|
||||||
|
fontSize = 30.sp,
|
||||||
|
fontWeight = FontWeight.Black,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp, vertical = 10.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(270.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
FormTextField(
|
||||||
|
value = viewModel.email,
|
||||||
|
label = stringResource(R.string.email),
|
||||||
|
onChange = { viewModel.setEmail(it) },
|
||||||
|
modifier = Modifier.padding(vertical = 25.dp)
|
||||||
|
)
|
||||||
|
FormTextField(
|
||||||
|
value = viewModel.password,
|
||||||
|
label = stringResource(R.string.password),
|
||||||
|
password = true,
|
||||||
|
onChange = { viewModel.setPassword(it) })
|
||||||
|
|
||||||
|
AnyLink(
|
||||||
|
text = stringResource(R.string.password_forgot),
|
||||||
|
action = { navController.navigate("about/placeholder") },
|
||||||
|
modifier = Modifier.padding(vertical = 7.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (viewModel.error != null) {
|
||||||
|
Text(
|
||||||
|
text = viewModel.error!!,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 15.sp
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(vertical = 7.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 30.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
|
||||||
|
|
||||||
|
Weblink(
|
||||||
|
text = stringResource(R.string.password_manager_hint),
|
||||||
|
url = "$REVOLT_SUPPORT/kb/interface/android/using-a-password-manager",
|
||||||
|
)
|
||||||
|
|
||||||
|
AnyLink(
|
||||||
|
text = stringResource(R.string.resend_verification),
|
||||||
|
action = { navController.navigate("about/placeholder") },
|
||||||
|
modifier = Modifier.padding(vertical = 7.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
ElevatedButton(
|
||||||
|
onClick = { navController.popBackStack() },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.back))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.doLogin() },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.login))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,270 @@
|
||||||
|
package chat.revolt.screens.login
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import chat.revolt.R
|
||||||
|
import chat.revolt.api.routes.account.MfaResponseRecoveryCode
|
||||||
|
import chat.revolt.api.routes.account.MfaResponseTotpCode
|
||||||
|
import chat.revolt.api.routes.account.authenticateWithMfaRecoveryCode
|
||||||
|
import chat.revolt.api.routes.account.authenticateWithMfaTotpCode
|
||||||
|
import chat.revolt.api.routes.user.fetchSelfWithNewToken
|
||||||
|
import chat.revolt.components.generic.CollapsibleCard
|
||||||
|
import chat.revolt.components.generic.FormTextField
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class MfaScreenViewModel : ViewModel() {
|
||||||
|
private var _totpCode by mutableStateOf("")
|
||||||
|
val totpCode: String
|
||||||
|
get() = _totpCode
|
||||||
|
|
||||||
|
private var _recoveryCode by mutableStateOf("")
|
||||||
|
val recoveryCode: String
|
||||||
|
get() = _recoveryCode
|
||||||
|
|
||||||
|
private var _error by mutableStateOf<String?>(null)
|
||||||
|
val error: String?
|
||||||
|
get() = _error
|
||||||
|
|
||||||
|
private var _navigateToHome by mutableStateOf(false)
|
||||||
|
val navigateToHome: Boolean
|
||||||
|
get() = _navigateToHome
|
||||||
|
|
||||||
|
fun setTotpCode(code: String) {
|
||||||
|
_totpCode = code.replace(Regex("[^0-9]"), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRecoveryCode(code: String) {
|
||||||
|
_recoveryCode = code
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigationComplete() {
|
||||||
|
_navigateToHome = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tryAuthorizeTotp(mfaTicket: String) {
|
||||||
|
_error = null
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = authenticateWithMfaTotpCode(mfaTicket, MfaResponseTotpCode(totpCode))
|
||||||
|
if (response.error != null) {
|
||||||
|
_error = response.error.type
|
||||||
|
} else {
|
||||||
|
Log.d(
|
||||||
|
"MFA",
|
||||||
|
"Successfully authorized TOTP. Token: ${response.firstUserHints!!.token}"
|
||||||
|
)
|
||||||
|
val self = fetchSelfWithNewToken(response.firstUserHints.token)
|
||||||
|
Log.d("MFA", "Self: ${self.username}")
|
||||||
|
_navigateToHome = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tryAuthorizeRecovery(mfaTicket: String) {
|
||||||
|
_error = null
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response =
|
||||||
|
authenticateWithMfaRecoveryCode(mfaTicket, MfaResponseRecoveryCode(recoveryCode))
|
||||||
|
if (response.error != null) {
|
||||||
|
_error = response.error.type
|
||||||
|
} else {
|
||||||
|
Log.d(
|
||||||
|
"MFA",
|
||||||
|
"Successfully authorized recovery code. Token: ${response.firstUserHints!!.token}"
|
||||||
|
)
|
||||||
|
val self = fetchSelfWithNewToken(response.firstUserHints.token)
|
||||||
|
Log.d("MFA", "Self: ${self.username}")
|
||||||
|
_navigateToHome = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MfaScreen(
|
||||||
|
navController: NavController,
|
||||||
|
allowedAuthTypesCommaSep: String,
|
||||||
|
mfaTicket: String,
|
||||||
|
viewModel: MfaScreenViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val allowedAuthTypes = allowedAuthTypesCommaSep.split(",")
|
||||||
|
|
||||||
|
if (viewModel.navigateToHome) {
|
||||||
|
navController.navigate("chat/home") {
|
||||||
|
popUpTo("setup/greeting") { inclusive = true }
|
||||||
|
}
|
||||||
|
viewModel.navigationComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
|
.weight(1f)
|
||||||
|
.verticalScroll(
|
||||||
|
rememberScrollState()
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.mfa_interstitial_header),
|
||||||
|
style = MaterialTheme.typography.displaySmall.copy(
|
||||||
|
fontSize = 30.sp,
|
||||||
|
fontWeight = FontWeight.Black,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp, vertical = 10.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.mfa_interstitial_lead),
|
||||||
|
color = Color(0xaaffffff),
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp, vertical = 10.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (viewModel.error != null) {
|
||||||
|
Text(
|
||||||
|
text = viewModel.error!!,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 15.sp
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp, vertical = 10.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Collapsible cards for each auth type
|
||||||
|
allowedAuthTypes.forEach { authType ->
|
||||||
|
when (authType) {
|
||||||
|
"Totp" -> {
|
||||||
|
CollapsibleCard(title = stringResource(R.string.mfa_totp_header)) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.mfa_totp_lead),
|
||||||
|
color = Color(0xaaffffff),
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp, vertical = 10.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
FormTextField(
|
||||||
|
label = stringResource(R.string.mfa_totp_code),
|
||||||
|
onChange = { viewModel.setTotpCode(it) },
|
||||||
|
value = viewModel.totpCode,
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.tryAuthorizeTotp(mfaTicket) },
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp, vertical = 10.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.next),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"Recovery" -> {
|
||||||
|
CollapsibleCard(title = stringResource(R.string.mfa_recovery_header)) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.mfa_recovery_lead),
|
||||||
|
color = Color(0xaaffffff),
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp, vertical = 10.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
FormTextField(
|
||||||
|
label = stringResource(R.string.mfa_recovery_code),
|
||||||
|
onChange = { viewModel.setRecoveryCode(it) },
|
||||||
|
value = viewModel.recoveryCode,
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.tryAuthorizeRecovery(mfaTicket) },
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 20.dp, vertical = 10.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.next),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 30.dp)
|
||||||
|
) {
|
||||||
|
ElevatedButton(
|
||||||
|
onClick = { navController.popBackStack() },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package chat.revolt.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
val Purple80 = Color(0xFFD0BCFF)
|
||||||
|
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||||
|
val Pink80 = Color(0xFFEFB8C8)
|
||||||
|
|
||||||
|
val Purple40 = Color(0xFF6650a4)
|
||||||
|
val PurpleGrey40 = Color(0xFF625b71)
|
||||||
|
val Pink40 = Color(0xFF7D5260)
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
package chat.revolt.ui.theme
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
|
||||||
|
const val FOREGROUND = 0xffffffff
|
||||||
|
|
||||||
|
val DarkColorScheme = darkColorScheme(
|
||||||
|
primary = Color(0xfffe4654),
|
||||||
|
onPrimary = Color(FOREGROUND),
|
||||||
|
secondary = Color(0xfffd6671),
|
||||||
|
onSecondary = Color(FOREGROUND),
|
||||||
|
tertiary = Color(0xffff6667),
|
||||||
|
onTertiary = Color(FOREGROUND),
|
||||||
|
background = Color(0xff101823),
|
||||||
|
onBackground = Color(FOREGROUND),
|
||||||
|
surfaceVariant = Color(0xff172333),
|
||||||
|
onSurfaceVariant = Color(FOREGROUND),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RevoltTheme(
|
||||||
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
// Dynamic color is available on Android 12+
|
||||||
|
dynamicColor: Boolean = false,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val colorScheme = when {
|
||||||
|
dynamicColor && darkTheme && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
dynamicDarkColorScheme(context)
|
||||||
|
}
|
||||||
|
dynamicColor && !darkTheme && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
dynamicLightColorScheme(context)
|
||||||
|
}
|
||||||
|
else -> DarkColorScheme
|
||||||
|
}
|
||||||
|
val view = LocalView.current
|
||||||
|
if (!view.isInEditMode) {
|
||||||
|
SideEffect {
|
||||||
|
val window = (view.context as Activity).window
|
||||||
|
window.statusBarColor = colorScheme.background.toArgb()
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = RevoltTypography,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
package chat.revolt.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import chat.revolt.R
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
private val Inter = FontFamily(
|
||||||
|
Font(R.font.inter_thin, FontWeight.Thin),
|
||||||
|
Font(R.font.inter_extralight, FontWeight.ExtraLight),
|
||||||
|
Font(R.font.inter_light, FontWeight.Light),
|
||||||
|
Font(R.font.inter_regular, FontWeight.Normal),
|
||||||
|
Font(R.font.inter_medium, FontWeight.Medium),
|
||||||
|
Font(R.font.inter_semibold, FontWeight.SemiBold),
|
||||||
|
Font(R.font.inter_bold, FontWeight.Bold),
|
||||||
|
Font(R.font.inter_extrabold, FontWeight.ExtraBold),
|
||||||
|
Font(R.font.inter_black, FontWeight.Black),
|
||||||
|
)
|
||||||
|
|
||||||
|
const val ALL_INTER = true
|
||||||
|
|
||||||
|
private val OpenSans = if (ALL_INTER) Inter else FontFamily(
|
||||||
|
Font(R.font.opensans_light, FontWeight.Light, FontStyle.Normal),
|
||||||
|
Font(R.font.opensans_lightitalic, FontWeight.Light, FontStyle.Italic),
|
||||||
|
Font(R.font.opensans_regular, FontWeight.Normal, FontStyle.Normal),
|
||||||
|
Font(R.font.opensans_regularitalic, FontWeight.Normal, FontStyle.Italic),
|
||||||
|
Font(R.font.opensans_medium, FontWeight.Medium, FontStyle.Normal),
|
||||||
|
Font(R.font.opensans_mediumitalic, FontWeight.Medium, FontStyle.Italic),
|
||||||
|
Font(R.font.opensans_semibold, FontWeight.SemiBold, FontStyle.Normal),
|
||||||
|
Font(R.font.opensans_semibolditalic, FontWeight.SemiBold, FontStyle.Italic),
|
||||||
|
Font(R.font.opensans_bold, FontWeight.Bold, FontStyle.Normal),
|
||||||
|
Font(R.font.opensans_bolditalic, FontWeight.Bold, FontStyle.Italic),
|
||||||
|
Font(R.font.opensans_extrabold, FontWeight.ExtraBold, FontStyle.Normal),
|
||||||
|
Font(R.font.opensans_extrabolditalic, FontWeight.ExtraBold, FontStyle.Italic),
|
||||||
|
)
|
||||||
|
|
||||||
|
val RevoltTypography = Typography(
|
||||||
|
displayLarge = TextStyle(
|
||||||
|
fontFamily = Inter,
|
||||||
|
fontWeight = FontWeight.Black,
|
||||||
|
fontSize = 57.sp
|
||||||
|
),
|
||||||
|
displayMedium = TextStyle(
|
||||||
|
fontFamily = Inter,
|
||||||
|
fontWeight = FontWeight.ExtraBold,
|
||||||
|
fontSize = 45.sp
|
||||||
|
),
|
||||||
|
displaySmall = TextStyle(
|
||||||
|
fontFamily = Inter,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 36.sp
|
||||||
|
),
|
||||||
|
|
||||||
|
headlineLarge = TextStyle(
|
||||||
|
fontFamily = OpenSans,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 32.sp
|
||||||
|
),
|
||||||
|
headlineMedium = TextStyle(
|
||||||
|
fontFamily = OpenSans,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 28.sp
|
||||||
|
),
|
||||||
|
headlineSmall = TextStyle(
|
||||||
|
fontFamily = OpenSans,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 24.sp
|
||||||
|
),
|
||||||
|
|
||||||
|
titleLarge = TextStyle(
|
||||||
|
fontFamily = OpenSans,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 22.sp
|
||||||
|
),
|
||||||
|
titleMedium = TextStyle(
|
||||||
|
fontFamily = OpenSans,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 16.sp
|
||||||
|
),
|
||||||
|
titleSmall = TextStyle(
|
||||||
|
fontFamily = OpenSans,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 14.sp
|
||||||
|
),
|
||||||
|
|
||||||
|
labelLarge = TextStyle(
|
||||||
|
fontFamily = OpenSans,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 14.sp
|
||||||
|
),
|
||||||
|
labelMedium = TextStyle(
|
||||||
|
fontFamily = OpenSans,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 12.sp
|
||||||
|
),
|
||||||
|
labelSmall = TextStyle(
|
||||||
|
fontFamily = OpenSans,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 11.sp
|
||||||
|
),
|
||||||
|
|
||||||
|
bodyLarge = TextStyle(
|
||||||
|
fontFamily = OpenSans,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 16.sp
|
||||||
|
),
|
||||||
|
bodyMedium = TextStyle(
|
||||||
|
fontFamily = OpenSans,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 14.sp
|
||||||
|
),
|
||||||
|
bodySmall = TextStyle(
|
||||||
|
fontFamily = OpenSans,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 12.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,0h108v108h-108z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="53.69"
|
||||||
|
android:startY="103.2"
|
||||||
|
android:endX="53.69"
|
||||||
|
android:endY="0.5"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FF101823"/>
|
||||||
|
<item android:offset="1" android:color="#FF182435"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="28.575"
|
||||||
|
android:viewportHeight="28.575">
|
||||||
|
<path
|
||||||
|
android:pathData="m16.272,12.646c0,0.734 -0.4,1.175 -1.255,1.175H13.602v-2.323h1.415c0.854,0 1.255,0.454 1.255,1.148zM9.95,9.602 L11.32,11.502v7.47h2.283v-3.537h0.547l1.949,3.538h2.576l-2.163,-3.711c1.244,-0.303 2.11,-1.43 2.082,-2.71 0,-1.629 -1.148,-2.95 -3.444,-2.95z"
|
||||||
|
android:strokeWidth="0.0684682"
|
||||||
|
android:fillColor="#fe4654"
|
||||||
|
android:strokeColor="#fe4654"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="193.73dp"
|
||||||
|
android:height="37.44dp"
|
||||||
|
android:viewportWidth="193.73"
|
||||||
|
android:viewportHeight="37.44">
|
||||||
|
<path
|
||||||
|
android:pathData="M24.98,12.56c0,2.79 -1.52,4.46 -4.76,4.46L14.84,17.02L14.84,8.2L20.22,8.2C23.46,8.2 24.98,9.93 24.98,12.56ZM0.98,1.01 L6.18,8.22L6.18,36.58h8.67L14.84,23.15h2.08l7.4,13.43h9.78l-8.21,-14.09A10.35,10.35 0,0 0,33.8 12.21c0,-6.18 -4.36,-11.2 -13.07,-11.2ZM61.01,1.01L39.22,1.01L39.22,36.58L61.01,36.58L61.01,29.64L47.89,29.64v-7.8L59.49,21.84L59.49,15.15L47.89,15.15L47.89,8.21L61.01,8.21ZM82,27.87 L73.18,1.01L63.95,1.01L76.57,36.58L87.42,36.58L100.03,1.01L90.86,1.01ZM138.65,18.69c0,-10.69 -8.06,-18.19 -18.19,-18.19 -10.09,0 -18.3,7.5 -18.3,18.19a17.9,17.9 0,0 0,18.3 18.24A17.82,17.82 0,0 0,138.65 18.69ZM111.03,18.69c0,-6.34 3.65,-10.34 9.43,-10.34 5.68,0 9.38,4 9.38,10.34 0,6.23 -3.7,10.34 -9.38,10.34C114.68,29.03 111.03,24.93 111.03,18.69ZM143.47,1.01L143.47,36.58L163.49,36.58v-6.95L152.13,29.63L152.13,1.01ZM165.71,8.21h9.43L175.14,36.58h8.67L183.81,8.2h9.43v-7.2L165.71,1.01Z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:fillColor="#fff"
|
||||||
|
android:strokeColor="#fff"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name" translatable="false">Revolt</string>
|
||||||
|
|
||||||
|
<string name="back">← Back</string>
|
||||||
|
<string name="next">Next →</string>
|
||||||
|
<string name="cancel">Cancel</string>
|
||||||
|
<string name="lets_go">Let\'s go</string>
|
||||||
|
<string name="loading">Fetching some info, hang in there…</string>
|
||||||
|
|
||||||
|
<string name="terms_of_service">Terms of Service</string>
|
||||||
|
<string name="privacy_policy">Privacy Policy</string>
|
||||||
|
<string name="community_guidelines">Community Guidelines</string>
|
||||||
|
|
||||||
|
<string name="login_onboarding_heading">Find your community, connect with the world.</string>
|
||||||
|
<string name="login_onboarding_body">Revolt is one of the best ways to stay connected with your friends and community, anywhere, anytime.</string>
|
||||||
|
|
||||||
|
<string name="email">Email</string>
|
||||||
|
<string name="password">Password</string>
|
||||||
|
|
||||||
|
<string name="login">Log In</string>
|
||||||
|
<string name="signup">Sign Up</string>
|
||||||
|
<string name="resend">Resend</string>
|
||||||
|
<string name="send_email">Send email</string>
|
||||||
|
|
||||||
|
<string name="login_heading">Let\'s log you in</string>
|
||||||
|
<string name="password_forgot">Forgot password?</string>
|
||||||
|
<string name="resend_verification">Resend a verification email</string>
|
||||||
|
<string name="password_manager_hint">Using a password manager?</string>
|
||||||
|
<string name="password_forgot_heading">Forgot your password?</string>
|
||||||
|
<string name="password_forgot_instructions">Enter your email and we\'ll send you instructions on how to reset your password.</string>
|
||||||
|
|
||||||
|
<string name="register_heading">Let\'s get you started</string>
|
||||||
|
<string name="check_mail">Check your mail!</string>
|
||||||
|
<string name="instructions_at_mail">We\'ve sent further instructions to %1$s.</string>
|
||||||
|
<string name="verify_then_choose_username">Verify your email, and then we\'ll get on with choosing your username.</string>
|
||||||
|
|
||||||
|
<string name="welcome">Welcome!</string>
|
||||||
|
<string name="username_choose_lead">It\'s time to choose a username!</string>
|
||||||
|
<string name="username_choose_others">Others will find, recognise and mention you with this name.</string>
|
||||||
|
<string name="username_choose_changeable">But if you change your mind, you can change your username at any time in your User Settings.</string>
|
||||||
|
|
||||||
|
<string name="mfa_interstitial_header">One more thing</string>
|
||||||
|
<string name="mfa_interstitial_lead">You\'ve got 2FA enabled to keep your account extra-safe.</string>
|
||||||
|
<string name="mfa_type_otp">Use a one-time password</string>
|
||||||
|
<string name="mfa_type_recovery">Use a recovery code</string>
|
||||||
|
|
||||||
|
<string name="mfa_totp_header">Enter a six-digit code</string>
|
||||||
|
<string name="mfa_totp_lead">Enter the six-digit code from your authenticator app.</string>
|
||||||
|
<string name="mfa_totp_code">Code</string>
|
||||||
|
|
||||||
|
<string name="mfa_recovery_header">Enter a recovery code</string>
|
||||||
|
<string name="mfa_recovery_lead">Enter one of your recovery codes.</string>
|
||||||
|
<string name="mfa_recovery_code">Code</string>
|
||||||
|
|
||||||
|
<string name="mfa_password_header">Enter your password</string>
|
||||||
|
<string name="mfa_password_lead">Enter your password to continue.</string>
|
||||||
|
|
||||||
|
<string name="about">About</string>
|
||||||
|
<string name="app_full_name">Revolt on Android</string>
|
||||||
|
<string name="oss_attribution">OSS Attribution</string>
|
||||||
|
|
||||||
|
<string name="comingsoon_heading">Gah, you found me!</string>
|
||||||
|
<string name="comingsoon_body">The feature you are trying to access is not ready yet, but we are steadily working on polishing it to perfection..</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Theme.Revolt" parent="android:Theme.Material.Light.NoActionBar" />
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample backup rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/guide/topics/data/autobackup
|
||||||
|
for details.
|
||||||
|
Note: This file is ignored for devices older that API 31
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore
|
||||||
|
-->
|
||||||
|
<full-backup-content>
|
||||||
|
<!--
|
||||||
|
<include domain="sharedpref" path="."/>
|
||||||
|
<exclude domain="sharedpref" path="device.xml"/>
|
||||||
|
-->
|
||||||
|
</full-backup-content>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample data extraction rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||||
|
for details.
|
||||||
|
-->
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup>
|
||||||
|
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
-->
|
||||||
|
</cloud-backup>
|
||||||
|
<!--
|
||||||
|
<device-transfer>
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
</device-transfer>
|
||||||
|
-->
|
||||||
|
</data-extraction-rules>
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
buildscript {
|
||||||
|
ext {
|
||||||
|
compose_version = '1.3.2'
|
||||||
|
compose_libraries_version = '1.3.1'
|
||||||
|
accompanist_version = '0.27.1'
|
||||||
|
okhttp_version = '4.10.0'
|
||||||
|
nav_version = '2.5.3'
|
||||||
|
hilt_version = '2.44'
|
||||||
|
coil_version = '2.2.2'
|
||||||
|
ktor_version = '2.1.3'
|
||||||
|
aboutlibraries_version = '10.5.2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'com.android.application' version '7.3.1' apply false
|
||||||
|
id 'com.android.library' version '7.3.1' apply false
|
||||||
|
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
|
||||||
|
id 'org.jetbrains.kotlin.plugin.serialization' version '1.7.20' apply false
|
||||||
|
id "com.google.dagger.hilt.android" version "2.44" apply false
|
||||||
|
id 'com.mikepenz.aboutlibraries.plugin' version "10.5.2" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app"s APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
#Sun Nov 20 00:24:55 CET 2022
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
gradlePluginPortal()
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootProject.name = "Revolt"
|
||||||
|
include ':app'
|
||||||