A cheap attempt at a native Bluesky client for Android
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

*: first commit

i'm so embarassed

geesawra 45aa49a4

+2310
+15
.gitignore
··· 1 + *.iml 2 + .gradle 3 + /local.properties 4 + /.idea/caches 5 + /.idea/libraries 6 + /.idea/modules.xml 7 + /.idea/workspace.xml 8 + /.idea/navEditor.xml 9 + /.idea/assetWizardSettings.xml 10 + .DS_Store 11 + /build 12 + /captures 13 + .externalNativeBuild 14 + .cxx 15 + local.properties
+3
.idea/.gitignore
··· 1 + # Default ignored files 2 + /shelf/ 3 + /workspace.xml
+1
.idea/.name
··· 1 + Jerry No
+6
.idea/AndroidProjectSystem.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="AndroidProjectSystem"> 4 + <option name="providerId" value="com.android.tools.idea.GradleProjectSystem" /> 5 + </component> 6 + </project>
+6
.idea/appInsightsSettings.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="AppInsightsSettings"> 4 + <option name="selectedTabId" value="Firebase Crashlytics" /> 5 + </component> 6 + </project>
+158
.idea/codeStyles/Project.xml
··· 1 + <component name="ProjectCodeStyleConfiguration"> 2 + <code_scheme name="Project" version="173"> 3 + <JavaCodeStyleSettings> 4 + <option name="IMPORT_LAYOUT_TABLE"> 5 + <value> 6 + <package name="" withSubpackages="true" static="false" module="true" /> 7 + <package name="android" withSubpackages="true" static="true" /> 8 + <package name="androidx" withSubpackages="true" static="true" /> 9 + <package name="com" withSubpackages="true" static="true" /> 10 + <package name="junit" withSubpackages="true" static="true" /> 11 + <package name="net" withSubpackages="true" static="true" /> 12 + <package name="org" withSubpackages="true" static="true" /> 13 + <package name="java" withSubpackages="true" static="true" /> 14 + <package name="javax" withSubpackages="true" static="true" /> 15 + <package name="" withSubpackages="true" static="true" /> 16 + <emptyLine /> 17 + <package name="android" withSubpackages="true" static="false" /> 18 + <emptyLine /> 19 + <package name="androidx" withSubpackages="true" static="false" /> 20 + <emptyLine /> 21 + <package name="com" withSubpackages="true" static="false" /> 22 + <emptyLine /> 23 + <package name="junit" withSubpackages="true" static="false" /> 24 + <emptyLine /> 25 + <package name="net" withSubpackages="true" static="false" /> 26 + <emptyLine /> 27 + <package name="org" withSubpackages="true" static="false" /> 28 + <emptyLine /> 29 + <package name="java" withSubpackages="true" static="false" /> 30 + <emptyLine /> 31 + <package name="javax" withSubpackages="true" static="false" /> 32 + <emptyLine /> 33 + <package name="" withSubpackages="true" static="false" /> 34 + <emptyLine /> 35 + </value> 36 + </option> 37 + </JavaCodeStyleSettings> 38 + <JetCodeStyleSettings> 39 + <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> 40 + </JetCodeStyleSettings> 41 + <codeStyleSettings language="XML"> 42 + <option name="FORCE_REARRANGE_MODE" value="1" /> 43 + <indentOptions> 44 + <option name="CONTINUATION_INDENT_SIZE" value="4" /> 45 + </indentOptions> 46 + <arrangement> 47 + <rules> 48 + <section> 49 + <rule> 50 + <match> 51 + <AND> 52 + <NAME>xmlns:android</NAME> 53 + <XML_ATTRIBUTE /> 54 + <XML_NAMESPACE>^$</XML_NAMESPACE> 55 + </AND> 56 + </match> 57 + </rule> 58 + </section> 59 + <section> 60 + <rule> 61 + <match> 62 + <AND> 63 + <NAME>xmlns:.*</NAME> 64 + <XML_ATTRIBUTE /> 65 + <XML_NAMESPACE>^$</XML_NAMESPACE> 66 + </AND> 67 + </match> 68 + <order>BY_NAME</order> 69 + </rule> 70 + </section> 71 + <section> 72 + <rule> 73 + <match> 74 + <AND> 75 + <NAME>.*:id</NAME> 76 + <XML_ATTRIBUTE /> 77 + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> 78 + </AND> 79 + </match> 80 + </rule> 81 + </section> 82 + <section> 83 + <rule> 84 + <match> 85 + <AND> 86 + <NAME>.*:name</NAME> 87 + <XML_ATTRIBUTE /> 88 + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> 89 + </AND> 90 + </match> 91 + </rule> 92 + </section> 93 + <section> 94 + <rule> 95 + <match> 96 + <AND> 97 + <NAME>name</NAME> 98 + <XML_ATTRIBUTE /> 99 + <XML_NAMESPACE>^$</XML_NAMESPACE> 100 + </AND> 101 + </match> 102 + </rule> 103 + </section> 104 + <section> 105 + <rule> 106 + <match> 107 + <AND> 108 + <NAME>style</NAME> 109 + <XML_ATTRIBUTE /> 110 + <XML_NAMESPACE>^$</XML_NAMESPACE> 111 + </AND> 112 + </match> 113 + </rule> 114 + </section> 115 + <section> 116 + <rule> 117 + <match> 118 + <AND> 119 + <NAME>.*</NAME> 120 + <XML_ATTRIBUTE /> 121 + <XML_NAMESPACE>^$</XML_NAMESPACE> 122 + </AND> 123 + </match> 124 + <order>BY_NAME</order> 125 + </rule> 126 + </section> 127 + <section> 128 + <rule> 129 + <match> 130 + <AND> 131 + <NAME>.*</NAME> 132 + <XML_ATTRIBUTE /> 133 + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> 134 + </AND> 135 + </match> 136 + <order>ANDROID_ATTRIBUTE_ORDER</order> 137 + </rule> 138 + </section> 139 + <section> 140 + <rule> 141 + <match> 142 + <AND> 143 + <NAME>.*</NAME> 144 + <XML_ATTRIBUTE /> 145 + <XML_NAMESPACE>.*</XML_NAMESPACE> 146 + </AND> 147 + </match> 148 + <order>BY_NAME</order> 149 + </rule> 150 + </section> 151 + </rules> 152 + </arrangement> 153 + </codeStyleSettings> 154 + <codeStyleSettings language="kotlin"> 155 + <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> 156 + </codeStyleSettings> 157 + </code_scheme> 158 + </component>
+5
.idea/codeStyles/codeStyleConfig.xml
··· 1 + <component name="ProjectCodeStyleConfiguration"> 2 + <state> 3 + <option name="USE_PER_PROJECT_SETTINGS" value="true" /> 4 + </state> 5 + </component>
+6
.idea/compiler.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="CompilerConfiguration"> 4 + <bytecodeTargetLevel target="21" /> 5 + </component> 6 + </project>
+24
.idea/deploymentTargetSelector.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="deploymentTargetSelector"> 4 + <selectionStates> 5 + <SelectionState runConfigName="app"> 6 + <option name="selectionMode" value="DROPDOWN" /> 7 + </SelectionState> 8 + <SelectionState runConfigName="MainActivity"> 9 + <option name="selectionMode" value="DROPDOWN" /> 10 + </SelectionState> 11 + <SelectionState runConfigName="MainActivity (1)"> 12 + <option name="selectionMode" value="DROPDOWN" /> 13 + <DropdownSelection timestamp="2025-09-25T16:00:45.132665900Z"> 14 + <Target type="DEFAULT_BOOT"> 15 + <handle> 16 + <DeviceId pluginId="PhysicalDevice" identifier="serial=57141FDCH007E3" /> 17 + </handle> 18 + </Target> 19 + </DropdownSelection> 20 + <DialogSelection /> 21 + </SelectionState> 22 + </selectionStates> 23 + </component> 24 + </project>
+13
.idea/deviceManager.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="DeviceTable"> 4 + <option name="columnSorters"> 5 + <list> 6 + <ColumnSorterState> 7 + <option name="column" value="Name" /> 8 + <option name="order" value="ASCENDING" /> 9 + </ColumnSorterState> 10 + </list> 11 + </option> 12 + </component> 13 + </project>
+19
.idea/gradle.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="GradleMigrationSettings" migrationVersion="1" /> 4 + <component name="GradleSettings"> 5 + <option name="linkedExternalProjectsSettings"> 6 + <GradleProjectSettings> 7 + <option name="testRunner" value="CHOOSE_PER_TEST" /> 8 + <option name="externalProjectPath" value="$PROJECT_DIR$" /> 9 + <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" /> 10 + <option name="modules"> 11 + <set> 12 + <option value="$PROJECT_DIR$" /> 13 + <option value="$PROJECT_DIR$/app" /> 14 + </set> 15 + </option> 16 + </GradleProjectSettings> 17 + </option> 18 + </component> 19 + </project>
+61
.idea/inspectionProfiles/Project_Default.xml
··· 1 + <component name="InspectionProjectProfileManager"> 2 + <profile version="1.0"> 3 + <option name="myName" value="Project Default" /> 4 + <inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> 5 + <option name="composableFile" value="true" /> 6 + <option name="previewFile" value="true" /> 7 + </inspection_tool> 8 + <inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true"> 9 + <option name="composableFile" value="true" /> 10 + <option name="previewFile" value="true" /> 11 + </inspection_tool> 12 + <inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> 13 + <option name="composableFile" value="true" /> 14 + <option name="previewFile" value="true" /> 15 + </inspection_tool> 16 + <inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true"> 17 + <option name="composableFile" value="true" /> 18 + <option name="previewFile" value="true" /> 19 + </inspection_tool> 20 + <inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> 21 + <option name="composableFile" value="true" /> 22 + </inspection_tool> 23 + <inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true"> 24 + <option name="composableFile" value="true" /> 25 + </inspection_tool> 26 + <inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> 27 + <option name="composableFile" value="true" /> 28 + </inspection_tool> 29 + <inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true"> 30 + <option name="composableFile" value="true" /> 31 + </inspection_tool> 32 + <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true"> 33 + <option name="composableFile" value="true" /> 34 + <option name="previewFile" value="true" /> 35 + </inspection_tool> 36 + <inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true"> 37 + <option name="composableFile" value="true" /> 38 + <option name="previewFile" value="true" /> 39 + </inspection_tool> 40 + <inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true"> 41 + <option name="composableFile" value="true" /> 42 + <option name="previewFile" value="true" /> 43 + </inspection_tool> 44 + <inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true"> 45 + <option name="composableFile" value="true" /> 46 + <option name="previewFile" value="true" /> 47 + </inspection_tool> 48 + <inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true"> 49 + <option name="composableFile" value="true" /> 50 + <option name="previewFile" value="true" /> 51 + </inspection_tool> 52 + <inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true"> 53 + <option name="composableFile" value="true" /> 54 + <option name="previewFile" value="true" /> 55 + </inspection_tool> 56 + <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> 57 + <option name="composableFile" value="true" /> 58 + <option name="previewFile" value="true" /> 59 + </inspection_tool> 60 + </profile> 61 + </component>
+10
.idea/migrations.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="ProjectMigrations"> 4 + <option name="MigrateToGradleLocalJavaHome"> 5 + <set> 6 + <option value="$PROJECT_DIR$" /> 7 + </set> 8 + </option> 9 + </component> 10 + </project>
+9
.idea/misc.xml
··· 1 + <project version="4"> 2 + <component name="ExternalStorageConfigurationManager" enabled="true" /> 3 + <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> 4 + <output url="file://$PROJECT_DIR$/build/classes" /> 5 + </component> 6 + <component name="ProjectType"> 7 + <option name="id" value="Android" /> 8 + </component> 9 + </project>
+17
.idea/runConfigurations.xml
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <project version="4"> 3 + <component name="RunConfigurationProducerService"> 4 + <option name="ignoredProducers"> 5 + <set> 6 + <option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" /> 7 + <option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" /> 8 + <option value="com.intellij.execution.junit.PatternConfigurationProducer" /> 9 + <option value="com.intellij.execution.junit.TestInClassConfigurationProducer" /> 10 + <option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" /> 11 + <option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" /> 12 + <option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" /> 13 + <option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" /> 14 + </set> 15 + </option> 16 + </component> 17 + </project>
+1
app/.gitignore
··· 1 + /build
+79
app/build.gradle.kts
··· 1 + plugins { 2 + id("com.google.devtools.ksp") 3 + id("com.google.dagger.hilt.android") 4 + alias(libs.plugins.android.application) 5 + alias(libs.plugins.kotlin.android) 6 + alias(libs.plugins.kotlin.compose) 7 + } 8 + 9 + android { 10 + namespace = "industries.geesawra.jerryno" 11 + compileSdk = 36 12 + 13 + defaultConfig { 14 + applicationId = "industries.geesawra.jerryno" 15 + minSdk = 36 16 + targetSdk = 36 17 + versionCode = 1 18 + versionName = "1.0" 19 + 20 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 + } 22 + 23 + buildTypes { 24 + release { 25 + isMinifyEnabled = false 26 + proguardFiles( 27 + getDefaultProguardFile("proguard-android-optimize.txt"), 28 + "proguard-rules.pro" 29 + ) 30 + } 31 + } 32 + compileOptions { 33 + sourceCompatibility = JavaVersion.VERSION_11 34 + targetCompatibility = JavaVersion.VERSION_11 35 + } 36 + kotlinOptions { 37 + jvmTarget = "11" 38 + } 39 + buildFeatures { 40 + compose = true 41 + } 42 + } 43 + 44 + dependencies { 45 + implementation("io.ktor:ktor-client-plugins:3.0.1") // Or more specifically: 46 + implementation("io.ktor:ktor-client-core:3.0.1") // Or the version aligned with the library 47 + implementation("io.ktor:ktor-client-okhttp:3.0.1") // Or your preferred engine 48 + implementation("io.ktor:ktor-client-content-negotiation:3.0.1") 49 + implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.1") 50 + implementation("sh.christian.ozone:bluesky:0.3.3") 51 + implementation("androidx.navigation:navigation-compose:2.9.4") 52 + implementation("io.coil-kt.coil3:coil-compose:3.0.1") 53 + implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.1") 54 + implementation("io.github.fornewid:placeholder-material3:2.0.0") 55 + implementation("androidx.media3:media3-exoplayer:1.8.0") 56 + implementation("androidx.media3:media3-ui:1.8.0") 57 + implementation("androidx.media3:media3-exoplayer-hls:1.8.0") 58 + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.4") 59 + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4") 60 + implementation("com.google.dagger:hilt-android:2.57.1") 61 + implementation("androidx.hilt:hilt-navigation-compose:1.3.0") 62 + implementation("androidx.compose.material3:material3-adaptive-navigation-suite") 63 + ksp("com.google.dagger:hilt-compiler:2.57.1") 64 + implementation(libs.androidx.core.ktx) 65 + implementation(libs.androidx.lifecycle.runtime.ktx) 66 + implementation(libs.androidx.activity.compose) 67 + implementation(platform(libs.androidx.compose.bom)) 68 + implementation(libs.androidx.compose.ui) 69 + implementation(libs.androidx.compose.ui.graphics) 70 + implementation(libs.androidx.compose.ui.tooling.preview) 71 + implementation(libs.androidx.compose.material3) 72 + testImplementation(libs.junit) 73 + androidTestImplementation(libs.androidx.junit) 74 + androidTestImplementation(libs.androidx.espresso.core) 75 + androidTestImplementation(platform(libs.androidx.compose.bom)) 76 + androidTestImplementation(libs.androidx.compose.ui.test.junit4) 77 + debugImplementation(libs.androidx.compose.ui.tooling) 78 + debugImplementation(libs.androidx.compose.ui.test.manifest) 79 + }
+23
app/proguard-rules.pro
··· 1 + # Add project specific ProGuard rules here. 2 + # You can control the set of applied configuration files using the 3 + # proguardFiles setting in build.gradle. 4 + # 5 + # For more details, see 6 + # http://developer.android.com/guide/developing/tools/proguard.html 7 + 8 + # If your project uses WebView with JS, uncomment the following 9 + # and specify the fully qualified class name to the JavaScript interface 10 + # class: 11 + #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 + # public *; 13 + #} 14 + 15 + # Uncomment this to preserve the line number information for 16 + # debugging stack traces. 17 + #-keepattributes SourceFile,LineNumberTable 18 + 19 + # If you keep the line number information, uncomment this to 20 + # hide the original source file name. 21 + #-renamesourcefileattribute SourceFile 22 + -keep class io.ktor.** { *; } 23 + -dontwarn io.ktor.**
+24
app/src/androidTest/java/industries/geesawra/jerryno/ExampleInstrumentedTest.kt
··· 1 + package industries.geesawra.jerryno 2 + 3 + import androidx.test.platform.app.InstrumentationRegistry 4 + import androidx.test.ext.junit.runners.AndroidJUnit4 5 + 6 + import org.junit.Test 7 + import org.junit.runner.RunWith 8 + 9 + import org.junit.Assert.* 10 + 11 + /** 12 + * Instrumented test, which will execute on an Android device. 13 + * 14 + * See [testing documentation](http://d.android.com/tools/testing). 15 + */ 16 + @RunWith(AndroidJUnit4::class) 17 + class ExampleInstrumentedTest { 18 + @Test 19 + fun useAppContext() { 20 + // Context of the app under test. 21 + val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 + assertEquals("industries.geesawra.jerryno", appContext.packageName) 23 + } 24 + }
+28
app/src/main/AndroidManifest.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <manifest xmlns:android="http://schemas.android.com/apk/res/android" 3 + xmlns:tools="http://schemas.android.com/tools"> 4 + <uses-permission android:name="android.permission.INTERNET" /> 5 + <application 6 + android:allowBackup="true" 7 + android:dataExtractionRules="@xml/data_extraction_rules" 8 + android:fullBackupContent="@xml/backup_rules" 9 + android:icon="@mipmap/ic_launcher" 10 + android:label="@string/app_name" 11 + android:roundIcon="@mipmap/ic_launcher_round" 12 + android:supportsRtl="true" 13 + android:theme="@style/Theme.JerryNo" 14 + android:name=".Application"> 15 + <activity 16 + android:name=".MainActivity" 17 + android:exported="true" 18 + android:theme="@style/Theme.JerryNo" 19 + android:windowSoftInputMode="adjustResize"> 20 + <intent-filter> 21 + <action android:name="android.intent.action.MAIN" /> 22 + 23 + <category android:name="android.intent.category.LAUNCHER" /> 24 + </intent-filter> 25 + </activity> 26 + </application> 27 + 28 + </manifest>
+165
app/src/main/java/industries/geesawra/jerryno/ComposeView.kt
··· 1 + package industries.geesawra.jerryno 2 + 3 + import android.util.Log 4 + import androidx.activity.compose.rememberLauncherForActivityResult 5 + import androidx.activity.result.PickVisualMediaRequest 6 + import androidx.activity.result.contract.ActivityResultContracts 7 + import androidx.compose.foundation.layout.Arrangement 8 + import androidx.compose.foundation.layout.Column 9 + import androidx.compose.foundation.layout.Row 10 + import androidx.compose.foundation.layout.WindowInsets 11 + import androidx.compose.foundation.layout.fillMaxWidth 12 + import androidx.compose.foundation.layout.height 13 + import androidx.compose.foundation.layout.ime 14 + import androidx.compose.foundation.layout.imePadding 15 + import androidx.compose.foundation.layout.padding 16 + import androidx.compose.material.icons.Icons 17 + import androidx.compose.material.icons.automirrored.filled.Send 18 + import androidx.compose.material.icons.filled.Add 19 + import androidx.compose.material3.Button 20 + import androidx.compose.material3.ExperimentalMaterial3Api 21 + import androidx.compose.material3.Icon 22 + import androidx.compose.material3.MaterialTheme 23 + import androidx.compose.material3.ModalBottomSheet 24 + import androidx.compose.material3.SheetState 25 + import androidx.compose.material3.Text 26 + import androidx.compose.material3.TextField 27 + import androidx.compose.runtime.Composable 28 + import androidx.compose.runtime.LaunchedEffect 29 + import androidx.compose.runtime.getValue 30 + import androidx.compose.runtime.mutableStateOf 31 + import androidx.compose.runtime.saveable.rememberSaveable 32 + import androidx.compose.runtime.setValue 33 + import androidx.compose.ui.Alignment 34 + import androidx.compose.ui.Modifier 35 + import androidx.compose.ui.focus.FocusRequester 36 + import androidx.compose.ui.focus.focusRequester 37 + import androidx.compose.ui.text.font.FontWeight 38 + import androidx.compose.ui.unit.dp 39 + import industries.geesawra.jerryno.datalayer.Bluesky 40 + import kotlinx.coroutines.CoroutineScope 41 + import kotlinx.coroutines.launch 42 + 43 + @OptIn(ExperimentalMaterial3Api::class) 44 + @Composable 45 + fun ComposeView( 46 + modalSheetState: SheetState, 47 + focusRequester: FocusRequester, 48 + coroutineScope: CoroutineScope, 49 + bluesky: Bluesky, 50 + onDismissRequest: () -> Unit, 51 + ) { 52 + var text by rememberSaveable { mutableStateOf("") } 53 + 54 + ModalBottomSheet( 55 + onDismissRequest = { 56 + onDismissRequest() 57 + text = "" 58 + }, 59 + sheetState = modalSheetState, 60 + modifier = Modifier.imePadding(), 61 + contentWindowInsets = { 62 + WindowInsets.ime 63 + } 64 + ) { 65 + // TODO: how do we hide the keyboard when modalbottomsheet is gone? 66 + LaunchedEffect(Unit) { 67 + focusRequester.requestFocus() 68 + } 69 + Column( 70 + Modifier 71 + .fillMaxWidth() // Changed from fillMaxSize() 72 + .imePadding() 73 + ) { 74 + 75 + TextField( 76 + value = text, 77 + onValueChange = { text = it }, 78 + modifier = Modifier 79 + .focusRequester( 80 + focusRequester 81 + ) 82 + .fillMaxWidth() 83 + .height(200.dp) 84 + .padding(4.dp) 85 + ) 86 + 87 + Row( 88 + modifier = Modifier.fillMaxWidth(), // Changed from fillMaxSize() 89 + horizontalArrangement = Arrangement.SpaceBetween 90 + ) { 91 + val maxChars = 300 92 + 93 + Row( 94 + horizontalArrangement = Arrangement.Start, 95 + verticalAlignment = Alignment.CenterVertically 96 + ) { 97 + Button( 98 + enabled = text.isNotBlank() && text.length <= maxChars, 99 + onClick = { 100 + coroutineScope.launch { 101 + bluesky.post(text) 102 + modalSheetState.hide() // Animate the sheet away 103 + } 104 + }, 105 + modifier = Modifier.padding(4.dp) 106 + ) { 107 + Icon( 108 + Icons.AutoMirrored.Default.Send, 109 + "Send" 110 + ) 111 + } 112 + 113 + val charColor = if (text.length > maxChars) { 114 + MaterialTheme.colorScheme.error 115 + } else { 116 + MaterialTheme.colorScheme.onSurface 117 + } 118 + Text( 119 + style = MaterialTheme.typography.bodyMedium, 120 + text = "${text.length}", 121 + modifier = Modifier.padding(4.dp), 122 + fontWeight = FontWeight.Bold, 123 + color = charColor 124 + ) 125 + } 126 + 127 + val pickMultipleMedia = 128 + rememberLauncherForActivityResult( 129 + ActivityResultContracts.PickMultipleVisualMedia( 130 + 5 131 + ) 132 + ) { uris -> 133 + // Callback is invoked after the user selects media items or closes the 134 + // photo picker. 135 + if (uris.isNotEmpty()) { 136 + Log.d( 137 + "PhotoPicker", 138 + "Number of items selected: ${uris.size}" 139 + ) 140 + } else { 141 + Log.d( 142 + "PhotoPicker", 143 + "No media selected" 144 + ) 145 + } 146 + } 147 + 148 + Button( 149 + onClick = { 150 + pickMultipleMedia.launch( 151 + PickVisualMediaRequest( 152 + ActivityResultContracts.PickVisualMedia.ImageAndVideo 153 + ) 154 + ) 155 + }, 156 + modifier = Modifier.padding(4.dp) 157 + ) { 158 + Icon(Icons.Default.Add, "Add") 159 + Text("Add media") 160 + } 161 + } 162 + } 163 + } 164 + 165 + }
+381
app/src/main/java/industries/geesawra/jerryno/MainActivity.kt
··· 1 + package industries.geesawra.jerryno 2 + 3 + import android.app.Application 4 + import android.os.Bundle 5 + import android.util.Log 6 + import androidx.activity.ComponentActivity 7 + import androidx.activity.compose.rememberLauncherForActivityResult 8 + import androidx.activity.compose.setContent 9 + import androidx.activity.enableEdgeToEdge 10 + import androidx.activity.result.PickVisualMediaRequest 11 + import androidx.activity.result.contract.ActivityResultContracts 12 + import androidx.annotation.StringRes 13 + import androidx.compose.animation.AnimatedContentTransitionScope 14 + import androidx.compose.animation.EnterTransition 15 + import androidx.compose.animation.ExitTransition 16 + import androidx.compose.animation.ExperimentalSharedTransitionApi 17 + import androidx.compose.animation.SharedTransitionLayout 18 + import androidx.compose.animation.core.EaseIn 19 + import androidx.compose.animation.core.EaseOut 20 + import androidx.compose.animation.core.LinearEasing 21 + import androidx.compose.animation.core.tween 22 + import androidx.compose.animation.fadeIn 23 + import androidx.compose.animation.fadeOut 24 + import androidx.compose.foundation.border 25 + import androidx.compose.foundation.clickable 26 + import androidx.compose.foundation.layout.Arrangement 27 + import androidx.compose.foundation.layout.Box 28 + import androidx.compose.foundation.layout.Column 29 + import androidx.compose.foundation.layout.PaddingValues 30 + import androidx.compose.foundation.layout.Row 31 + import androidx.compose.foundation.layout.WindowInsets 32 + import androidx.compose.foundation.layout.fillMaxHeight 33 + import androidx.compose.foundation.layout.fillMaxSize 34 + import androidx.compose.foundation.layout.fillMaxWidth 35 + import androidx.compose.foundation.layout.height 36 + import androidx.compose.foundation.layout.ime 37 + import androidx.compose.foundation.layout.imePadding // Added import 38 + import androidx.compose.foundation.layout.padding 39 + import androidx.compose.foundation.layout.size 40 + import androidx.compose.foundation.layout.sizeIn 41 + import androidx.compose.foundation.layout.width 42 + import androidx.compose.foundation.lazy.LazyColumn 43 + import androidx.compose.foundation.lazy.grid.GridCells 44 + import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 45 + import androidx.compose.foundation.shape.RoundedCornerShape 46 + import androidx.compose.material.icons.Icons 47 + import androidx.compose.material.icons.automirrored.filled.Send 48 + import androidx.compose.material.icons.filled.Add 49 + import androidx.compose.material.icons.filled.Create 50 + import androidx.compose.material.icons.filled.Home 51 + import androidx.compose.material.icons.filled.Notifications 52 + import androidx.compose.material3.Button 53 + import androidx.compose.material3.Card 54 + import androidx.compose.material3.CircularProgressIndicator 55 + import androidx.compose.material3.ExperimentalMaterial3Api 56 + import androidx.compose.material3.FloatingActionButton 57 + import androidx.compose.material3.Icon 58 + import androidx.compose.material3.MaterialTheme 59 + import androidx.compose.material3.MediumTopAppBar 60 + import androidx.compose.material3.ModalBottomSheet 61 + import androidx.compose.material3.Scaffold 62 + import androidx.compose.material3.SheetValue 63 + import androidx.compose.material3.Surface 64 + import androidx.compose.material3.Text 65 + import androidx.compose.material3.TextField 66 + import androidx.compose.material3.TopAppBarColors 67 + import androidx.compose.material3.TopAppBarDefaults 68 + import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold 69 + import androidx.compose.material3.rememberModalBottomSheetState 70 + import androidx.compose.material3.rememberTopAppBarState 71 + import androidx.compose.runtime.Composable 72 + import androidx.compose.runtime.DisposableEffect 73 + import androidx.compose.runtime.LaunchedEffect 74 + import androidx.compose.runtime.getValue 75 + import androidx.compose.runtime.mutableStateOf 76 + import androidx.compose.runtime.remember 77 + import androidx.compose.runtime.rememberCoroutineScope 78 + import androidx.compose.runtime.saveable.rememberSaveable 79 + import androidx.compose.runtime.setValue 80 + import androidx.compose.ui.Alignment 81 + import androidx.compose.ui.Modifier 82 + import androidx.compose.ui.draw.clip 83 + import androidx.compose.ui.focus.FocusRequester 84 + import androidx.compose.ui.focus.focusRequester 85 + import androidx.compose.ui.graphics.vector.ImageVector 86 + import androidx.compose.ui.input.nestedscroll.nestedScroll 87 + import androidx.compose.ui.layout.ContentScale 88 + import androidx.compose.ui.platform.LocalContext 89 + import androidx.compose.ui.platform.LocalSoftwareKeyboardController 90 + import androidx.compose.ui.res.stringResource 91 + import androidx.compose.ui.text.font.FontWeight 92 + import androidx.compose.ui.unit.dp 93 + import androidx.compose.ui.viewinterop.AndroidView 94 + import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel 95 + import androidx.lifecycle.ViewModel 96 + import androidx.lifecycle.viewModelScope 97 + import androidx.lifecycle.viewmodel.compose.viewModel 98 + import androidx.media3.common.MediaItem 99 + import androidx.media3.exoplayer.ExoPlayer 100 + import androidx.media3.ui.PlayerView 101 + import androidx.navigation.compose.NavHost 102 + import androidx.navigation.compose.composable 103 + import androidx.navigation.compose.rememberNavController 104 + import app.bsky.feed.FeedViewPost 105 + import app.bsky.feed.FeedViewPostReasonUnion 106 + import app.bsky.feed.Post 107 + import app.bsky.feed.PostViewEmbedUnion 108 + import app.bsky.feed.ReplyRefParentUnion 109 + import coil3.compose.AsyncImage 110 + import coil3.request.ImageRequest 111 + import coil3.request.crossfade 112 + import dagger.assisted.Assisted 113 + import dagger.assisted.AssistedFactory 114 + import dagger.assisted.AssistedInject 115 + import dagger.hilt.android.AndroidEntryPoint 116 + import dagger.hilt.android.HiltAndroidApp 117 + import dagger.hilt.android.lifecycle.HiltViewModel 118 + import industries.geesawra.jerryno.datalayer.Bluesky 119 + import industries.geesawra.jerryno.ui.theme.JerryNoTheme 120 + import kotlinx.coroutines.Job 121 + import kotlinx.coroutines.launch 122 + import kotlinx.serialization.json.decodeFromJsonElement 123 + import sh.christian.ozone.BlueskyJson 124 + 125 + 126 + @HiltAndroidApp 127 + class Application : Application() {} 128 + 129 + enum class TimelineScreen() { 130 + Timeline, 131 + Compose 132 + } 133 + 134 + enum class TabBarDestinations( 135 + @StringRes val label: Int, 136 + val icon: ImageVector, 137 + @StringRes val contentDescription: Int 138 + ) { 139 + HOME(R.string.timeline, Icons.Filled.Home, R.string.timeline), 140 + NOTIFICATIONS(R.string.notifications, Icons.Filled.Notifications, R.string.notifications) 141 + } 142 + 143 + @AndroidEntryPoint 144 + @OptIn(ExperimentalMaterial3Api::class) 145 + class MainActivity : ComponentActivity() { 146 + @OptIn(ExperimentalSharedTransitionApi::class) 147 + override fun onCreate(savedInstanceState: Bundle?) { 148 + super.onCreate(savedInstanceState) 149 + enableEdgeToEdge() 150 + 151 + setContent { 152 + JerryNoTheme { 153 + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( 154 + rememberTopAppBarState() 155 + ) 156 + 157 + val bluesky = Bluesky( 158 + "https://wallera.computer", 159 + // TODO: login here 160 + ) 161 + 162 + val timelineViewModel = hiltViewModel<TimelineViewModel, TimelineViewModel.Factory>( 163 + creationCallback = { factory -> 164 + factory.create(bluesky) 165 + } 166 + ) 167 + 168 + var currentDestination by rememberSaveable { mutableStateOf(TabBarDestinations.HOME) } 169 + val navController = rememberNavController() 170 + 171 + val modalSheetState = rememberModalBottomSheetState( 172 + skipPartiallyExpanded = true, 173 + confirmValueChange = { sv -> 174 + sv != SheetValue.PartiallyExpanded 175 + } 176 + ) 177 + var showBottomSheet by remember { mutableStateOf(false) } 178 + val focusRequester = remember { FocusRequester() } 179 + val coroutineScope = rememberCoroutineScope() 180 + 181 + Surface( 182 + modifier = Modifier.fillMaxSize(), 183 + ) { 184 + NavigationSuiteScaffold( 185 + navigationSuiteItems = { 186 + TabBarDestinations.entries.forEach { 187 + item( 188 + icon = { 189 + Icon( 190 + it.icon, 191 + contentDescription = stringResource(it.contentDescription) 192 + ) 193 + }, 194 + label = { Text(stringResource(it.label)) }, 195 + selected = it == currentDestination, 196 + onClick = { currentDestination = it } 197 + ) 198 + } 199 + } 200 + ) { 201 + Scaffold( 202 + containerColor = MaterialTheme.colorScheme.surfaceContainer, 203 + modifier = Modifier 204 + .fillMaxSize() 205 + .nestedScroll(scrollBehavior.nestedScrollConnection), 206 + topBar = { 207 + MediumTopAppBar( 208 + colors = TopAppBarColors( 209 + containerColor = MaterialTheme.colorScheme.surfaceContainer, 210 + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer, 211 + navigationIconContentColor = MaterialTheme.colorScheme.surfaceContainer, 212 + titleContentColor = MaterialTheme.colorScheme.onSurface, 213 + actionIconContentColor = MaterialTheme.colorScheme.onSurface 214 + ), 215 + title = { 216 + Text(text = "Jerry No") 217 + }, 218 + scrollBehavior = scrollBehavior 219 + 220 + ) 221 + }, 222 + floatingActionButton = { 223 + FloatingActionButton( 224 + onClick = { 225 + // showBottomSheet = true 226 + navController.navigate(TimelineScreen.Compose.name) 227 + }, 228 + ) { 229 + Icon(Icons.Filled.Create, "Post") 230 + } 231 + }, 232 + ) { values -> 233 + NavHost( 234 + navController = navController, 235 + startDestination = TimelineScreen.Timeline.name, 236 + modifier = Modifier.padding(values) 237 + ) { 238 + composable(route = TimelineScreen.Timeline.name) { 239 + ShowSkeets( 240 + viewModel = timelineViewModel 241 + ) 242 + 243 + if (showBottomSheet) { 244 + ComposeView( 245 + modalSheetState = modalSheetState, 246 + focusRequester = focusRequester, 247 + coroutineScope = coroutineScope, 248 + bluesky = bluesky, 249 + onDismissRequest = { 250 + showBottomSheet = false 251 + } 252 + ) 253 + } 254 + } 255 + composable(route = TimelineScreen.Compose.name) { 256 + Text( 257 + "Compose", 258 + modifier = Modifier 259 + .fillMaxSize() 260 + .padding(10.dp) 261 + ) 262 + } 263 + } 264 + 265 + } 266 + } 267 + } 268 + } 269 + } 270 + } 271 + } 272 + 273 + @Composable 274 + fun ExoPlayerView(uri: String, modifier: Modifier) { 275 + // Get the current context 276 + val context = LocalContext.current 277 + 278 + // Initialize ExoPlayer 279 + val exoPlayer = ExoPlayer.Builder(context).build() 280 + 281 + // Create a MediaSource 282 + val mediaSource = remember(uri) { 283 + MediaItem.fromUri(uri) 284 + } 285 + 286 + // Set MediaSource to ExoPlayer 287 + LaunchedEffect(mediaSource) { 288 + exoPlayer.setMediaItem(mediaSource) 289 + exoPlayer.prepare() 290 + } 291 + 292 + // Manage lifecycle events 293 + DisposableEffect(Unit) { 294 + onDispose { 295 + exoPlayer.release() 296 + } 297 + } 298 + 299 + // Use AndroidView to embed an Android View (PlayerView) into Compose 300 + AndroidView( 301 + factory = { ctx -> 302 + PlayerView(ctx).apply { 303 + player = exoPlayer 304 + } 305 + }, 306 + modifier = modifier 307 + ) 308 + } 309 + 310 + @Composable 311 + fun ShowSkeets( 312 + viewModel: TimelineViewModel 313 + ) { 314 + viewModel.fetchTimeline() 315 + 316 + LazyColumn( 317 + modifier = Modifier 318 + .fillMaxSize(), 319 + verticalArrangement = Arrangement.spacedBy(8.dp), 320 + 321 + ) { 322 + if (viewModel.uiState.skeets.isEmpty()) { 323 + item { 324 + Box( 325 + contentAlignment = Alignment.Center, 326 + modifier = Modifier.fillMaxSize() 327 + ) { 328 + CircularProgressIndicator( 329 + modifier = Modifier 330 + .width(64.dp), 331 + color = MaterialTheme.colorScheme.onPrimaryContainer, 332 + trackColor = MaterialTheme.colorScheme.onPrimary, 333 + ) 334 + } 335 + } 336 + } else { 337 + viewModel.uiState.skeets.forEach { skeet -> 338 + item(key = skeet.post.uri.toString()) { // Added a key for better performance 339 + SkeetRowView(skeet) 340 + } 341 + } 342 + } 343 + 344 + } 345 + } 346 + 347 + data class TimelineUiState( 348 + val skeets: List<FeedViewPost> = listOf() 349 + ) 350 + 351 + @HiltViewModel(assistedFactory = TimelineViewModel.Factory::class) 352 + class TimelineViewModel @AssistedInject constructor( 353 + @Assisted private val bsky: Bluesky 354 + ) : ViewModel() { 355 + 356 + @AssistedFactory 357 + interface Factory { 358 + fun create(bsky: Bluesky): TimelineViewModel 359 + } 360 + 361 + var uiState by mutableStateOf(TimelineUiState()) 362 + private set 363 + 364 + private var fetchJob: Job? = null 365 + 366 + fun fetchTimeline() { 367 + fetchJob?.cancel() 368 + 369 + fetchJob = viewModelScope.launch { 370 + bsky.fetchTimeline().onSuccess { 371 + uiState = uiState.copy(skeets = it.feed) 372 + }.onFailure { Log.e("TimelineViewModel", "Failed to fetch timeline: ${it.message}") } 373 + } 374 + } 375 + 376 + fun post(content: String) { 377 + viewModelScope.launch { 378 + bsky.post(content) 379 + } 380 + } 381 + }
+217
app/src/main/java/industries/geesawra/jerryno/SkeetRowView.kt
··· 1 + package industries.geesawra.jerryno 2 + 3 + import androidx.compose.foundation.layout.Arrangement 4 + import androidx.compose.foundation.layout.Column 5 + import androidx.compose.foundation.layout.PaddingValues 6 + import androidx.compose.foundation.layout.Row 7 + import androidx.compose.foundation.layout.fillMaxSize 8 + import androidx.compose.foundation.layout.fillMaxWidth 9 + import androidx.compose.foundation.layout.height 10 + import androidx.compose.foundation.layout.padding 11 + import androidx.compose.foundation.layout.size 12 + import androidx.compose.foundation.layout.sizeIn 13 + import androidx.compose.foundation.lazy.grid.GridCells 14 + import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 15 + import androidx.compose.foundation.shape.RoundedCornerShape 16 + import androidx.compose.material3.Card 17 + import androidx.compose.material3.CardDefaults 18 + import androidx.compose.material3.MaterialTheme 19 + import androidx.compose.material3.Text 20 + import androidx.compose.runtime.Composable 21 + import androidx.compose.ui.Alignment 22 + import androidx.compose.ui.Modifier 23 + import androidx.compose.ui.draw.clip 24 + import androidx.compose.ui.draw.dropShadow 25 + import androidx.compose.ui.layout.ContentScale 26 + import androidx.compose.ui.platform.LocalContext 27 + import androidx.compose.ui.text.font.FontWeight 28 + import androidx.compose.ui.unit.dp 29 + import app.bsky.feed.FeedViewPost 30 + import app.bsky.feed.FeedViewPostReasonUnion 31 + import app.bsky.feed.Post 32 + import app.bsky.feed.PostViewEmbedUnion 33 + import app.bsky.feed.ReplyRefParentUnion 34 + import coil3.compose.AsyncImage 35 + import coil3.request.ImageRequest 36 + import coil3.request.crossfade 37 + import kotlinx.serialization.json.decodeFromJsonElement 38 + import sh.christian.ozone.BlueskyJson 39 + 40 + @Composable 41 + fun SkeetRowView(skeet: FeedViewPost) { 42 + val content = BlueskyJson.decodeFromJsonElement<Post>(skeet.post.record.value) 43 + val authorName = skeet.post.author.displayName ?: skeet.post.author.handle.toString(); 44 + 45 + val likes = skeet.post.likeCount; 46 + val reposts = skeet.post.repostCount; 47 + val replies = skeet.post.replyCount; 48 + 49 + val minSize = 55.dp; 50 + 51 + Row( 52 + verticalAlignment = Alignment.Top, 53 + horizontalArrangement = Arrangement.Start, 54 + modifier = Modifier.fillMaxWidth() 55 + ) { 56 + AsyncImage( 57 + model = ImageRequest.Builder(LocalContext.current) 58 + .data(skeet.post.author.avatar?.toString()) 59 + .crossfade(true) 60 + .build(), 61 + contentDescription = "Avatar", 62 + modifier = Modifier 63 + .padding(end = 8.dp, start = 8.dp, top = 10.dp) 64 + .size(minSize) 65 + .dropShadow(shape = RoundedCornerShape(12.dp), block = { 66 + radius = 2f 67 + }) 68 + .clip(RoundedCornerShape(12.dp)) 69 + 70 + ) 71 + 72 + Column( 73 + modifier = Modifier 74 + .weight(1f) 75 + .sizeIn(minHeight = minSize) 76 + .padding(end = 8.dp), 77 + verticalArrangement = Arrangement.spacedBy(4.dp), 78 + ) { 79 + Card( 80 + modifier = Modifier.fillMaxWidth(), 81 + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) 82 + ) { 83 + var headerSet = false 84 + 85 + skeet.reason?.let { 86 + it 87 + when (it) { 88 + is FeedViewPostReasonUnion.ReasonRepost -> { 89 + headerSet = true 90 + Text( 91 + text = "Reposted by ${it.value.by.displayName ?: it.value.by.handle.toString()}", 92 + color = MaterialTheme.colorScheme.onSurfaceVariant, 93 + style = MaterialTheme.typography.bodySmall, 94 + modifier = Modifier 95 + .fillMaxSize() 96 + .padding(top = 4.dp, start = 4.dp, end = 4.dp), 97 + fontWeight = FontWeight.Bold 98 + ) 99 + } 100 + 101 + else -> {} 102 + } 103 + } 104 + 105 + val titlePadding = { 106 + when (headerSet) { 107 + true -> PaddingValues(top = 1.dp, bottom = 1.dp, start = 4.dp, end = 4.dp) 108 + false -> PaddingValues(top = 4.dp, bottom = 1.dp, start = 4.dp, end = 4.dp) 109 + } 110 + }() 111 + 112 + Text( 113 + text = authorName, 114 + color = MaterialTheme.colorScheme.onSurface, 115 + style = MaterialTheme.typography.titleMedium, 116 + modifier = Modifier 117 + .padding(paddingValues = titlePadding), 118 + fontWeight = FontWeight.Bold 119 + ) 120 + 121 + Text( 122 + text = "@" + skeet.post.author.handle.handle, 123 + color = MaterialTheme.colorScheme.onSurfaceVariant, 124 + style = MaterialTheme.typography.bodySmall, 125 + modifier = Modifier 126 + .padding(bottom = 4.dp, start = 4.dp, end = 4.dp), 127 + 128 + ) 129 + 130 + skeet.reply?.let { 131 + it 132 + val parent = it.parent; 133 + when (parent) { 134 + is ReplyRefParentUnion.PostView -> { 135 + Text( 136 + text = "↪ In reply to ${parent.value.author.displayName ?: parent.value.author.handle.toString()}", 137 + color = MaterialTheme.colorScheme.onSurfaceVariant, 138 + style = MaterialTheme.typography.labelSmall, 139 + modifier = Modifier 140 + .fillMaxSize() 141 + .padding(start = 4.dp, end = 4.dp, bottom = 4.dp), 142 + ) 143 + } 144 + 145 + else -> {} 146 + } 147 + } 148 + 149 + 150 + Text( 151 + text = content.text, 152 + color = MaterialTheme.colorScheme.onSurface, 153 + style = MaterialTheme.typography.bodyMedium, 154 + modifier = Modifier 155 + .padding(bottom = 4.dp, start = 4.dp, end = 4.dp), 156 + ) 157 + 158 + val embed = skeet.post.embed 159 + when (embed) { 160 + is PostViewEmbedUnion.ImagesView -> { 161 + val img = embed.value.images 162 + 163 + LazyVerticalGrid( 164 + columns = GridCells.Fixed({ 165 + when (img.size) { 166 + 1 -> 1 167 + else -> 2 168 + } 169 + }()), 170 + modifier = Modifier 171 + .height(200.dp) 172 + .fillMaxWidth() 173 + .padding(top = 4.dp), 174 + userScrollEnabled = false, 175 + content = { 176 + items(img.size) { index -> 177 + val img = img.get(index) 178 + 179 + AsyncImage( 180 + model = ImageRequest.Builder(LocalContext.current) 181 + .data(img.thumb.toString()) 182 + .crossfade(true) 183 + .build(), 184 + contentScale = ContentScale.Crop, 185 + contentDescription = img.alt, 186 + modifier = Modifier 187 + .height(200.dp) 188 + .fillMaxWidth() 189 + ) 190 + } 191 + } 192 + ) 193 + } 194 + 195 + is PostViewEmbedUnion.VideoView -> { 196 + val video = embed.value 197 + ExoPlayerView( 198 + video.playlist.uri, 199 + modifier = Modifier // TODO: https://github.com/fengdai/compose-media 200 + .height(200.dp) 201 + ) 202 + } // TODO: build this 203 + else -> {} 204 + } 205 + } 206 + TimelinePostActionsView( 207 + modifier = Modifier 208 + .padding(start = 8.dp) 209 + .fillMaxWidth(), 210 + replies = replies, 211 + likes = likes, 212 + reposts = reposts, 213 + ) 214 + } 215 + 216 + } 217 + }
+183
app/src/main/java/industries/geesawra/jerryno/TimelinePostActionsView.kt
··· 1 + package industries.geesawra.jerryno 2 + 3 + import androidx.compose.foundation.layout.Arrangement 4 + import androidx.compose.foundation.layout.Row 5 + import androidx.compose.foundation.layout.padding 6 + import androidx.compose.foundation.layout.size 7 + import androidx.compose.material.icons.Icons 8 + import androidx.compose.material.icons.filled.ThumbUp 9 + import androidx.compose.material3.Icon 10 + import androidx.compose.material3.IconButton 11 + import androidx.compose.material3.Text 12 + import androidx.compose.runtime.Composable 13 + import androidx.compose.runtime.getValue 14 + import androidx.compose.runtime.mutableStateOf 15 + import androidx.compose.runtime.remember 16 + import androidx.compose.runtime.setValue 17 + import androidx.compose.ui.Alignment 18 + import androidx.compose.ui.Modifier 19 + import androidx.compose.ui.graphics.Color 20 + import androidx.compose.ui.graphics.PathFillType 21 + import androidx.compose.ui.graphics.SolidColor 22 + import androidx.compose.ui.graphics.vector.ImageVector 23 + import androidx.compose.ui.graphics.vector.path 24 + import androidx.compose.ui.unit.dp 25 + 26 + 27 + @Composable 28 + private fun IconWithNumber(imageVector: ImageVector, contentDescription: String, number: Long?) { 29 + Row( 30 + horizontalArrangement = Arrangement.Center, 31 + verticalAlignment = Alignment.CenterVertically 32 + ) { 33 + var fontSize by remember { 34 + mutableStateOf(10.dp) 35 + } 36 + Icon( 37 + imageVector, 38 + contentDescription = contentDescription, 39 + modifier = Modifier.size(15.dp), 40 + ) 41 + Text( 42 + modifier = Modifier.padding(start = 2.dp), 43 + text = (number ?: 0).toString(), 44 + maxLines = 1, 45 + onTextLayout = { textLayout -> 46 + if (textLayout.multiParagraph.didExceedMaxLines) { 47 + fontSize -= 1.dp 48 + } 49 + } 50 + ) 51 + } 52 + } 53 + 54 + @Composable 55 + fun TimelinePostActionsView( 56 + modifier: Modifier = Modifier, 57 + replies: Long?, 58 + likes: Long?, 59 + reposts: Long?, 60 + ) { 61 + Row( 62 + horizontalArrangement = Arrangement.End, 63 + modifier = modifier, 64 + ) { 65 + IconButton( 66 + onClick = {} 67 + ) { 68 + IconWithNumber( 69 + Chat_bubble, 70 + contentDescription = "Reply", 71 + number = replies, 72 + ) 73 + } 74 + IconButton( 75 + onClick = {} 76 + ) { 77 + IconWithNumber( 78 + Icons.Default.ThumbUp, 79 + contentDescription = "Like", 80 + number = likes 81 + ) 82 + } 83 + IconButton( 84 + onClick = {} 85 + ) { 86 + IconWithNumber( 87 + Reload, 88 + contentDescription = "Repost", 89 + number = reposts, 90 + ) 91 + } 92 + } 93 + } 94 + 95 + val Chat_bubble: ImageVector 96 + get() { 97 + if (_Chat_bubble != null) return _Chat_bubble!! 98 + 99 + _Chat_bubble = ImageVector.Builder( 100 + name = "Chat_bubble", 101 + defaultWidth = 24.dp, 102 + defaultHeight = 24.dp, 103 + viewportWidth = 960f, 104 + viewportHeight = 960f 105 + ).apply { 106 + path( 107 + fill = SolidColor(Color(0xFF000000)) 108 + ) { 109 + moveTo(80f, 880f) 110 + verticalLineToRelative(-720f) 111 + quadToRelative(0f, -33f, 23.5f, -56.5f) 112 + reflectiveQuadTo(160f, 80f) 113 + horizontalLineToRelative(640f) 114 + quadToRelative(33f, 0f, 56.5f, 23.5f) 115 + reflectiveQuadTo(880f, 160f) 116 + verticalLineToRelative(480f) 117 + quadToRelative(0f, 33f, -23.5f, 56.5f) 118 + reflectiveQuadTo(800f, 720f) 119 + horizontalLineTo(240f) 120 + close() 121 + moveToRelative(126f, -240f) 122 + horizontalLineToRelative(594f) 123 + verticalLineToRelative(-480f) 124 + horizontalLineTo(160f) 125 + verticalLineToRelative(525f) 126 + close() 127 + moveToRelative(-46f, 0f) 128 + verticalLineToRelative(-480f) 129 + close() 130 + } 131 + }.build() 132 + 133 + return _Chat_bubble!! 134 + } 135 + 136 + private var _Chat_bubble: ImageVector? = null 137 + 138 + val Reload: ImageVector 139 + get() { 140 + if (_Reload != null) return _Reload!! 141 + 142 + _Reload = ImageVector.Builder( 143 + name = "Reload", 144 + defaultWidth = 15.dp, 145 + defaultHeight = 15.dp, 146 + viewportWidth = 15f, 147 + viewportHeight = 15f 148 + ).apply { 149 + path( 150 + fill = SolidColor(Color.Black), 151 + pathFillType = PathFillType.EvenOdd 152 + ) { 153 + moveTo(1.84998f, 7.49998f) 154 + curveTo(1.84998f, 4.66458f, 4.05979f, 1.84998f, 7.49998f, 1.84998f) 155 + curveTo(10.2783f, 1.84998f, 11.6515f, 3.9064f, 12.2367f, 5f) 156 + horizontalLineTo(10.5f) 157 + curveTo(10.2239f, 5f, 10f, 5.22386f, 10f, 5.5f) 158 + curveTo(10f, 5.77614f, 10.2239f, 6f, 10.5f, 6f) 159 + horizontalLineTo(13.5f) 160 + curveTo(13.7761f, 6f, 14f, 5.77614f, 14f, 5.5f) 161 + verticalLineTo(2.5f) 162 + curveTo(14f, 2.22386f, 13.7761f, 2f, 13.5f, 2f) 163 + curveTo(13.2239f, 2f, 13f, 2.22386f, 13f, 2.5f) 164 + verticalLineTo(4.31318f) 165 + curveTo(12.2955f, 3.07126f, 10.6659f, 0.849976f, 7.49998f, 0.849976f) 166 + curveTo(3.43716f, 0.849976f, 0.849976f, 4.18537f, 0.849976f, 7.49998f) 167 + curveTo(0.849976f, 10.8146f, 3.43716f, 14.15f, 7.49998f, 14.15f) 168 + curveTo(9.44382f, 14.15f, 11.0622f, 13.3808f, 12.2145f, 12.2084f) 169 + curveTo(12.8315f, 11.5806f, 13.3133f, 10.839f, 13.6418f, 10.0407f) 170 + curveTo(13.7469f, 9.78536f, 13.6251f, 9.49315f, 13.3698f, 9.38806f) 171 + curveTo(13.1144f, 9.28296f, 12.8222f, 9.40478f, 12.7171f, 9.66014f) 172 + curveTo(12.4363f, 10.3425f, 12.0251f, 10.9745f, 11.5013f, 11.5074f) 173 + curveTo(10.5295f, 12.4963f, 9.16504f, 13.15f, 7.49998f, 13.15f) 174 + curveTo(4.05979f, 13.15f, 1.84998f, 10.3354f, 1.84998f, 7.49998f) 175 + close() 176 + } 177 + }.build() 178 + 179 + return _Reload!! 180 + } 181 + 182 + private var _Reload: ImageVector? = null 183 +
+106
app/src/main/java/industries/geesawra/jerryno/datalayer/Bluesky.kt
··· 1 + package industries.geesawra.jerryno.datalayer 2 + 3 + import android.util.Log 4 + import androidx.lifecycle.viewModelScope 5 + import app.bsky.feed.GetTimelineQueryParams 6 + import app.bsky.feed.GetTimelineResponse 7 + import app.bsky.feed.Post 8 + import com.atproto.repo.CreateRecordRequest 9 + import com.atproto.server.CreateSessionRequest 10 + import com.atproto.server.CreateSessionResponse 11 + import io.ktor.client.HttpClient 12 + import io.ktor.client.engine.okhttp.OkHttp 13 + import io.ktor.client.plugins.HttpTimeout 14 + import io.ktor.client.plugins.defaultRequest 15 + import kotlinx.coroutines.launch 16 + import kotlinx.datetime.Clock 17 + import sh.christian.ozone.BlueskyJson 18 + import sh.christian.ozone.XrpcBlueskyApi 19 + import sh.christian.ozone.api.AuthenticatedXrpcBlueskyApi 20 + import sh.christian.ozone.api.BlueskyAuthPlugin 21 + import sh.christian.ozone.api.Nsid 22 + import sh.christian.ozone.api.model.JsonContent.Companion.encodeAsJsonContent 23 + import sh.christian.ozone.api.response.AtpResponse 24 + import javax.inject.Inject 25 + 26 + data class Client( 27 + var client: AuthenticatedXrpcBlueskyApi, 28 + var session: CreateSessionResponse 29 + ) 30 + 31 + private class BlueskyConn() { 32 + var client: AuthenticatedXrpcBlueskyApi? = null 33 + var session: CreateSessionResponse? = null 34 + 35 + suspend fun create(pdsURL: String, handle: String, password: String): Result<Client> { 36 + return runCatching { 37 + if (client != null && session != null) { 38 + return Result.success(Client(client!!, session!!)) 39 + } 40 + 41 + val httpClient = HttpClient(OkHttp) { 42 + defaultRequest { 43 + url(pdsURL) 44 + } 45 + install(HttpTimeout) { 46 + requestTimeoutMillis = 15000 47 + connectTimeoutMillis = 15000 48 + socketTimeoutMillis = 15000 49 + } 50 + } 51 + 52 + val client = XrpcBlueskyApi(httpClient) 53 + 54 + val s = client.createSession(CreateSessionRequest(handle, password)) 55 + when (s) { 56 + is AtpResponse.Failure<*> -> Result.failure<Exception>(Exception("Failed to create session: ${s.error}")) 57 + is AtpResponse.Success<CreateSessionResponse> -> { 58 + val tokens = BlueskyAuthPlugin.Tokens(s.response.accessJwt, s.response.refreshJwt) 59 + val authClient = AuthenticatedXrpcBlueskyApi(httpClient, tokens) 60 + this.client = authClient 61 + session = s.response 62 + } 63 + } 64 + 65 + return Result.success(Client(this.client!!, session!!)) 66 + } 67 + } 68 + } 69 + 70 + class Bluesky(val pdsURL: String, val handle: String, val password: String) { 71 + private val conn = BlueskyConn() 72 + 73 + suspend fun fetchTimeline(): Result<GetTimelineResponse> { 74 + return runCatching { 75 + val conn = conn.create(pdsURL, handle, password).getOrThrow() 76 + 77 + val timeline = conn.client.getTimeline(GetTimelineQueryParams()); 78 + val feed = when (timeline) { 79 + is AtpResponse.Failure<*> -> { 80 + return Result.failure(Exception("Failed to fetch timeline: ${timeline.error}")) 81 + } 82 + 83 + is AtpResponse.Success<GetTimelineResponse> -> timeline.response 84 + }; 85 + 86 + return Result.success(feed) 87 + } 88 + } 89 + 90 + suspend fun post(content: String){ 91 + val conn = conn.create(pdsURL, handle, password).getOrThrow() 92 + 93 + val r = BlueskyJson.encodeAsJsonContent(Post( 94 + text = content, 95 + createdAt = Clock.System.now() 96 + )) 97 + val resp = conn.client.createRecord( 98 + CreateRecordRequest( 99 + repo = conn.session.handle, 100 + collection = Nsid("app.bsky.feed.post"), 101 + record = r, 102 + ) 103 + ) // TODO: finish 104 + 105 + } 106 + }
+11
app/src/main/java/industries/geesawra/jerryno/ui/theme/Color.kt
··· 1 + package industries.geesawra.jerryno.ui.theme 2 + 3 + import androidx.compose.ui.graphics.Color 4 + 5 + val Purple80 = Color(0xFFD0BCFF) 6 + val PurpleGrey80 = Color(0xFFCCC2DC) 7 + val Pink80 = Color(0xFFEFB8C8) 8 + 9 + val Purple40 = Color(0xFF6650a4) 10 + val PurpleGrey40 = Color(0xFF625b71) 11 + val Pink40 = Color(0xFF7D5260)
+58
app/src/main/java/industries/geesawra/jerryno/ui/theme/Theme.kt
··· 1 + package industries.geesawra.jerryno.ui.theme 2 + 3 + import android.app.Activity 4 + import android.os.Build 5 + import androidx.compose.foundation.isSystemInDarkTheme 6 + import androidx.compose.material3.MaterialTheme 7 + import androidx.compose.material3.darkColorScheme 8 + import androidx.compose.material3.dynamicDarkColorScheme 9 + import androidx.compose.material3.dynamicLightColorScheme 10 + import androidx.compose.material3.lightColorScheme 11 + import androidx.compose.runtime.Composable 12 + import androidx.compose.ui.platform.LocalContext 13 + 14 + private val DarkColorScheme = darkColorScheme( 15 + primary = Purple80, 16 + secondary = PurpleGrey80, 17 + tertiary = Pink80 18 + ) 19 + 20 + private val LightColorScheme = lightColorScheme( 21 + primary = Purple40, 22 + secondary = PurpleGrey40, 23 + tertiary = Pink40 24 + 25 + /* Other default colors to override 26 + background = Color(0xFFFFFBFE), 27 + surface = Color(0xFFFFFBFE), 28 + onPrimary = Color.White, 29 + onSecondary = Color.White, 30 + onTertiary = Color.White, 31 + onBackground = Color(0xFF1C1B1F), 32 + onSurface = Color(0xFF1C1B1F), 33 + */ 34 + ) 35 + 36 + @Composable 37 + fun JerryNoTheme( 38 + darkTheme: Boolean = isSystemInDarkTheme(), 39 + // Dynamic color is available on Android 12+ 40 + dynamicColor: Boolean = true, 41 + content: @Composable () -> Unit 42 + ) { 43 + val colorScheme = when { 44 + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 45 + val context = LocalContext.current 46 + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 47 + } 48 + 49 + darkTheme -> DarkColorScheme 50 + else -> LightColorScheme 51 + } 52 + 53 + MaterialTheme( 54 + colorScheme = colorScheme, 55 + typography = Typography, 56 + content = content 57 + ) 58 + }
+34
app/src/main/java/industries/geesawra/jerryno/ui/theme/Type.kt
··· 1 + package industries.geesawra.jerryno.ui.theme 2 + 3 + import androidx.compose.material3.Typography 4 + import androidx.compose.ui.text.TextStyle 5 + import androidx.compose.ui.text.font.FontFamily 6 + import androidx.compose.ui.text.font.FontWeight 7 + import androidx.compose.ui.unit.sp 8 + 9 + // Set of Material typography styles to start with 10 + val Typography = Typography( 11 + bodyLarge = TextStyle( 12 + fontFamily = FontFamily.Default, 13 + fontWeight = FontWeight.Normal, 14 + fontSize = 16.sp, 15 + lineHeight = 24.sp, 16 + letterSpacing = 0.5.sp 17 + ) 18 + /* Other default text styles to override 19 + titleLarge = TextStyle( 20 + fontFamily = FontFamily.Default, 21 + fontWeight = FontWeight.Normal, 22 + fontSize = 22.sp, 23 + lineHeight = 28.sp, 24 + letterSpacing = 0.sp 25 + ), 26 + labelSmall = TextStyle( 27 + fontFamily = FontFamily.Default, 28 + fontWeight = FontWeight.Medium, 29 + fontSize = 11.sp, 30 + lineHeight = 16.sp, 31 + letterSpacing = 0.5.sp 32 + ) 33 + */ 34 + )
+170
app/src/main/res/drawable/ic_launcher_background.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <vector xmlns:android="http://schemas.android.com/apk/res/android" 3 + android:width="108dp" 4 + android:height="108dp" 5 + android:viewportWidth="108" 6 + android:viewportHeight="108"> 7 + <path 8 + android:fillColor="#3DDC84" 9 + android:pathData="M0,0h108v108h-108z" /> 10 + <path 11 + android:fillColor="#00000000" 12 + android:pathData="M9,0L9,108" 13 + android:strokeWidth="0.8" 14 + android:strokeColor="#33FFFFFF" /> 15 + <path 16 + android:fillColor="#00000000" 17 + android:pathData="M19,0L19,108" 18 + android:strokeWidth="0.8" 19 + android:strokeColor="#33FFFFFF" /> 20 + <path 21 + android:fillColor="#00000000" 22 + android:pathData="M29,0L29,108" 23 + android:strokeWidth="0.8" 24 + android:strokeColor="#33FFFFFF" /> 25 + <path 26 + android:fillColor="#00000000" 27 + android:pathData="M39,0L39,108" 28 + android:strokeWidth="0.8" 29 + android:strokeColor="#33FFFFFF" /> 30 + <path 31 + android:fillColor="#00000000" 32 + android:pathData="M49,0L49,108" 33 + android:strokeWidth="0.8" 34 + android:strokeColor="#33FFFFFF" /> 35 + <path 36 + android:fillColor="#00000000" 37 + android:pathData="M59,0L59,108" 38 + android:strokeWidth="0.8" 39 + android:strokeColor="#33FFFFFF" /> 40 + <path 41 + android:fillColor="#00000000" 42 + android:pathData="M69,0L69,108" 43 + android:strokeWidth="0.8" 44 + android:strokeColor="#33FFFFFF" /> 45 + <path 46 + android:fillColor="#00000000" 47 + android:pathData="M79,0L79,108" 48 + android:strokeWidth="0.8" 49 + android:strokeColor="#33FFFFFF" /> 50 + <path 51 + android:fillColor="#00000000" 52 + android:pathData="M89,0L89,108" 53 + android:strokeWidth="0.8" 54 + android:strokeColor="#33FFFFFF" /> 55 + <path 56 + android:fillColor="#00000000" 57 + android:pathData="M99,0L99,108" 58 + android:strokeWidth="0.8" 59 + android:strokeColor="#33FFFFFF" /> 60 + <path 61 + android:fillColor="#00000000" 62 + android:pathData="M0,9L108,9" 63 + android:strokeWidth="0.8" 64 + android:strokeColor="#33FFFFFF" /> 65 + <path 66 + android:fillColor="#00000000" 67 + android:pathData="M0,19L108,19" 68 + android:strokeWidth="0.8" 69 + android:strokeColor="#33FFFFFF" /> 70 + <path 71 + android:fillColor="#00000000" 72 + android:pathData="M0,29L108,29" 73 + android:strokeWidth="0.8" 74 + android:strokeColor="#33FFFFFF" /> 75 + <path 76 + android:fillColor="#00000000" 77 + android:pathData="M0,39L108,39" 78 + android:strokeWidth="0.8" 79 + android:strokeColor="#33FFFFFF" /> 80 + <path 81 + android:fillColor="#00000000" 82 + android:pathData="M0,49L108,49" 83 + android:strokeWidth="0.8" 84 + android:strokeColor="#33FFFFFF" /> 85 + <path 86 + android:fillColor="#00000000" 87 + android:pathData="M0,59L108,59" 88 + android:strokeWidth="0.8" 89 + android:strokeColor="#33FFFFFF" /> 90 + <path 91 + android:fillColor="#00000000" 92 + android:pathData="M0,69L108,69" 93 + android:strokeWidth="0.8" 94 + android:strokeColor="#33FFFFFF" /> 95 + <path 96 + android:fillColor="#00000000" 97 + android:pathData="M0,79L108,79" 98 + android:strokeWidth="0.8" 99 + android:strokeColor="#33FFFFFF" /> 100 + <path 101 + android:fillColor="#00000000" 102 + android:pathData="M0,89L108,89" 103 + android:strokeWidth="0.8" 104 + android:strokeColor="#33FFFFFF" /> 105 + <path 106 + android:fillColor="#00000000" 107 + android:pathData="M0,99L108,99" 108 + android:strokeWidth="0.8" 109 + android:strokeColor="#33FFFFFF" /> 110 + <path 111 + android:fillColor="#00000000" 112 + android:pathData="M19,29L89,29" 113 + android:strokeWidth="0.8" 114 + android:strokeColor="#33FFFFFF" /> 115 + <path 116 + android:fillColor="#00000000" 117 + android:pathData="M19,39L89,39" 118 + android:strokeWidth="0.8" 119 + android:strokeColor="#33FFFFFF" /> 120 + <path 121 + android:fillColor="#00000000" 122 + android:pathData="M19,49L89,49" 123 + android:strokeWidth="0.8" 124 + android:strokeColor="#33FFFFFF" /> 125 + <path 126 + android:fillColor="#00000000" 127 + android:pathData="M19,59L89,59" 128 + android:strokeWidth="0.8" 129 + android:strokeColor="#33FFFFFF" /> 130 + <path 131 + android:fillColor="#00000000" 132 + android:pathData="M19,69L89,69" 133 + android:strokeWidth="0.8" 134 + android:strokeColor="#33FFFFFF" /> 135 + <path 136 + android:fillColor="#00000000" 137 + android:pathData="M19,79L89,79" 138 + android:strokeWidth="0.8" 139 + android:strokeColor="#33FFFFFF" /> 140 + <path 141 + android:fillColor="#00000000" 142 + android:pathData="M29,19L29,89" 143 + android:strokeWidth="0.8" 144 + android:strokeColor="#33FFFFFF" /> 145 + <path 146 + android:fillColor="#00000000" 147 + android:pathData="M39,19L39,89" 148 + android:strokeWidth="0.8" 149 + android:strokeColor="#33FFFFFF" /> 150 + <path 151 + android:fillColor="#00000000" 152 + android:pathData="M49,19L49,89" 153 + android:strokeWidth="0.8" 154 + android:strokeColor="#33FFFFFF" /> 155 + <path 156 + android:fillColor="#00000000" 157 + android:pathData="M59,19L59,89" 158 + android:strokeWidth="0.8" 159 + android:strokeColor="#33FFFFFF" /> 160 + <path 161 + android:fillColor="#00000000" 162 + android:pathData="M69,19L69,89" 163 + android:strokeWidth="0.8" 164 + android:strokeColor="#33FFFFFF" /> 165 + <path 166 + android:fillColor="#00000000" 167 + android:pathData="M79,19L79,89" 168 + android:strokeWidth="0.8" 169 + android:strokeColor="#33FFFFFF" /> 170 + </vector>
+30
app/src/main/res/drawable/ic_launcher_foreground.xml
··· 1 + <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 + xmlns:aapt="http://schemas.android.com/aapt" 3 + android:width="108dp" 4 + android:height="108dp" 5 + android:viewportWidth="108" 6 + android:viewportHeight="108"> 7 + <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z"> 8 + <aapt:attr name="android:fillColor"> 9 + <gradient 10 + android:endX="85.84757" 11 + android:endY="92.4963" 12 + android:startX="42.9492" 13 + android:startY="49.59793" 14 + android:type="linear"> 15 + <item 16 + android:color="#44000000" 17 + android:offset="0.0" /> 18 + <item 19 + android:color="#00000000" 20 + android:offset="1.0" /> 21 + </gradient> 22 + </aapt:attr> 23 + </path> 24 + <path 25 + android:fillColor="#FFFFFF" 26 + android:fillType="nonZero" 27 + android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z" 28 + android:strokeWidth="1" 29 + android:strokeColor="#00000000" /> 30 + </vector>
+6
app/src/main/res/mipmap-anydpi/ic_launcher.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> 3 + <background android:drawable="@drawable/ic_launcher_background" /> 4 + <foreground android:drawable="@drawable/ic_launcher_foreground" /> 5 + <monochrome android:drawable="@drawable/ic_launcher_foreground" /> 6 + </adaptive-icon>
+6
app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> 3 + <background android:drawable="@drawable/ic_launcher_background" /> 4 + <foreground android:drawable="@drawable/ic_launcher_foreground" /> 5 + <monochrome android:drawable="@drawable/ic_launcher_foreground" /> 6 + </adaptive-icon>
app/src/main/res/mipmap-hdpi/ic_launcher.webp

This is a binary file and will not be displayed.

app/src/main/res/mipmap-hdpi/ic_launcher_round.webp

This is a binary file and will not be displayed.

app/src/main/res/mipmap-mdpi/ic_launcher.webp

This is a binary file and will not be displayed.

app/src/main/res/mipmap-mdpi/ic_launcher_round.webp

This is a binary file and will not be displayed.

app/src/main/res/mipmap-xhdpi/ic_launcher.webp

This is a binary file and will not be displayed.

app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp

This is a binary file and will not be displayed.

app/src/main/res/mipmap-xxhdpi/ic_launcher.webp

This is a binary file and will not be displayed.

app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp

This is a binary file and will not be displayed.

app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp

This is a binary file and will not be displayed.

app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp

This is a binary file and will not be displayed.

+10
app/src/main/res/values/colors.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <resources> 3 + <color name="purple_200">#FFBB86FC</color> 4 + <color name="purple_500">#FF6200EE</color> 5 + <color name="purple_700">#FF3700B3</color> 6 + <color name="teal_200">#FF03DAC5</color> 7 + <color name="teal_700">#FF018786</color> 8 + <color name="black">#FF000000</color> 9 + <color name="white">#FFFFFFFF</color> 10 + </resources>
+5
app/src/main/res/values/strings.xml
··· 1 + <resources> 2 + <string name="app_name">Jerry No</string> 3 + <string name="timeline">Timeline</string> 4 + <string name="notifications">Notifications</string> 5 + </resources>
+5
app/src/main/res/values/themes.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <resources> 3 + 4 + <style name="Theme.JerryNo" parent="android:Theme.Material.Light.NoActionBar" /> 5 + </resources>
+13
app/src/main/res/xml/backup_rules.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?><!-- 2 + Sample backup rules file; uncomment and customize as necessary. 3 + See https://developer.android.com/guide/topics/data/autobackup 4 + for details. 5 + Note: This file is ignored for devices older than API 31 6 + See https://developer.android.com/about/versions/12/backup-restore 7 + --> 8 + <full-backup-content> 9 + <!-- 10 + <include domain="sharedpref" path="."/> 11 + <exclude domain="sharedpref" path="device.xml"/> 12 + --> 13 + </full-backup-content>
+19
app/src/main/res/xml/data_extraction_rules.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?><!-- 2 + Sample data extraction rules file; uncomment and customize as necessary. 3 + See https://developer.android.com/about/versions/12/backup-restore#xml-changes 4 + for details. 5 + --> 6 + <data-extraction-rules> 7 + <cloud-backup> 8 + <!-- TODO: Use <include> and <exclude> to control what is backed up. 9 + <include .../> 10 + <exclude .../> 11 + --> 12 + </cloud-backup> 13 + <!-- 14 + <device-transfer> 15 + <include .../> 16 + <exclude .../> 17 + </device-transfer> 18 + --> 19 + </data-extraction-rules>
+17
app/src/test/java/industries/geesawra/jerryno/ExampleUnitTest.kt
··· 1 + package industries.geesawra.jerryno 2 + 3 + import org.junit.Test 4 + 5 + import org.junit.Assert.* 6 + 7 + /** 8 + * Example local unit test, which will execute on the development machine (host). 9 + * 10 + * See [testing documentation](http://d.android.com/tools/testing). 11 + */ 12 + class ExampleUnitTest { 13 + @Test 14 + fun addition_isCorrect() { 15 + assertEquals(4, 2 + 2) 16 + } 17 + }
+8
build.gradle.kts
··· 1 + // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 + plugins { 3 + alias(libs.plugins.android.application) apply false 4 + alias(libs.plugins.kotlin.android) apply false 5 + alias(libs.plugins.kotlin.compose) apply false 6 + id("com.google.dagger.hilt.android") version "2.57.1" apply false 7 + id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false 8 + }
+23
gradle.properties
··· 1 + # Project-wide Gradle settings. 2 + # IDE (e.g. Android Studio) users: 3 + # Gradle settings configured through the IDE *will override* 4 + # any settings specified in this file. 5 + # For more details on how to configure your build environment visit 6 + # http://www.gradle.org/docs/current/userguide/build_environment.html 7 + # Specifies the JVM arguments used for the daemon process. 8 + # The setting is particularly useful for tweaking memory settings. 9 + org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 + # When configured, Gradle will run in incubating parallel mode. 11 + # This option should only be used with decoupled projects. For more details, visit 12 + # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 13 + # org.gradle.parallel=true 14 + # AndroidX package structure to make it clearer which packages are bundled with the 15 + # Android operating system, and which are packaged with your app's APK 16 + # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 + android.useAndroidX=true 18 + # Kotlin code style for this project: "official" or "obsolete": 19 + kotlin.code.style=official 20 + # Enables namespacing of each library's R class so that its R class includes only the 21 + # resources declared in the library itself and none from the library's dependencies, 22 + # thereby reducing the size of the R class for that library 23 + android.nonTransitiveRClass=true
+32
gradle/libs.versions.toml
··· 1 + [versions] 2 + agp = "8.13.0" 3 + kotlin = "2.0.21" 4 + coreKtx = "1.10.1" 5 + junit = "4.13.2" 6 + junitVersion = "1.1.5" 7 + espressoCore = "3.5.1" 8 + lifecycleRuntimeKtx = "2.6.1" 9 + activityCompose = "1.8.0" 10 + composeBom = "2024.09.00" 11 + 12 + [libraries] 13 + androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 14 + junit = { group = "junit", name = "junit", version.ref = "junit" } 15 + androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } 16 + androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } 17 + androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } 18 + androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } 19 + androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } 20 + androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } 21 + androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } 22 + androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } 23 + androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 24 + androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } 25 + androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } 26 + androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } 27 + 28 + [plugins] 29 + android-application = { id = "com.android.application", version.ref = "agp" } 30 + kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 31 + kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 32 +
gradle/wrapper/gradle-wrapper.jar

This is a binary file and will not be displayed.

+6
gradle/wrapper/gradle-wrapper.properties
··· 1 + #Wed Sep 10 18:31:37 CEST 2025 2 + distributionBase=GRADLE_USER_HOME 3 + distributionPath=wrapper/dists 4 + distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 5 + zipStoreBase=GRADLE_USER_HOME 6 + zipStorePath=wrapper/dists
+185
gradlew
··· 1 + #!/usr/bin/env sh 2 + 3 + # 4 + # Copyright 2015 the original author or authors. 5 + # 6 + # Licensed under the Apache License, Version 2.0 (the "License"); 7 + # you may not use this file except in compliance with the License. 8 + # You may obtain a copy of the License at 9 + # 10 + # https://www.apache.org/licenses/LICENSE-2.0 11 + # 12 + # Unless required by applicable law or agreed to in writing, software 13 + # distributed under the License is distributed on an "AS IS" BASIS, 14 + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + # See the License for the specific language governing permissions and 16 + # limitations under the License. 17 + # 18 + 19 + ############################################################################## 20 + ## 21 + ## Gradle start up script for UN*X 22 + ## 23 + ############################################################################## 24 + 25 + # Attempt to set APP_HOME 26 + # Resolve links: $0 may be a link 27 + PRG="$0" 28 + # Need this for relative symlinks. 29 + while [ -h "$PRG" ] ; do 30 + ls=`ls -ld "$PRG"` 31 + link=`expr "$ls" : '.*-> \(.*\)$'` 32 + if expr "$link" : '/.*' > /dev/null; then 33 + PRG="$link" 34 + else 35 + PRG=`dirname "$PRG"`"/$link" 36 + fi 37 + done 38 + SAVED="`pwd`" 39 + cd "`dirname \"$PRG\"`/" >/dev/null 40 + APP_HOME="`pwd -P`" 41 + cd "$SAVED" >/dev/null 42 + 43 + APP_NAME="Gradle" 44 + APP_BASE_NAME=`basename "$0"` 45 + 46 + # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 + DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 + 49 + # Use the maximum available, or set MAX_FD != -1 to use that value. 50 + MAX_FD="maximum" 51 + 52 + warn () { 53 + echo "$*" 54 + } 55 + 56 + die () { 57 + echo 58 + echo "$*" 59 + echo 60 + exit 1 61 + } 62 + 63 + # OS specific support (must be 'true' or 'false'). 64 + cygwin=false 65 + msys=false 66 + darwin=false 67 + nonstop=false 68 + case "`uname`" in 69 + CYGWIN* ) 70 + cygwin=true 71 + ;; 72 + Darwin* ) 73 + darwin=true 74 + ;; 75 + MINGW* ) 76 + msys=true 77 + ;; 78 + NONSTOP* ) 79 + nonstop=true 80 + ;; 81 + esac 82 + 83 + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 + 85 + 86 + # Determine the Java command to use to start the JVM. 87 + if [ -n "$JAVA_HOME" ] ; then 88 + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 + # IBM's JDK on AIX uses strange locations for the executables 90 + JAVACMD="$JAVA_HOME/jre/sh/java" 91 + else 92 + JAVACMD="$JAVA_HOME/bin/java" 93 + fi 94 + if [ ! -x "$JAVACMD" ] ; then 95 + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 + 97 + Please set the JAVA_HOME variable in your environment to match the 98 + location of your Java installation." 99 + fi 100 + else 101 + JAVACMD="java" 102 + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 + 104 + Please set the JAVA_HOME variable in your environment to match the 105 + location of your Java installation." 106 + fi 107 + 108 + # Increase the maximum file descriptors if we can. 109 + if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 + MAX_FD_LIMIT=`ulimit -H -n` 111 + if [ $? -eq 0 ] ; then 112 + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 + MAX_FD="$MAX_FD_LIMIT" 114 + fi 115 + ulimit -n $MAX_FD 116 + if [ $? -ne 0 ] ; then 117 + warn "Could not set maximum file descriptor limit: $MAX_FD" 118 + fi 119 + else 120 + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 + fi 122 + fi 123 + 124 + # For Darwin, add options to specify how the application appears in the dock 125 + if $darwin; then 126 + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 + fi 128 + 129 + # For Cygwin or MSYS, switch paths to Windows format before running java 130 + if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 + APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 + 134 + JAVACMD=`cygpath --unix "$JAVACMD"` 135 + 136 + # We build the pattern for arguments to be converted via cygpath 137 + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 + SEP="" 139 + for dir in $ROOTDIRSRAW ; do 140 + ROOTDIRS="$ROOTDIRS$SEP$dir" 141 + SEP="|" 142 + done 143 + OURCYGPATTERN="(^($ROOTDIRS))" 144 + # Add a user-defined pattern to the cygpath arguments 145 + if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 + fi 148 + # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 + i=0 150 + for arg in "$@" ; do 151 + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 + 154 + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 + else 157 + eval `echo args$i`="\"$arg\"" 158 + fi 159 + i=`expr $i + 1` 160 + done 161 + case $i in 162 + 0) set -- ;; 163 + 1) set -- "$args0" ;; 164 + 2) set -- "$args0" "$args1" ;; 165 + 3) set -- "$args0" "$args1" "$args2" ;; 166 + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 + esac 173 + fi 174 + 175 + # Escape application args 176 + save () { 177 + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 + echo " " 179 + } 180 + APP_ARGS=`save "$@"` 181 + 182 + # Collect all arguments for the java command, following the shell quoting and substitution rules 183 + eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 + 185 + exec "$JAVACMD" "$@"
+89
gradlew.bat
··· 1 + @rem 2 + @rem Copyright 2015 the original author or authors. 3 + @rem 4 + @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 + @rem you may not use this file except in compliance with the License. 6 + @rem You may obtain a copy of the License at 7 + @rem 8 + @rem https://www.apache.org/licenses/LICENSE-2.0 9 + @rem 10 + @rem Unless required by applicable law or agreed to in writing, software 11 + @rem distributed under the License is distributed on an "AS IS" BASIS, 12 + @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 + @rem See the License for the specific language governing permissions and 14 + @rem limitations under the License. 15 + @rem 16 + 17 + @if "%DEBUG%" == "" @echo off 18 + @rem ########################################################################## 19 + @rem 20 + @rem Gradle startup script for Windows 21 + @rem 22 + @rem ########################################################################## 23 + 24 + @rem Set local scope for the variables with windows NT shell 25 + if "%OS%"=="Windows_NT" setlocal 26 + 27 + set DIRNAME=%~dp0 28 + if "%DIRNAME%" == "" set DIRNAME=. 29 + set APP_BASE_NAME=%~n0 30 + set APP_HOME=%DIRNAME% 31 + 32 + @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 + for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 + 35 + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 + set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 + 38 + @rem Find java.exe 39 + if defined JAVA_HOME goto findJavaFromJavaHome 40 + 41 + set JAVA_EXE=java.exe 42 + %JAVA_EXE% -version >NUL 2>&1 43 + if "%ERRORLEVEL%" == "0" goto execute 44 + 45 + echo. 46 + echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 + echo. 48 + echo Please set the JAVA_HOME variable in your environment to match the 49 + echo location of your Java installation. 50 + 51 + goto fail 52 + 53 + :findJavaFromJavaHome 54 + set JAVA_HOME=%JAVA_HOME:"=% 55 + set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 + 57 + if exist "%JAVA_EXE%" goto execute 58 + 59 + echo. 60 + echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 + echo. 62 + echo Please set the JAVA_HOME variable in your environment to match the 63 + echo location of your Java installation. 64 + 65 + goto fail 66 + 67 + :execute 68 + @rem Setup the command line 69 + 70 + set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 + 72 + 73 + @rem Execute Gradle 74 + "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 + 76 + :end 77 + @rem End local scope for the variables with windows NT shell 78 + if "%ERRORLEVEL%"=="0" goto mainEnd 79 + 80 + :fail 81 + rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 + rem the _cmd.exe /c_ return code! 83 + if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 + exit /b 1 85 + 86 + :mainEnd 87 + if "%OS%"=="Windows_NT" endlocal 88 + 89 + :omega
+23
settings.gradle.kts
··· 1 + pluginManagement { 2 + repositories { 3 + google { 4 + content { 5 + includeGroupByRegex("com\\.android.*") 6 + includeGroupByRegex("com\\.google.*") 7 + includeGroupByRegex("androidx.*") 8 + } 9 + } 10 + mavenCentral() 11 + gradlePluginPortal() 12 + } 13 + } 14 + dependencyResolutionManagement { 15 + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 16 + repositories { 17 + google() 18 + mavenCentral() 19 + } 20 + } 21 + 22 + rootProject.name = "Jerry No" 23 + include(":app")