[
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor.xml\n/.idea/assetWizardSettings.xml\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n.cxx\nlocal.properties\n"
  },
  {
    "path": ".idea/.gitignore",
    "content": "# Default ignored files\n/shelf/\n/workspace.xml\n"
  },
  {
    "path": ".idea/.name",
    "content": "Pineapple"
  },
  {
    "path": ".idea/AndroidProjectSystem.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"AndroidProjectSystem\">\n    <option name=\"providerId\" value=\"com.android.tools.idea.GradleProjectSystem\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/codeStyles/Project.xml",
    "content": "<component name=\"ProjectCodeStyleConfiguration\">\n  <code_scheme name=\"Project\" version=\"173\">\n    <JetCodeStyleSettings>\n      <option name=\"CODE_STYLE_DEFAULTS\" value=\"KOTLIN_OFFICIAL\" />\n    </JetCodeStyleSettings>\n    <codeStyleSettings language=\"XML\">\n      <option name=\"FORCE_REARRANGE_MODE\" value=\"1\" />\n      <indentOptions>\n        <option name=\"CONTINUATION_INDENT_SIZE\" value=\"4\" />\n      </indentOptions>\n      <arrangement>\n        <rules>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>xmlns:android</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>xmlns:.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*:id</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*:name</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>name</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>style</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>ANDROID_ATTRIBUTE_ORDER</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>.*</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n        </rules>\n      </arrangement>\n    </codeStyleSettings>\n    <codeStyleSettings language=\"kotlin\">\n      <option name=\"CODE_STYLE_DEFAULTS\" value=\"KOTLIN_OFFICIAL\" />\n    </codeStyleSettings>\n  </code_scheme>\n</component>"
  },
  {
    "path": ".idea/codeStyles/codeStyleConfig.xml",
    "content": "<component name=\"ProjectCodeStyleConfiguration\">\n  <state>\n    <option name=\"USE_PER_PROJECT_SETTINGS\" value=\"true\" />\n  </state>\n</component>"
  },
  {
    "path": ".idea/compiler.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"CompilerConfiguration\">\n    <bytecodeTargetLevel target=\"21\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/copilot.data.migration.agent.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"AgentMigrationStateService\">\n    <option name=\"migrationStatus\" value=\"COMPLETED\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/copilot.data.migration.ask2agent.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"Ask2AgentMigrationStateService\">\n    <option name=\"migrationStatus\" value=\"COMPLETED\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/copilot.data.migration.edit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"EditMigrationStateService\">\n    <option name=\"migrationStatus\" value=\"COMPLETED\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/deploymentTargetSelector.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"deploymentTargetSelector\">\n    <selectionStates>\n      <SelectionState runConfigName=\"app\">\n        <option name=\"selectionMode\" value=\"DROPDOWN\" />\n      </SelectionState>\n    </selectionStates>\n  </component>\n</project>"
  },
  {
    "path": ".idea/gradle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"GradleMigrationSettings\" migrationVersion=\"1\" />\n  <component name=\"GradleSettings\">\n    <option name=\"linkedExternalProjectsSettings\">\n      <GradleProjectSettings>\n        <option name=\"testRunner\" value=\"CHOOSE_PER_TEST\" />\n        <option name=\"externalProjectPath\" value=\"$PROJECT_DIR$\" />\n        <option name=\"gradleJvm\" value=\"#GRADLE_LOCAL_JAVA_HOME\" />\n        <option name=\"modules\">\n          <set>\n            <option value=\"$PROJECT_DIR$\" />\n            <option value=\"$PROJECT_DIR$/app\" />\n          </set>\n        </option>\n      </GradleProjectSettings>\n    </option>\n  </component>\n</project>"
  },
  {
    "path": ".idea/inspectionProfiles/Project_Default.xml",
    "content": "<component name=\"InspectionProjectProfileManager\">\n  <profile version=\"1.0\">\n    <option name=\"myName\" value=\"Project Default\" />\n    <inspection_tool class=\"ComposePreviewDimensionRespectsLimit\" enabled=\"true\" level=\"WARNING\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"ComposePreviewMustBeTopLevelFunction\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"ComposePreviewNeedsComposableAnnotation\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"ComposePreviewNotSupportedInUnitTestFiles\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"GlancePreviewDimensionRespectsLimit\" enabled=\"true\" level=\"WARNING\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"GlancePreviewMustBeTopLevelFunction\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"GlancePreviewNeedsComposableAnnotation\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"GlancePreviewNotSupportedInUnitTestFiles\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"PreviewAnnotationInFunctionWithParameters\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"PreviewApiLevelMustBeValid\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"PreviewDeviceShouldUseNewSpec\" enabled=\"true\" level=\"WEAK WARNING\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"PreviewFontScaleMustBeGreaterThanZero\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"PreviewMultipleParameterProviders\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"PreviewParameterProviderOnFirstParameter\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n    <inspection_tool class=\"PreviewPickerAnnotation\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\">\n      <option name=\"composableFile\" value=\"true\" />\n    </inspection_tool>\n  </profile>\n</component>"
  },
  {
    "path": ".idea/kotlinc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"KotlinJpsPluginSettings\">\n    <option name=\"version\" value=\"2.0.21\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/migrations.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectMigrations\">\n    <option name=\"MigrateToGradleLocalJavaHome\">\n      <set>\n        <option value=\"$PROJECT_DIR$\" />\n      </set>\n    </option>\n  </component>\n</project>"
  },
  {
    "path": ".idea/misc.xml",
    "content": "<project version=\"4\">\n  <component name=\"ExternalStorageConfigurationManager\" enabled=\"true\" />\n  <component name=\"ProjectRootManager\" version=\"2\" languageLevel=\"JDK_21\" default=\"true\" project-jdk-name=\"jbr-21\" project-jdk-type=\"JavaSDK\">\n    <output url=\"file://$PROJECT_DIR$/build/classes\" />\n  </component>\n  <component name=\"ProjectType\">\n    <option name=\"id\" value=\"Android\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/runConfigurations.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"RunConfigurationProducerService\">\n    <option name=\"ignoredProducers\">\n      <set>\n        <option value=\"com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer\" />\n        <option value=\"com.intellij.execution.junit.AllInPackageConfigurationProducer\" />\n        <option value=\"com.intellij.execution.junit.PatternConfigurationProducer\" />\n        <option value=\"com.intellij.execution.junit.TestInClassConfigurationProducer\" />\n        <option value=\"com.intellij.execution.junit.UniqueIdConfigurationProducer\" />\n        <option value=\"com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer\" />\n        <option value=\"org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer\" />\n        <option value=\"org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer\" />\n      </set>\n    </option>\n  </component>\n</project>"
  },
  {
    "path": ".idea/vcs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping directory=\"\" vcs=\"Git\" />\n  </component>\n</project>"
  },
  {
    "path": "README.md",
    "content": "# Pineapple 🍍\n\nPineapple is an Android reddit client application developed with Jetpack Compose following the Material 3 Expresive design guidelines with Dynamic Color and a clean architecture approach. The application is designed to be free to use, with each user supplying their own created Reddit key that remains on-device to avoid the new API pricing restrictions. \n\n<div>\n  <img src=\"media/onboard.png\" width=19% />\n  <img src=\"media/clientid.png\" width=19% />\n  <img src=\"media/home.png\" width=19% />\n  <img src=\"media/filters.png\" width=19% />\n  <img src=\"media/comments.png\" width=19% />\n\n</div>\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build"
  },
  {
    "path": "app/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.android.application)\n    alias(libs.plugins.kotlin.android)\n    alias(libs.plugins.kotlin.compose)\n    alias(libs.plugins.ksp)\n    alias(libs.plugins.hilt.android)\n}\n\nandroid {\n    namespace = \"com.pineapple.app\"\n    compileSdk = 36\n\n    defaultConfig {\n        applicationId = \"com.pineapple.app\"\n        minSdk = 26\n        targetSdk = 36\n        versionCode = 1\n        versionName = \"1.0\"\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n    }\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_11\n        targetCompatibility = JavaVersion.VERSION_11\n    }\n    kotlinOptions {\n        jvmTarget = \"11\"\n    }\n    buildFeatures {\n        compose = true\n    }\n}\n\ndependencies {\n\n    implementation(libs.androidx.core.ktx)\n    implementation(libs.androidx.lifecycle.runtime.ktx)\n    implementation(libs.androidx.activity.compose)\n    implementation(platform(libs.androidx.compose.bom))\n    implementation(libs.androidx.ui)\n    implementation(libs.androidx.ui.graphics)\n    implementation(libs.androidx.ui.tooling.preview)\n    implementation(libs.androidx.material3)\n    implementation(libs.androidx.navigation.compose)\n    implementation(libs.mmkv)\n\n    implementation(libs.retrofit)\n    implementation(libs.converter.gson)\n    implementation(libs.okhttp)\n    implementation(libs.logging.interceptor)\n\n    implementation(libs.hilt.android)\n    ksp(libs.hilt.android.compiler)\n    implementation(libs.androidx.hilt.navigation.compose)\n\n    implementation(libs.coil.compose)\n    implementation(libs.coil.network.okhttp)\n\n    implementation(libs.room.runtime)\n    implementation(libs.room.ktx)\n    implementation(libs.room.paging)\n    ksp(libs.room.compiler)\n\n    implementation(libs.paging.runtime)\n    implementation(libs.paging.compose)\n\n    testImplementation(libs.junit)\n    androidTestImplementation(libs.androidx.junit)\n    androidTestImplementation(libs.androidx.espresso.core)\n    androidTestImplementation(platform(libs.androidx.compose.bom))\n    androidTestImplementation(libs.androidx.ui.test.junit4)\n    debugImplementation(libs.androidx.ui.tooling)\n    debugImplementation(libs.androidx.ui.test.manifest)\n}"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "app/src/androidTest/java/com/pineapple/app/ExampleInstrumentedTest.kt",
    "content": "package com.pineapple.app\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.runners.AndroidJUnit4\n\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\nimport org.junit.Assert.*\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"com.pineapple.app\", appContext.packageName)\n    }\n}"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n\n    <application\n        android:name=\".PineappleApp\"\n        android:allowBackup=\"true\"\n        android:dataExtractionRules=\"@xml/data_extraction_rules\"\n        android:fullBackupContent=\"@xml/backup_rules\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/welcome_app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Theme.Pineapple\"\n        android:enableOnBackInvokedCallback=\"true\"\n        tools:targetApi=\"31\">\n        <activity\n            android:name=\".MainActivity\"\n            android:exported=\"true\"\n            android:theme=\"@style/Theme.Pineapple\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n                <data android:scheme=\"pineapple\"\n                    android:host=\"login\" />\n            </intent-filter>\n        </activity>\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/MainActivity.kt",
    "content": "package com.pineapple.app\n\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.navigation.NavHostController\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.rememberNavController\nimport androidx.navigation.navDeepLink\nimport com.pineapple.app.consts.MMKVKey\nimport com.pineapple.app.consts.NavDestinationKey\nimport com.pineapple.app.network.repository.RedditAuthRepository\nimport com.pineapple.app.network.repository.RedditRepository\nimport com.pineapple.app.ui.theme.PineappleTheme\nimport com.pineapple.app.ui.view.CommunityView\nimport com.pineapple.app.ui.view.HomeView\nimport com.pineapple.app.ui.view.KeyProviderView\nimport com.pineapple.app.ui.view.PostView\nimport com.pineapple.app.ui.view.UserView\nimport com.pineapple.app.ui.view.WelcomeView\nimport com.tencent.mmkv.MMKV\nimport dagger.hilt.android.AndroidEntryPoint\nimport javax.inject.Inject\n\n@AndroidEntryPoint\nclass MainActivity : ComponentActivity() {\n\n    @Inject lateinit var repository: RedditAuthRepository\n    @Inject lateinit var mmkv: MMKV\n    lateinit var navController: NavHostController\n\n    @OptIn(ExperimentalMaterial3ExpressiveApi::class)\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        enableEdgeToEdge()\n\n        setContent {\n\n            navController = rememberNavController()\n\n            PineappleTheme {\n                NavHost(\n                    navController = navController,\n                    startDestination = if (mmkv.decodeBool(MMKVKey.ONBOARDING_COMPLETE)) {\n                        NavDestinationKey.HomeView\n                    } else {\n                        NavDestinationKey.WelcomeView\n                    },\n                    enterTransition = {\n                        scaleIn(initialScale = 0.9f, animationSpec = tween(350)) +\n                                fadeIn(animationSpec = tween(350))\n                    },\n                    exitTransition = {\n                        scaleOut(targetScale = 0.95f, animationSpec = tween(350)) +\n                                fadeOut(animationSpec = tween(350))\n                    }\n                ) {\n                    composable(NavDestinationKey.WelcomeView) {\n                        WelcomeView(navController)\n                    }\n                    composable(\"${NavDestinationKey.KeyProviderView}/{authType}\") { backStackEntry ->\n                        KeyProviderView(\n                            navController = navController,\n                            loginType = backStackEntry.arguments?.getString(\"authType\")!!\n                        )\n                    }\n                    composable(NavDestinationKey.HomeView) {\n                        HomeView(navController)\n                    }\n                    composable(\n                        route = \"${NavDestinationKey.HomeView}/{error}/{code}/{state}\",\n                        deepLinks = listOf(\n                            navDeepLink {\n                                uriPattern = \"pineapple://login?error={error}&code={code}&state={state}\"\n                            }\n                        )\n                    ) {\n                        mmkv.encode(MMKVKey.API_LOGIN_AUTH_CODE, it.arguments?.getString(\"code\"))\n                        mmkv.encode(MMKVKey.ONBOARDING_COMPLETE, true)\n                        LaunchedEffect(Unit) {\n                            repository.authenticateUser()\n                        }\n                        HomeView(navController)\n                    }\n                    composable(\"${NavDestinationKey.PostView}/{postID}\") {\n                        val postIdArg = it.arguments?.getString(\"postID\")\n                        android.util.Log.e(\"MainActivity\", \"Navigated to PostView with postID=$postIdArg\")\n                        PostView(\n                            navController = navController,\n                            postID = postIdArg!!\n                        )\n                    }\n                    composable(\"${NavDestinationKey.UserView}/{user}\") {\n                        UserView(\n                            navController = navController,\n                            user = it.arguments?.getString(\"user\")!!\n                        )\n                    }\n                    composable(\"${NavDestinationKey.CommunityView}/{community}\") {\n                        CommunityView(\n                            navController = navController,\n                            community = it.arguments?.getString(\"community\")!!\n                        )\n                    }\n                }\n            }\n        }\n    }\n\n    override fun onNewIntent(intent: Intent) {\n        super.onNewIntent(intent)\n        navController.handleDeepLink(intent)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/PineappleApp.kt",
    "content": "package com.pineapple.app\n\nimport android.app.Application\nimport com.tencent.mmkv.MMKV\nimport dagger.hilt.android.HiltAndroidApp\n\n@HiltAndroidApp\nclass PineappleApp : Application() {\n\n    override fun onCreate() {\n        super.onCreate()\n        MMKV.initialize(this)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/consts/MMKVKey.kt",
    "content": "package com.pineapple.app.consts\n\n/**\n * Holds constants that represent all keys used in the MMKV preference table\n */\nobject MMKVKey {\n    const val ONBOARDING_COMPLETE = \"onboarding_complete\"\n    const val ACCESS_TOKEN = \"access_token\"\n    const val TOKEN_EXPIRES = \"token_expires\"\n    const val CLIENT_ID = \"client_id\"\n    const val TOKEN_TYPE = \"token_type\"\n    const val REFRESH_TOKEN = \"refresh_token\"\n    const val USER_GUEST = \"user_guest\"\n    const val API_LOGIN_AUTH_CODE = \"api_login_auth_code\"\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/consts/NavDestinationKey.kt",
    "content": "package com.pineapple.app.consts\n\n/**\n * Holds constants used to define all navigation destination routes\n * used in the main navigation graph.\n */\nobject NavDestinationKey {\n    const val WelcomeView = \"welcome\"\n    const val KeyProviderView = \"keyprovider\"\n    const val HomeView = \"home\"\n    const val PostView = \"post\"\n    const val UserView = \"user\"\n    const val CommunityView = \"community\"\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/consts/OnboardingLoginType.kt",
    "content": "package com.pineapple.app.consts\n\n/**\n * Represents the types of login methods available during onboarding.\n */\nobject OnboardingLoginType {\n    const val Guest = \"guest\"\n    const val RedditAuth = \"reddit-auth\"\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/consts/PageDestinationKey.kt",
    "content": "package com.pineapple.app.consts\n\n/**\n * Represents the different pages that can be navigated between in the [HomeView]\n * using the bottom navigation bar\n */\nobject PageDestinationKey {\n    const val BROWSE = 0\n    const val SEARCH = 1\n    const val CHATS = 2\n    const val ACCOUNT = 3\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/consts/PostFilterSort.kt",
    "content": "package com.pineapple.app.consts\n\n/**\n * Represent the available options for sorting posts in a list\n * These values represent the same strings that are sent in an API request\n */\nobject PostFilterSort {\n    const val SORT_HOT = \"hot\"\n    const val SORT_NEW = \"new\"\n    const val SORT_TOP = \"top\"\n    const val SORT_RISING = \"rising\"\n    const val SORT_CONTROVERSIAL = \"controversial\"\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/consts/PostFilterTime.kt",
    "content": "package com.pineapple.app.consts\n\n/**\n * Represents the options for time range supplied when filtering posts by top or controversial\n * These values are the same strings used in API requests\n */\nobject PostFilterTime {\n    const val TIME_DAY = \"day\"\n    const val TIME_MONTH = \"month\"\n    const val TIME_WEEK = \"week\"\n    const val TIME_YEAR = \"year\"\n    const val TIME_ALL = \"all\"\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/di/DatabaseModule.kt",
    "content": "package com.pineapple.app.di\n\nimport android.content.Context\nimport androidx.room.Room\nimport com.pineapple.app.network.caching.AppDatabase\nimport com.pineapple.app.network.caching.dao.PostDao\nimport com.pineapple.app.network.caching.dao.RemoteKeyDao\nimport dagger.Module\nimport dagger.Provides\nimport dagger.hilt.InstallIn\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport dagger.hilt.components.SingletonComponent\nimport javax.inject.Singleton\n\n@Module\n@InstallIn(SingletonComponent::class)\nobject DatabaseModule {\n\n    @Provides\n    @Singleton\n    fun provideDatabase(\n        @ApplicationContext context: Context\n    ): AppDatabase {\n        return Room.databaseBuilder(\n            context,\n            AppDatabase::class.java,\n            \"pineapple-db\"\n        ).build()\n    }\n\n    @Provides\n    fun providePostDao(db: AppDatabase): PostDao = db.postDao()\n\n    @Provides\n    fun provideRemoteKeyDao(db: AppDatabase): RemoteKeyDao = db.remoteKeyDao()\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/di/MMKVModule.kt",
    "content": "package com.pineapple.app.di\n\nimport android.content.Context\nimport com.tencent.mmkv.MMKV\nimport dagger.Module\nimport dagger.Provides\nimport dagger.hilt.InstallIn\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport dagger.hilt.components.SingletonComponent\nimport javax.inject.Singleton\n\n@Module\n@InstallIn(SingletonComponent::class)\nobject MMKVModule {\n    @Provides\n    @Singleton\n    fun provideMMKV(): MMKV = MMKV.defaultMMKV()\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/di/NetworkModule.kt",
    "content": "package com.pineapple.app.di\n\nimport com.google.gson.GsonBuilder\nimport com.pineapple.app.network.interceptor.AuthInterceptor\nimport com.pineapple.app.network.api.AuthRetrofit\nimport com.pineapple.app.network.api.RedditApi\nimport com.pineapple.app.network.repository.RedditRepository\nimport com.pineapple.app.network.api.RedditTokenApi\nimport com.pineapple.app.network.api.TokenRetrofit\nimport com.pineapple.app.network.interceptor.TokenUserAgentInterceptor\nimport com.pineapple.app.network.model.reddit.CommentDataNull\nimport com.pineapple.app.network.repository.RedditAuthRepository\nimport com.pineapple.app.network.serialization.RedditRepliesAdapter\nimport dagger.Module\nimport dagger.Provides\nimport dagger.hilt.InstallIn\nimport dagger.hilt.components.SingletonComponent\nimport okhttp3.OkHttpClient\nimport okhttp3.logging.HttpLoggingInterceptor\nimport retrofit2.Retrofit\nimport retrofit2.converter.gson.GsonConverterFactory\nimport javax.inject.Singleton\n\n@Module\n@InstallIn(SingletonComponent::class)\nobject NetworkModule {\n\n    @Provides\n    @Singleton\n    fun provideLoggingInterceptor(): HttpLoggingInterceptor =\n        HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }\n\n    @Provides\n    @Singleton\n    fun provideAuthInterceptor(\n        repository: RedditAuthRepository\n    ): AuthInterceptor = AuthInterceptor(repository)\n\n    @Provides\n    @Singleton\n    fun provideTokenUaInterceptor(): TokenUserAgentInterceptor =\n        TokenUserAgentInterceptor()\n\n    @AuthRetrofit\n    @Provides\n    @Singleton\n    fun provideOAuthOkHttpClient(\n        logging: HttpLoggingInterceptor,\n        authInterceptor: AuthInterceptor\n    ): OkHttpClient = OkHttpClient.Builder()\n        .addInterceptor(logging)\n        .addInterceptor(authInterceptor)\n        .build()\n\n    @TokenRetrofit\n    @Provides\n    @Singleton\n    fun provideTokenOkHttpClient(\n        logging: HttpLoggingInterceptor,\n        tokenUaInterceptor: TokenUserAgentInterceptor\n    ): OkHttpClient = OkHttpClient.Builder()\n        .addInterceptor(logging)\n        .addInterceptor(tokenUaInterceptor)\n        .build()\n\n    @AuthRetrofit\n    @Provides\n    @Singleton\n    fun provideOAuthRetrofit(\n        @AuthRetrofit okHttpClient: OkHttpClient\n    ): Retrofit = Retrofit.Builder()\n        .baseUrl(\"https://oauth.reddit.com/\")\n        .client(okHttpClient)\n        .addConverterFactory(\n            GsonConverterFactory.create(\n                GsonBuilder()\n                    .setLenient()\n                    .registerTypeAdapter(CommentDataNull::class.java, RedditRepliesAdapter())\n                    .create()\n            )\n        )\n        .build()\n\n    @TokenRetrofit\n    @Provides\n    @Singleton\n    fun provideTokenRetrofit(\n        @TokenRetrofit okHttpClient: OkHttpClient\n    ): Retrofit = Retrofit.Builder()\n        .baseUrl(\"https://www.reddit.com/\")\n        .client(okHttpClient)\n        .addConverterFactory(\n            GsonConverterFactory.create(\n                GsonBuilder()\n                    .setLenient()\n                    .registerTypeAdapter(CommentDataNull::class.java, RedditRepliesAdapter())\n                    .create()\n            )\n        )\n        .build()\n\n    @Provides\n    @Singleton\n    fun provideRedditApi(\n        @AuthRetrofit oauthRetrofit: Retrofit\n    ): RedditApi = oauthRetrofit.create(RedditApi::class.java)\n\n    @Provides\n    @Singleton\n    fun provideTokenApi(\n        @TokenRetrofit tokenRetrofit: Retrofit\n    ): RedditTokenApi = tokenRetrofit.create(RedditTokenApi::class.java)\n\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/api/RedditApi.kt",
    "content": "package com.pineapple.app.network.api\n\nimport com.pineapple.app.network.model.reddit.CommentPreData\nimport com.pineapple.app.network.model.reddit.CondensedUserAboutListing\nimport com.pineapple.app.network.model.reddit.Listing\nimport com.pineapple.app.network.model.reddit.ListingBase\nimport com.pineapple.app.network.model.reddit.ListingItem\nimport com.pineapple.app.network.model.reddit.PostData\nimport com.pineapple.app.network.model.reddit.PostListing\nimport com.pineapple.app.network.model.reddit.SubredditItem\nimport com.pineapple.app.network.model.reddit.UserAboutListing\nimport retrofit2.http.GET\nimport retrofit2.http.POST\nimport retrofit2.http.Path\nimport retrofit2.http.Query\nimport javax.inject.Qualifier\n\n/**\n * Interface with the Reddit API endpoints giving access to app content\n */\ninterface RedditApi {\n\n    /**\n     * Fetch posts from a specific subreddit with sorting and time filters\n     * @param name The name of the subreddit (without the r/ prefix)\n     * @param sort The sorting method (use [com.pineapple.app.consts.PostFilterSort]\n     * @param time The time filter (use [com.pineapple.app.consts.PostFilterTime])\n     * @param after The ID of the last post from the previous fetch for pagination\n     * @param rawJson Whether to get raw JSON (1) or not (0)\n     * @param limit The number of posts to fetch (default is 5)\n     * @return A [PostListing] containing the fetched posts\n     */\n    @GET(\"r/{name}/{sort}\")\n    suspend fun fetchSubreddit(\n        @Path(\"name\") name: String,\n        @Path(\"sort\") sort: String,\n        @Query(\"t\") time: String,\n        @Query(\"after\") after: String? = null,\n        @Query(\"raw_json\") rawJson: Int = 1,\n        @Query(\"limit\") limit: Int = 5\n    ): PostListing\n\n    /**\n     * Search posts globally (or within a subreddit if using /r/{subreddit}/search endpoint)\n     * @param query The search query string\n     * @param sort Sorting for the search (relevance, hot, new, top, comments)\n     * @param time Time filter (use [com.pineapple.app.consts.PostFilterTime])\n     * @param after Pagination token\n     * @param rawJson Whether to get raw JSON (1) or not (0)\n     * @param limit Number of results to fetch\n     */\n    @GET(\"search\")\n    suspend fun searchPosts(\n        @Query(\"q\") query: String,\n        @Query(\"sort\") sort: String? = null,\n        @Query(\"t\") time: String? = null,\n        @Query(\"after\") after: String? = null,\n        @Query(\"raw_json\") rawJson: Int = 1,\n        @Query(\"limit\") limit: Int = 25\n    ): PostListing\n\n    /**\n     * Fetch a specific post and its comments\n     * @param subreddit The name of the subreddit (without the r/ prefix)\n     * @param postID The ID of the post\n     * @param post The post's slug (title in URL-friendly format)\n     * @param rawJson Whether to get raw JSON (1) or not (0)\n     * @return A [String] containing the raw JSON response\n     */\n    @GET(\"r/{name}/comments/{id}/{post}\")\n    suspend fun fetchPost(\n        @Path(\"name\") subreddit: String,\n        @Path(\"id\") postID: String,\n        @Path(\"post\") post: String,\n        @Query(\"raw_json\") rawJson: Int = 1\n    ): List<Listing<ListingItem<PostData>>>\n\n    /**\n     * Fetch a specific post and its comments using only the post id (no subreddit)\n     * This hits /comments/{id} which returns the post listing and is useful when we don't have the subreddit/slug cached\n     */\n    @GET(\"comments/{id}\")\n    suspend fun fetchPostById(\n        @Path(\"id\") postID: String,\n        @Query(\"raw_json\") rawJson: Int = 1\n    ): List<Listing<ListingItem<PostData>>>\n\n    /**\n     * Fetch the subreddits the authenticated user is subscribed to\n     * @return A [Listing] of [SubredditItem] representing the subscribed subreddits\n     */\n    @GET(\"/subreddits/mine/subscriber\")\n    suspend fun fetchSubscribedSubreddits(\n        @Query(\"raw_json\") rawJson: Int = 1,\n        @Query(\"after\") after: String? = null,\n        @Query(\"limit\") limit: Int = 6\n    ): Listing<SubredditItem>\n\n    /**\n     * Fetch the current trending subreddits\n     * @return A [Listing] of [SubredditItem] representing the top subreddits\n     */\n    @GET(\"subreddits/popular\")\n    suspend fun fetchTopSubreddits(\n        @Query(\"raw_json\") rawJson: Int = 1,\n        @Query(\"after\") after: String? = null,\n        @Query(\"limit\") limit: Int = 5\n    ): Listing<SubredditItem>\n\n    /**\n     * Fetch the current popular users\n     * @return A [Listing] of [CondensedUserAboutListing] representing the top users\n     */\n    @GET(\"users/popular\")\n    suspend fun fetchTopUsers(\n        @Query(\"raw_json\") rawJson: Int = 1\n    ): Listing<CondensedUserAboutListing>\n\n    /**\n     * Fetch detailed information about a specific user\n     * @param user The username of the user\n     * @param rawJson Whether to get raw JSON (1) or not (0)\n     * @return A [UserAboutListing] containing the user's information\n     */\n    @GET(\"/user/{user}/about\")\n    suspend fun fetchUserInfo(\n        @Path(\"user\") user: String,\n        @Query(\"raw_json\") rawJson: Int = 1\n    ) : UserAboutListing\n\n    /**\n     * Cast an upvote, downvote, or remove vote on a post or comment\n     * @param id The full name of the post or comment (with the t* prefix)\n     * @param dir The direction of the vote: 1 (upvote), -1 (downvote), 0 (remove vote)\n     */\n    @POST(\"/api/vote\")\n    suspend fun castVote(\n        @Query(\"id\") id: String,\n        @Query(\"dir\") dir: Int\n    )\n\n    /**\n     * Save a post or comment to the user's saved items\n     * @param id The full name of the post or comment (with the t*_ prefix)\n     */\n    @POST(\"/api/save\")\n    suspend fun savePost(\n        @Query(\"id\") id: String\n    )\n\n    /**\n     * Unsave a post or comment from the user's saved items\n     * @param id The full name of the post or comment (with the t*_ prefix)\n     */\n    @POST(\"/api/unsave\")\n    suspend fun unsavePost(\n        @Query(\"id\") id: String\n    )\n\n    /**\n     * Search for communities (subreddits) by query. Returns Listing<SubredditItem>\n     */\n    @GET(\"search\")\n    suspend fun searchCommunities(\n        @Query(\"q\") query: String,\n        @Query(\"type\") type: String = \"sr\",\n        @Query(\"raw_json\") rawJson: Int = 1,\n        @Query(\"limit\") limit: Int = 6\n    ): Listing<SubredditItem>\n\n    /**\n     * Search for users by query. Returns ListingBase<UserAboutListing>\n     */\n    @GET(\"search\")\n    suspend fun searchUsers(\n        @Query(\"q\") query: String,\n        @Query(\"type\") type: String = \"user\",\n        @Query(\"raw_json\") rawJson: Int = 1,\n        @Query(\"limit\") limit: Int = 6\n    ): ListingBase<UserAboutListing>\n\n    /**\n     * Fetch comments for a post id (comments/{id}), returns the same structure as fetchPost\n     */\n    @GET(\"comments/{id}\")\n    suspend fun fetchCommentsByPostId(\n        @Path(\"id\") postID: String,\n        @Query(\"raw_json\") rawJson: Int = 1\n    ): List<Listing<CommentPreData>>\n\n\n}\n\n/**\n * Qualifier annotation for Retrofit instance used for authenticated requests\n * (to differentiate between this and [RedditTokenApi] in dependency injection)\n */\n@Qualifier\n@Retention(AnnotationRetention.RUNTIME)\nannotation class AuthRetrofit"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/api/RedditTokenApi.kt",
    "content": "package com.pineapple.app.network.api\n\nimport com.pineapple.app.network.model.auth.AuthResponse\nimport retrofit2.Response\nimport retrofit2.http.Field\nimport retrofit2.http.FormUrlEncoded\nimport retrofit2.http.Header\nimport retrofit2.http.POST\nimport javax.inject.Qualifier\n\n/**\n * Interface with the Reddit OAuth API used to authenticate, request, and refresh tokens\n * that are then passed to all calls made with the RedditApi interface\n */\ninterface RedditTokenApi {\n\n    /**\n     * Request an access token for the API without a user context\n     * @param basicAuth The basic authentication header containing the client ID as username and an empty password\n     * @param grantType The type of grant being requested, leave as installed client\n     * @param deviceID A unique device identifier, or leave as default to avoid tracking\n     * @return A Response object containing the AuthResponse with access token details\n     */\n    @FormUrlEncoded\n    @POST(\"api/v1/access_token\")\n    suspend fun authenticateUserless(\n        @Header(\"Authorization\") basicAuth: String,\n        @Field(\"grant_type\") grantType: String = \"https://oauth.reddit.com/grants/installed_client\",\n        @Field(\"device_id\") deviceID: String = \"DO_NOT_TRACK_THIS_DEVICE\"\n    ): Response<AuthResponse>\n\n    /**\n     * Request a new access token if you already have one that expired, and a refresh token\n     * @param basicAuth The basic authentication header containing the client ID as username and an empty password\n     * @param grantType The type of grant being requested, leave as refresh token\n     * @param refreshToken The refresh token previously obtained during authentication\n     * @return A Response object containing the AuthResponse with new access and refresh token details\n     */\n    @FormUrlEncoded\n    @POST(\"api/v1/access_token\")\n    suspend fun refreshAccessToken(\n        @Header(\"Authorization\") basicAuth: String,\n        @Field(\"grant_type\") grantType: String = \"refresh_token\",\n        @Field(\"refresh_token\") refreshToken: String\n    ): Response<AuthResponse>\n\n    /**\n     * Request an access token after having authenticated using OAuth in the browser, so that\n     * future API calls can be made on behalf of the authenticated user\n     * @param basicAuth The basic authentication header containing the client ID as username and an empty password\n     * @param grantType The type of grant being requested, leave as authorization code\n     * @param authCode The authorization code received from the OAuth redirect after user login\n     * @param redirectURI The redirect URI used during the OAuth authentication\n     * @return A Response object containing the AuthResponse with access token and refresh token details\n     */\n    @FormUrlEncoded\n    @POST(\"/api/v1/access_token\")\n    suspend fun authenticateUser(\n        @Header(\"Authorization\") basicAuth: String ,\n        @Field(\"grant_type\") grantType: String = \"authorization_code\",\n        @Field(\"code\") authCode: String,\n        @Field(\"redirect_uri\") redirectURI: String = \"pineapple://login\"\n    ) : Response<AuthResponse>\n\n}\n\n/**\n * Qualifier annotation to identify the Retrofit instance for RedditTokenApi\n * (and differentiate it between [RedditApi] in dependency injection)\n */\n@Qualifier\n@Retention(AnnotationRetention.RUNTIME)\nannotation class TokenRetrofit"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/caching/AppDatabase.kt",
    "content": "package com.pineapple.app.network.caching\n\nimport androidx.room.Database\nimport androidx.room.RoomDatabase\nimport com.pineapple.app.network.caching.dao.PostDao\nimport com.pineapple.app.network.caching.dao.RemoteKeyDao\nimport com.pineapple.app.network.caching.dao.SubredditDao\nimport com.pineapple.app.network.caching.dao.UserDao\nimport com.pineapple.app.network.caching.dao.SearchResultDao\nimport com.pineapple.app.network.caching.dao.SearchRemoteKeyDao\nimport com.pineapple.app.network.caching.dao.CommentDao\nimport com.pineapple.app.network.caching.entity.PostEntity\nimport com.pineapple.app.network.caching.entity.RemoteKeyEntity\nimport com.pineapple.app.network.caching.entity.SubredditEntity\nimport com.pineapple.app.network.caching.entity.UserEntity\nimport com.pineapple.app.network.caching.entity.SearchResultEntity\nimport com.pineapple.app.network.caching.entity.SearchRemoteKeyEntity\nimport com.pineapple.app.network.caching.entity.CommentEntity\n\n@Database(\n    entities = [\n        PostEntity::class,\n        RemoteKeyEntity::class,\n        UserEntity::class,\n        SubredditEntity::class,\n        SearchResultEntity::class,\n        SearchRemoteKeyEntity::class,\n        CommentEntity::class\n    ],\n    version = 6\n)\nabstract class AppDatabase : RoomDatabase() {\n    abstract fun postDao(): PostDao\n    abstract fun remoteKeyDao(): RemoteKeyDao\n    abstract fun userDao(): UserDao\n    abstract fun subredditDao(): SubredditDao\n    abstract fun searchResultDao(): SearchResultDao\n    abstract fun searchRemoteKeyDao(): SearchRemoteKeyDao\n    abstract fun commentDao(): CommentDao\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/caching/dao/CommentDao.kt",
    "content": "package com.pineapple.app.network.caching.dao\n\nimport androidx.paging.PagingSource\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport androidx.room.Transaction\nimport com.pineapple.app.network.caching.entity.CommentEntity\nimport com.pineapple.app.network.model.cache.CommentWithUser\nimport kotlinx.coroutines.flow.Flow\n\n@Dao\ninterface CommentDao {\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun upsertAll(comments: List<CommentEntity>)\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun upsert(comment: CommentEntity)\n\n    @Transaction\n    @Query(\"SELECT * FROM comments WHERE postId = :postId ORDER BY sortKey ASC\")\n    fun pagingSourceForPost(postId: String): PagingSource<Int, CommentWithUser>\n\n    @Query(\"SELECT MAX(sortKey) FROM comments WHERE postId = :postId\")\n    suspend fun maxSortKeyForPost(postId: String): Int?\n\n    @Transaction\n    @Query(\"SELECT * FROM comments WHERE id = :commentId\")\n    suspend fun getComment(commentId: String): CommentWithUser?\n\n    // Replies handling: fetch replies whose parentId matches a given comment id\n    @Transaction\n    @Query(\"SELECT * FROM comments WHERE parentId = :parentId ORDER BY sortKey ASC\")\n    fun getRepliesForCommentFlow(parentId: String): Flow<List<CommentWithUser>>\n\n    @Query(\"SELECT COUNT(*) FROM comments WHERE parentId = :parentId\")\n    suspend fun countRepliesForComment(parentId: String): Int\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/caching/dao/PostDao.kt",
    "content": "package com.pineapple.app.network.caching.dao\n\nimport androidx.paging.PagingSource\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport androidx.room.Transaction\nimport com.pineapple.app.network.caching.entity.PostEntity\nimport com.pineapple.app.network.model.cache.PostWithUser\nimport kotlinx.coroutines.flow.Flow\n\n@Dao\ninterface PostDao {\n\n    @Transaction\n    @Query(\"SELECT * FROM posts ORDER BY sortKey ASC\")\n    fun pagingSourceWithUser(): PagingSource<Int, PostWithUser>\n\n    // PagingSource for search results: select posts by postId from search_results for query\n    @Transaction\n    @Query(\"SELECT * FROM posts WHERE id IN (SELECT postId FROM search_results WHERE query = :q) ORDER BY (SELECT sortKey FROM search_results WHERE query = :q AND postId = posts.id) ASC\")\n    fun pagingSourceForSearchQuery(q: String): PagingSource<Int, PostWithUser>\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertAll(posts: List<PostEntity>)\n\n    @Query(\"DELETE FROM posts\")\n    suspend fun clearAll()\n\n    @Query(\"SELECT COUNT(*) FROM posts\")\n    suspend fun countAll(): Int\n\n    @Query(\"SELECT MAX(sortKey) FROM posts\")\n    suspend fun maxSortKey(): Int?\n\n    @Query(\"SELECT * FROM posts WHERE id = :id LIMIT 1\")\n    suspend fun getPost(id: String): PostEntity?\n\n    @Transaction\n    @Query(\"SELECT * FROM posts WHERE id = :id LIMIT 1\")\n    fun getPostWithUserFlow(id: String): Flow<PostWithUser?>\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun upsert(post: PostEntity)\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun upsertAll(posts: List<PostEntity>)\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/caching/dao/RemoteKeyDao.kt",
    "content": "package com.pineapple.app.network.caching.dao\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport com.pineapple.app.network.caching.entity.RemoteKeyEntity\n\n\n@Dao\ninterface RemoteKeyDao {\n    @Query(\"SELECT * FROM remote_keys WHERE postId = :id\")\n    suspend fun remoteKeysPostId(id: String): RemoteKeyEntity?\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertAll(keys: List<RemoteKeyEntity>)\n\n    @Query(\"DELETE FROM remote_keys\")\n    suspend fun clearRemoteKeys()\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/caching/dao/SearchRemoteKeyDao.kt",
    "content": "package com.pineapple.app.network.caching.dao\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport com.pineapple.app.network.caching.entity.SearchRemoteKeyEntity\n\n@Dao\ninterface SearchRemoteKeyDao {\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertAll(keys: List<SearchRemoteKeyEntity>)\n\n    @Query(\"SELECT nextKey FROM search_remote_keys WHERE query = :q AND postId = :postId LIMIT 1\")\n    suspend fun remoteKeysPostId(q: String, postId: String): String?\n\n    @Query(\"DELETE FROM search_remote_keys WHERE query = :q\")\n    suspend fun clearRemoteKeysForQuery(q: String)\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/caching/dao/SearchResultDao.kt",
    "content": "package com.pineapple.app.network.caching.dao\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport com.pineapple.app.network.caching.entity.SearchResultEntity\n\n@Dao\ninterface SearchResultDao {\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertAll(results: List<SearchResultEntity>)\n\n    @Query(\"DELETE FROM search_results WHERE query = :q\")\n    suspend fun clearQuery(q: String)\n\n    @Query(\"SELECT postId FROM search_results WHERE query = :q ORDER BY sortKey ASC\")\n    suspend fun getPostIdsForQuery(q: String): List<String>\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/caching/dao/SubredditDao.kt",
    "content": "package com.pineapple.app.network.caching.dao\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport com.pineapple.app.network.caching.entity.SubredditEntity\nimport kotlinx.coroutines.flow.Flow\n\n@Dao\ninterface SubredditDao {\n\n    @Query(\"SELECT * FROM subreddits ORDER BY subscribers DESC\")\n    fun getPopularSubreddits(): Flow<List<SubredditEntity>>\n\n    @Query(\"SELECT * FROM subreddits WHERE isSubscribed = 1 ORDER BY name COLLATE NOCASE ASC\")\n    fun getSubscribedSubreddits(): Flow<List<SubredditEntity>>\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun upsertAll(subreddits: List<SubredditEntity>)\n\n    @Query(\"UPDATE subreddits SET isSubscribed = 0\")\n    suspend fun markAllUnsubscribed()\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/caching/dao/UserDao.kt",
    "content": "package com.pineapple.app.network.caching.dao\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport com.pineapple.app.network.caching.entity.UserEntity\n\n@Dao\ninterface UserDao {\n    @Query(\"SELECT * FROM users WHERE name = :name\")\n    suspend fun getUser(name: String): UserEntity?\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertAll(users: List<UserEntity>)\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/caching/entity/CommentEntity.kt",
    "content": "package com.pineapple.app.network.caching.entity\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"comments\")\ndata class CommentEntity(\n    @PrimaryKey val id: String,\n    val postId: String,\n    val parentId: String?,\n    val author: String?,\n    val body: String?,\n    val bodyHtml: String?,\n    val ups: Int?,\n    val sortKey: Int,\n    val depth: Int = 0,\n    val replyCount: Int = 0,\n    val createdUtc: Long? = null,\n    val saved: Boolean? = null,\n    val likes: Boolean? = null,\n    val permalink: String? = null\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/caching/entity/PostEntity.kt",
    "content": "package com.pineapple.app.network.caching.entity\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"posts\")\ndata class PostEntity(\n    @PrimaryKey val id: String,\n    val title: String,\n    val author: String?,\n    val subreddit: String?,\n    val createdUtc: Long,\n    val ups: Int?,\n    val thumbnail: String?,\n    val permalink: String,\n    val url: String?,\n    val previewImageUrl: String?,\n    val previewWidth: Long?,\n    val previewHeight: Long?,\n    val sortKey: Int,\n    val saved: Boolean? = null,\n    val likes: Boolean? = null,\n    val selftext: String? = null\n)"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/caching/entity/RemoteKeyEntity.kt",
    "content": "package com.pineapple.app.network.caching.entity\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"remote_keys\")\ndata class RemoteKeyEntity(\n    @PrimaryKey val postId: String,\n    val prevKey: String?,\n    val nextKey: String?\n)"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/caching/entity/SearchRemoteKeyEntity.kt",
    "content": "package com.pineapple.app.network.caching.entity\n\nimport androidx.room.Entity\n\n@Entity(\n    tableName = \"search_remote_keys\",\n    primaryKeys = [\"query\", \"postId\"]\n)\ndata class SearchRemoteKeyEntity(\n    val query: String,\n    val postId: String,\n    val prevKey: String?,\n    val nextKey: String?\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/caching/entity/SearchResultEntity.kt",
    "content": "package com.pineapple.app.network.caching.entity\n\nimport androidx.room.Entity\n\n@Entity(\n    tableName = \"search_results\",\n    primaryKeys = [\"query\", \"postId\"]\n)\ndata class SearchResultEntity(\n    val query: String,\n    val postId: String,\n    val sortKey: Int\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/caching/entity/SubredditEntity.kt",
    "content": "package com.pineapple.app.network.caching.entity\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"subreddits\")\ndata class SubredditEntity(\n    @PrimaryKey val id: String,          // \"t5_xxx\" or short id\n    val name: String,                    // \"androiddev\"\n    val title: String,\n    val iconUrl: String,\n    val subscribers: Long,\n    val isNsfw: Boolean,\n    val isSubscribed: Boolean\n)"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/caching/entity/UserEntity.kt",
    "content": "package com.pineapple.app.network.caching.entity\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"users\")\ndata class UserEntity(\n    @PrimaryKey val name: String,\n    val iconUrl: String?,\n    val snoovatarUrl: String?\n)"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/interceptor/AuthInterceptor.kt",
    "content": "package com.pineapple.app.network.interceptor\n\nimport com.pineapple.app.network.repository.RedditAuthRepository\nimport com.pineapple.app.network.repository.RedditRepository\nimport com.pineapple.app.network.repository.USER_AGENT\nimport kotlinx.coroutines.runBlocking\nimport okhttp3.Interceptor\nimport okhttp3.Response\n\n/**\n * Injects our auth token into all requests, handling token refresh and validity\n * checks as needed so callers do not need to\n */\nclass AuthInterceptor(private val repository: RedditAuthRepository) : Interceptor {\n\n    override fun intercept(chain: Interceptor.Chain): Response {\n        val original = chain.request()\n\n        // Skip auth for token endpoint\n        if (original.url.host == \"www.reddit.com\" &&\n            original.url.encodedPath.startsWith(\"/api/v1/access_token\")\n        ) {\n            val tokenReq = original.newBuilder()\n                .header(\"User-Agent\", USER_AGENT)\n                .build()\n            return chain.proceed(tokenReq)\n        }\n\n        val authHeader = runBlocking {\n            repository.ensureValidToken()\n            repository.authorizationHeaderOrNull()\n        }\n        val newReqBuilder = original.newBuilder()\n            .header(\"User-Agent\", USER_AGENT)\n\n        if (!authHeader.isNullOrBlank()) {\n            newReqBuilder.header(\"Authorization\", authHeader)\n        }\n\n        return chain.proceed(newReqBuilder.build())\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/interceptor/TokenUserAgentInterceptor.kt",
    "content": "package com.pineapple.app.network.interceptor\n\nimport com.pineapple.app.network.repository.USER_AGENT\nimport okhttp3.Interceptor\nimport okhttp3.Response\n\n/**\n * Injects the User-Agent header into each request\n */\nclass TokenUserAgentInterceptor : Interceptor {\n\n    override fun intercept(chain: Interceptor.Chain): Response {\n        val req = chain.request().newBuilder()\n            .header(\"User-Agent\", USER_AGENT)\n            .build()\n        return chain.proceed(req)\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/auth/AuthResponse.kt",
    "content": "package com.pineapple.app.network.model.auth\n\nimport com.google.gson.annotations.SerializedName\n\ndata class AuthResponse(\n    @SerializedName(\"access_token\")\n    var accessToken: String,\n    @SerializedName(\"token_type\")\n    var tokenType: String,\n    @SerializedName(\"expires_in\")\n    var expires: Long,\n    @SerializedName(\"scope\")\n    var scope: String,\n    @SerializedName(\"refresh_token\")\n    var refreshToken: String? = null\n)"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/cache/CommentWithUser.kt",
    "content": "package com.pineapple.app.network.model.cache\n\nimport androidx.room.Embedded\nimport androidx.room.Relation\nimport com.pineapple.app.network.caching.entity.CommentEntity\nimport com.pineapple.app.network.caching.entity.UserEntity\n\ndata class CommentWithUser(\n    @Embedded val comment: CommentEntity,\n    @Relation(\n        parentColumn = \"author\",\n        entityColumn = \"name\"\n    )\n    val user: UserEntity?\n)\n\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/cache/PostwithUser.kt",
    "content": "package com.pineapple.app.network.model.cache\n\nimport androidx.room.Embedded\nimport androidx.room.Relation\nimport com.pineapple.app.network.caching.entity.PostEntity\nimport com.pineapple.app.network.caching.entity.UserEntity\n\ndata class PostWithUser(\n    @Embedded val post: PostEntity,\n    @Relation(\n        parentColumn = \"author\",\n        entityColumn = \"name\"\n    )\n    val user: UserEntity?\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/AboutAccount.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class AboutAccount(\n    val subreddit: UserSubredditData,\n    val snoovatar_img: String\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/AllAwarding.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\nimport com.google.gson.annotations.SerializedName\n\ndata class AllAwarding(\n    @SerializedName(\"giver_coin_reward\")\n    val giverCoinReward: Long? = null,\n\n    @SerializedName(\"subreddit_id\")\n    val subredditID: Any? = null,\n\n    @SerializedName(\"is_new\")\n    val isNew: Boolean,\n\n    @SerializedName(\"days_of_drip_extension\")\n    val daysOfDripExtension: Long,\n\n    @SerializedName(\"coin_price\")\n    val coinPrice: Long,\n\n    val id: String,\n\n    @SerializedName(\"penny_donate\")\n    val pennyDonate: Long? = null,\n\n    @SerializedName(\"award_sub_type\")\n    val awardSubType: String,\n\n    @SerializedName(\"coin_reward\")\n    val coinReward: Long,\n\n    @SerializedName(\"icon_url\")\n    val iconURL: String,\n\n    @SerializedName(\"days_of_premium\")\n    val daysOfPremium: Long,\n\n    @SerializedName(\"tiers_by_required_awardings\")\n    val tiersByRequiredAwardings: Any? = null,\n\n    @SerializedName(\"resized_icons\")\n    val resizedIcons: List<ResizedIcon>,\n\n    @SerializedName(\"icon_width\")\n    val iconWidth: Long,\n\n    @SerializedName(\"static_icon_width\")\n    val staticIconWidth: Long,\n\n    @SerializedName(\"start_date\")\n    val startDate: Any? = null,\n\n    @SerializedName(\"is_enabled\")\n    val isEnabled: Boolean,\n\n    @SerializedName(\"awardings_required_to_grant_benefits\")\n    val awardingsRequiredToGrantBenefits: Any? = null,\n\n    val description: String,\n\n    @SerializedName(\"end_date\")\n    val endDate: Any? = null,\n\n    @SerializedName(\"subreddit_coin_reward\")\n    val subredditCoinReward: Long,\n\n    val count: Long,\n\n    @SerializedName(\"static_icon_height\")\n    val staticIconHeight: Long,\n\n    val name: String,\n\n    @SerializedName(\"resized_static_icons\")\n    val resizedStaticIcons: List<ResizedIcon>,\n\n    @SerializedName(\"icon_format\")\n    val iconFormat: String? = null,\n\n    @SerializedName(\"icon_height\")\n    val iconHeight: Long,\n\n    @SerializedName(\"penny_price\")\n    val pennyPrice: Long? = null,\n\n    @SerializedName(\"award_type\")\n    val awardType: String,\n\n    @SerializedName(\"static_icon_url\")\n    val staticIconURL: String\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/CommentData.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\nimport com.google.gson.JsonElement\n\ndata class CommentPreData(\n    var kind: String,\n    var data: CommentData\n)\n\ndata class CommentData(\n    var author: String?,\n    var subreddit: String?,\n    var id: String,\n    var ups: Long?,\n    var body: String?,\n    var body_html: String,\n    var permalink: String,\n    var replies: JsonElement? = null,\n    var created_utc: Double? = null,\n    var saved: Boolean? = null,\n    var likes: Boolean? = null\n)\n\ndata class CommentDataNull(\n    var author: String,\n    var subreddit: String,\n    var id: String,\n    var ups: Long,\n    var body: String?,\n    var body_html: String,\n    var permalink: String,\n    var link_title: String? = null\n)"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/CommentListing.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class CommentListing(\n    var kind: String,\n    var data: ListingItem<CommentPreData>\n)\n\ndata class CommentListingNull(\n    var kind: String,\n    var data: ListingItem<Any>\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/CondensedUserAbout.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class CondensedUserAboutListing(\n    var kind: String,\n    var data: CondensedUserAbout\n)\n\ndata class CondensedUserAbout(\n    var id: String,\n    var snoovatar_img: String?,\n    var icon_img: String?,\n    var name: String?,\n    var display_name_prefixed: String,\n    var is_gold: Boolean,\n    var total_karma: Long,\n    var awardee_karma: Long,\n    var link_karma: Long,\n    var awarder_karma: Long,\n    var comment_karma: Long,\n    var has_verified_email: Boolean,\n    var accept_chats: Boolean,\n    var created_utc: Long,\n    var accept_followers: Boolean,\n    var accept_pms: Boolean,\n    var verified: Boolean\n\n)"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/FlairRichItem.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class FlairRichItem(\n    var a: String?,\n    var e: String,\n    var u: String?,\n    var t: String?\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/Gildings.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\nimport com.google.gson.annotations.SerializedName\n\ndata class Gildings (\n    @SerializedName(\"gid_1\")\n    val gid1: Long\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/Image.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class Image (\n    val source: ResizedIcon,\n    val resolutions: ArrayList<ResizedIcon>\n)\n\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/Listing.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class Listing<T>(\n    val kind: String,\n    val data: ListingItem<T>\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/ListingBase.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class ListingBase<T>(\n    var kind: String,\n    var data: ListingItem<T>\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/ListingItem.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class ListingItem<T>(\n    var after: String,\n    var before: String,\n    var dist: Int,\n    var modhash: String,\n    var children: List<T>\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/PostData.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\nimport com.google.gson.JsonObject\nimport com.google.gson.annotations.SerializedName\n\ntypealias MediaEmbed = JsonObject\ndata class PostData(\n\n    @SerializedName(\"approved_at_utc\")\n    val approvedAtUTC: Any? = null,\n\n    val subreddit: String? = null,\n    val selftext: String? = null,\n\n    @SerializedName(\"author_fullname\")\n    val authorFullname: String? = null,\n\n    val saved: Boolean? = null,\n\n    @SerializedName(\"mod_reason_title\")\n    val modReasonTitle: Any? = null,\n\n    val gilded: Long? = null,\n    val clicked: Boolean? = null,\n    val title: String? = null,\n\n    @SerializedName(\"link_flair_richtext\")\n    val linkFlairRichtext: List<FlairRichItem>? = null,\n\n    @SerializedName(\"subreddit_name_prefixed\")\n    val subredditNamePrefixed: String? = null,\n\n    val hidden: Boolean? = null,\n    val pwls: Long? = null,\n\n    @SerializedName(\"link_flair_css_class\")\n    val linkFlairCSSClass: String? = null,\n\n    val downs: Long? = null,\n\n    @SerializedName(\"thumbnail_height\")\n    val thumbnailHeight: Long? = null,\n\n    @SerializedName(\"top_awarded_type\")\n    val topAwardedType: Any? = null,\n\n    @SerializedName(\"hide_score\")\n    val hideScore: Boolean? = null,\n\n    val name: String? = null,\n    val quarantine: Boolean? = null,\n\n    @SerializedName(\"link_flair_text_color\")\n    val linkFlairTextColor: String? = null,\n\n    @SerializedName(\"upvote_ratio\")\n    val upvoteRatio: Double? = null,\n\n    @SerializedName(\"author_flair_background_color\")\n    val authorFlairBackgroundColor: Any? = null,\n\n    @SerializedName(\"subreddit_type\")\n    val subredditType: String? = null,\n\n    val ups: Long? = null,\n\n    @SerializedName(\"total_awards_received\")\n    val totalAwardsReceived: Long? = null,\n\n    @SerializedName(\"media_embed\")\n    val mediaEmbed: MediaEmbed? = null,\n\n    @SerializedName(\"thumbnail_width\")\n    val thumbnailWidth: Long? = null,\n\n    @SerializedName(\"author_flair_template_id\")\n    val authorFlairTemplateID: Any? = null,\n\n    @SerializedName(\"is_original_content\")\n    val isOriginalContent: Boolean? = null,\n\n    @SerializedName(\"user_reports\")\n    val userReports: List<Any?>? = null,\n\n    @SerializedName(\"secure_media\")\n    val secureMedia: SecureMedia? = null,\n\n    @SerializedName(\"is_reddit_media_domain\")\n    val isRedditMediaDomain: Boolean? = null,\n\n    @SerializedName(\"is_meta\")\n    val isMeta: Boolean? = null,\n\n    val category: Any? = null,\n\n    @SerializedName(\"secure_media_embed\")\n    val secureMediaEmbed: MediaEmbed? = null,\n\n    @SerializedName(\"link_flair_text\")\n    val linkFlairText: String? = null,\n\n    @SerializedName(\"can_mod_post\")\n    val canModPost: Boolean? = null,\n\n    val score: Long? = null,\n\n    @SerializedName(\"approved_by\")\n    val approvedBy: Any? = null,\n\n    @SerializedName(\"is_created_from_ads_ui\")\n    val isCreatedFromAdsUI: Boolean? = null,\n\n    @SerializedName(\"author_premium\")\n    val authorPremium: Boolean? = null,\n\n    val thumbnail: String? = null,\n    val edited: Any? = null,\n\n    @SerializedName(\"author_flair_css_class\")\n    val authorFlairCSSClass: Any? = null,\n\n    @SerializedName(\"author_flair_richtext\")\n    val authorFlairRichtext: List<Any?>? = null,\n\n    val gildings: Gildings? = null,\n\n    @SerializedName(\"post_hint\")\n    val postHint: String? = null,\n\n    @SerializedName(\"content_categories\")\n    val contentCategories: Any? = null,\n\n    @SerializedName(\"is_self\")\n    val isSelf: Boolean? = null,\n\n    @SerializedName(\"mod_note\")\n    val modNote: Any? = null,\n\n    val created: Long? = null,\n\n    @SerializedName(\"link_flair_type\")\n    val linkFlairType: String? = null,\n\n    val wls: Long? = null,\n\n    @SerializedName(\"removed_by_category\")\n    val removedByCategory: Any? = null,\n\n    @SerializedName(\"banned_by\")\n    val bannedBy: Any? = null,\n\n    @SerializedName(\"author_flair_type\")\n    val authorFlairType: String? = null,\n\n    val domain: String? = null,\n\n    @SerializedName(\"allow_live_comments\")\n    val allowLiveComments: Boolean? = null,\n\n    @SerializedName(\"selftext_html\")\n    val selftextHTML: Any? = null,\n\n    val likes: Boolean? = null,\n\n    @SerializedName(\"suggested_sort\")\n    val suggestedSort: Any? = null,\n\n    @SerializedName(\"banned_at_utc\")\n    val bannedAtUTC: Any? = null,\n\n    @SerializedName(\"url_overridden_by_dest\")\n    val urlOverriddenByDest: String? = null,\n\n    @SerializedName(\"view_count\")\n    val viewCount: Any? = null,\n\n    val archived: Boolean? = null,\n\n    @SerializedName(\"no_follow\")\n    val noFollow: Boolean? = null,\n\n    @SerializedName(\"is_crosspostable\")\n    val isCrosspostable: Boolean? = null,\n\n    val pinned: Boolean? = null,\n\n    @SerializedName(\"over_18\")\n    val over18: Boolean? = null,\n\n    val preview: Preview? = null,\n\n    @SerializedName(\"all_awardings\")\n    val allAwardings: List<AllAwarding>? = null,\n\n    val awarders: List<Any?>? = null,\n\n    @SerializedName(\"media_only\")\n    val mediaOnly: Boolean? = null,\n\n    @SerializedName(\"can_gild\")\n    val canGild: Boolean? = null,\n\n    val spoiler: Boolean? = null,\n    val locked: Boolean? = null,\n\n    @SerializedName(\"author_flair_text\")\n    val authorFlairText: Any? = null,\n\n    @SerializedName(\"treatment_tags\")\n    val treatmentTags: List<Any?>? = null,\n\n    val visited: Boolean? = null,\n\n    @SerializedName(\"removed_by\")\n    val removedBy: Any? = null,\n\n    @SerializedName(\"num_reports\")\n    val numReports: Any? = null,\n\n    val distinguished: Any? = null,\n\n    @SerializedName(\"subreddit_id\")\n    val subredditID: String? = null,\n\n    @SerializedName(\"author_is_blocked\")\n    val authorIsBlocked: Boolean? = null,\n\n    @SerializedName(\"mod_reason_by\")\n    val modReasonBy: Any? = null,\n\n    @SerializedName(\"removal_reason\")\n    val removalReason: Any? = null,\n\n    @SerializedName(\"link_flair_background_color\")\n    val linkFlairBackgroundColor: String? = null,\n\n    val id: String? = null,\n\n    @SerializedName(\"is_robot_indexable\")\n    val isRobotIndexable: Boolean? = null,\n\n    @SerializedName(\"report_reasons\")\n    val reportReasons: Any? = null,\n\n    val author: String? = null,\n\n    @SerializedName(\"discussion_type\")\n    val discussionType: Any? = null,\n\n    @SerializedName(\"num_comments\")\n    val numComments: Long? = null,\n\n    @SerializedName(\"send_replies\")\n    val sendReplies: Boolean? = null,\n\n    @SerializedName(\"whitelist_status\")\n    val whitelistStatus: String? = null,\n\n    @SerializedName(\"contest_mode\")\n    val contestMode: Boolean? = null,\n\n    @SerializedName(\"mod_reports\")\n    val modReports: List<Any?>? = null,\n\n    @SerializedName(\"author_patreon_flair\")\n    val authorPatreonFlair: Boolean? = null,\n\n    @SerializedName(\"author_flair_text_color\")\n    val authorFlairTextColor: Any? = null,\n\n    val permalink: String? = null,\n\n    @SerializedName(\"parent_whitelist_status\")\n    val parentWhitelistStatus: String? = null,\n\n    val stickied: Boolean? = null,\n    val url: String? = null,\n\n    @SerializedName(\"subreddit_subscribers\")\n    val subredditSubscribers: Long? = null,\n\n    @SerializedName(\"created_utc\")\n    val createdUTC: Long? = null,\n\n    @SerializedName(\"num_crossposts\")\n    val numCrossposts: Long? = null,\n\n    val media: Any? = null,\n\n    @SerializedName(\"is_video\")\n    val isVideo: Boolean? = null\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/PostItem.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class PostItem(\n    var kind: String,\n    var data: PostData\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/PostListing.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class PostListing(\n    var kind: String,\n    var data: ListingItem<PostItem>\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/Preview.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class Preview(\n    val images: ArrayList<Image>?\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/ResizedIcon.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class ResizedIcon (\n    val url: String,\n    val width: Long,\n    val height: Long\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/SecureMedia.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class SecureMedia(\n    var reddit_video: RedditVideo\n)\n\ndata class RedditVideo(\n    var bitrate_kbps: Long,\n    var fallback_url: String,\n    var height: Long,\n    var width: Long,\n    var hls_url: String,\n    var is_gif: Boolean\n)"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/SubredditData.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\nimport com.google.gson.annotations.SerializedName\n\ndata class SubredditData(\n    val title: String,\n    @SerializedName(\"display_name\")\n    val displayName: String,\n    @SerializedName(\"display_name_prefixed\")\n    val displayNamePrefixed: String,\n    @SerializedName(\"description_html\")\n    val descriptionHtml: String,\n    val description: String,\n    val created: Long,\n    val over18: Boolean,\n    val url: String,\n    @SerializedName(\"community_icon\")\n    val iconUrl: String,\n    val subscribers: Long,\n    val public_description: String\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/SubredditInfo.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class SubredditInfo(\n    var data: SubredditData\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/SubredditItem.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class SubredditItem(\n    val kind: String,\n    val data: SubredditData\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/UserAbout.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class UserAboutListing(\n    var kind: String,\n    var data: UserAbout\n)\n\ndata class UserAbout(\n    var id: String? = null,\n    var snoovatar_img: String? = null,\n    var icon_img: String? = null,\n    var name: String? = null,\n    var subreddit: UserSubredditData? = null,\n    var is_gold: Boolean? = null,\n    var total_karma: Long? = null,\n    var awardee_karma: Long? = null,\n    var link_karma: Long? = null,\n    var awarder_karma: Long? = null,\n    var comment_karma: Long? = null,\n    var has_verified_email: Boolean? = null,\n    var accept_chats: Boolean? = null,\n    var created_utc: Long? = null,\n    var accept_followers: Boolean? = null,\n    var accept_pms: Boolean? = null,\n    var verified: Boolean? = null\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/model/reddit/UserSubredditData.kt",
    "content": "package com.pineapple.app.network.model.reddit\n\ndata class UserSubredditData(\n    var banner_img: String,\n    var display_name: String,\n    var over_18: Boolean,\n    var icon_img: String,\n    var public_description: String,\n    var subreddit_type: String,\n    var user_is_subscriber: Boolean,\n    var display_name_prefixed: String,\n    var is_default_icon: Boolean\n)\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/paging/CommentsRemoteMediator.kt",
    "content": "package com.pineapple.app.network.paging\n\nimport androidx.paging.ExperimentalPagingApi\nimport androidx.paging.LoadType\nimport androidx.paging.PagingState\nimport androidx.paging.RemoteMediator\nimport androidx.room.withTransaction\nimport com.google.gson.Gson\nimport com.google.gson.JsonObject\nimport com.google.gson.reflect.TypeToken\nimport com.pineapple.app.network.api.RedditApi\nimport com.pineapple.app.network.caching.AppDatabase\nimport com.pineapple.app.network.caching.entity.CommentEntity\nimport com.pineapple.app.network.caching.entity.UserEntity\nimport com.pineapple.app.network.model.cache.CommentWithUser\nimport com.pineapple.app.network.model.reddit.CommentPreData\nimport com.pineapple.app.network.model.reddit.Listing\nimport java.util.concurrent.atomic.AtomicInteger\n\n@OptIn(ExperimentalPagingApi::class)\nclass CommentsRemoteMediator(\n    private val redditApi: RedditApi,\n    private val db: AppDatabase,\n    private val postId: String\n) : RemoteMediator<Int, CommentWithUser>() {\n\n    private val gson = Gson()\n\n    override suspend fun load(loadType: LoadType, state: PagingState<Int, CommentWithUser>): MediatorResult {\n        return try {\n            // For comments we generally only support REFRESH loads (comments are hierarchical)\n            if (loadType == LoadType.PREPEND) {\n                return MediatorResult.Success(endOfPaginationReached = true)\n            }\n\n            val response = redditApi.fetchCommentsByPostId(postId)\n\n            // The first listing is the post itself (t3), the second is the comments (t1)\n            val commentsListing = response.getOrNull(1) \n                ?: return MediatorResult.Success(endOfPaginationReached = true)\n            \n            // We expect the second item to be a Listing<CommentPreData>\n            val children = commentsListing.data.children\n\n            val startIndex = db.commentDao().maxSortKeyForPost(postId)?.plus(1) ?: 0\n            val sortKeyCounter = AtomicInteger(startIndex)\n\n            val out = mutableListOf<CommentEntity>()\n            \n            // IMPORTANT: Start depth at 0. Root comments are Depth 0.\n            processComments(children, postId, null, 0, out, sortKeyCounter)\n\n            db.withTransaction {\n                if (out.isNotEmpty()) db.commentDao().upsertAll(out)\n\n                // Insert placeholder users for authors we don't have locally to avoid blocking UI\n                val authorNames = out.mapNotNull { it.author }.distinct()\n                val existingUsers = authorNames.mapNotNull { db.userDao().getUser(it) }.associateBy { it.name }\n                val missing = authorNames.filter { it !in existingUsers }\n                if (missing.isNotEmpty()) {\n                    val placeholders = missing.map { name -> UserEntity(name = name, iconUrl = \"\", snoovatarUrl = \"\") }\n                    db.userDao().insertAll(placeholders)\n                }\n            }\n\n            MediatorResult.Success(endOfPaginationReached = true)\n        } catch (e: Exception) {\n            MediatorResult.Error(e)\n        }\n    }\n\n    private fun processComments(\n        children: List<CommentPreData>,\n        postId: String,\n        parentId: String?,\n        depth: Int,\n        out: MutableList<CommentEntity>,\n        sortKeyCounter: AtomicInteger\n    ) {\n        for (child in children) {\n            if (child.kind == \"t1\") {\n                val d = child.data\n                val id = d.id\n                if (id.isEmpty()) continue\n                \n                val entityId = \"t1_$id\"\n                val author = d.author\n                val body = d.body\n                val bodyHtml = d.body_html\n                val ups = try { d.ups?.toInt() ?: 0 } catch (_: Throwable) { 0 }\n                val created = d.created_utc?.toLong()\n                val saved = d.saved\n                val likes = d.likes\n                val permalink = d.permalink\n                val sortKey = sortKeyCounter.getAndIncrement()\n\n                // Parse replies to calculate count and recurse\n                var replyChildren: List<CommentPreData> = emptyList()\n                d.replies?.let { repliesElement ->\n                    if (repliesElement is JsonObject) {\n                        try {\n                            val type = object : TypeToken<Listing<CommentPreData>>() {}.type\n                            val listing = gson.fromJson<Listing<CommentPreData>>(repliesElement, type)\n                            replyChildren = listing.data.children\n                        } catch (e: Exception) {\n                            // Ignore parsing errors for replies\n                        }\n                    }\n                }\n                \n                val replyCount = replyChildren.count { it.kind == \"t1\" }\n\n                out.add(\n                    CommentEntity(\n                        id = entityId,\n                        postId = postId,\n                        parentId = parentId,\n                        author = author,\n                        body = body,\n                        bodyHtml = bodyHtml,\n                        ups = ups,\n                        sortKey = sortKey,\n                        depth = depth,\n                        replyCount = replyCount,\n                        createdUtc = created,\n                        saved = saved,\n                        likes = likes,\n                        permalink = permalink\n                    )\n                )\n\n                if (replyChildren.isNotEmpty()) {\n                    // Recurse: Ensure we increment depth\n                    processComments(replyChildren, postId, entityId, depth + 1, out, sortKeyCounter)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/paging/PagingRepository.kt",
    "content": "package com.pineapple.app.network.paging\n\nimport androidx.paging.ExperimentalPagingApi\nimport androidx.paging.Pager\nimport androidx.paging.PagingConfig\nimport com.pineapple.app.network.api.RedditApi\nimport com.pineapple.app.network.caching.AppDatabase\nimport com.pineapple.app.network.model.cache.PostWithUser\nimport com.pineapple.app.network.model.cache.CommentWithUser\nimport javax.inject.Inject\n\n\n@OptIn(ExperimentalPagingApi::class)\nclass PagingRepository @Inject constructor(\n    private val db: AppDatabase,\n    private val redditApi: RedditApi\n) {\n\n    fun postsPager(\n        subreddit: String,\n        sort: String,\n        time: String\n    ): Pager<Int, PostWithUser> {\n        return Pager(\n            config = PagingConfig(\n                pageSize = 25,\n                enablePlaceholders = false\n            ),\n            remoteMediator = PostsRemoteMediator(\n                redditApi = redditApi,\n                db = db,\n                subreddit = subreddit,\n                sort = sort,\n                time = time\n            ),\n            pagingSourceFactory = { db.postDao().pagingSourceWithUser() }\n        )\n    }\n\n    fun searchPostsPager(query: String, sort: String? = null, time: String? = null): Pager<Int, PostWithUser> {\n        return Pager(\n            config = PagingConfig(\n                pageSize = 25,\n                enablePlaceholders = false\n            ),\n            remoteMediator = SearchRemoteMediator(\n                redditApi = redditApi,\n                db = db,\n                query = query,\n                sort = sort,\n                time = time\n            ),\n            pagingSourceFactory = { db.postDao().pagingSourceForSearchQuery(query) }\n        )\n    }\n\n    fun commentsPager(postId: String): Pager<Int, CommentWithUser> {\n        return Pager<Int, CommentWithUser>(\n             config = PagingConfig(\n                 pageSize = 25,\n                 enablePlaceholders = false\n             ),\n             remoteMediator = CommentsRemoteMediator(\n                 redditApi = redditApi,\n                 db = db,\n                 postId = postId\n             ),\n             pagingSourceFactory = { db.commentDao().pagingSourceForPost(postId) }\n         )\n     }\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/paging/PostsRemoteMediator.kt",
    "content": "package com.pineapple.app.network.paging\n\nimport androidx.paging.ExperimentalPagingApi\nimport androidx.paging.LoadType\nimport androidx.paging.PagingState\nimport androidx.paging.RemoteMediator\nimport androidx.room.withTransaction\nimport com.pineapple.app.network.api.RedditApi\nimport com.pineapple.app.network.caching.AppDatabase\nimport com.pineapple.app.network.caching.entity.PostEntity\nimport com.pineapple.app.network.caching.entity.RemoteKeyEntity\nimport com.pineapple.app.network.caching.entity.UserEntity\nimport com.pineapple.app.network.model.cache.PostWithUser\n\n@OptIn(ExperimentalPagingApi::class)\nclass PostsRemoteMediator(\n    private val redditApi: RedditApi,\n    private val db: AppDatabase,\n    private val subreddit: String,\n    private val sort: String,\n    private val time: String\n) : RemoteMediator<Int, PostWithUser>() {\n\n    override suspend fun load(\n        loadType: LoadType,\n        state: PagingState<Int, PostWithUser>\n    ): MediatorResult {\n        return try {\n            val pageKeyData = when (loadType) {\n                LoadType.REFRESH -> null\n                LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)\n                LoadType.APPEND -> {\n                    val lastItem = state.lastItemOrNull()\n                    if (lastItem == null) {\n                        null\n                    } else {\n                        db.remoteKeyDao().remoteKeysPostId(lastItem.post.id)?.nextKey\n                    }\n                }\n            }\n\n            val response = redditApi.fetchSubreddit(\n                name = subreddit,\n                sort = sort,\n                time = time,\n                after = pageKeyData,\n                rawJson = 1,\n                limit = state.config.pageSize\n            )\n\n            val posts = response.data.children\n            val endOfPaginationReached = posts.isEmpty()\n            val startIndex = when (loadType) {\n                LoadType.REFRESH -> 0\n                LoadType.APPEND -> {\n                    db.postDao().maxSortKey() ?: 0\n                }\n                else -> 0\n            }\n\n            val entities = posts.mapIndexed { index, item ->\n                val d = item.data\n                val apiIndex = startIndex + index\n\n                val source = d.preview?.images?.firstOrNull()?.source\n                val previewUrl = source?.url?.replace(\"amp;\", \"\")\n                val previewWidth = source?.width\n                val previewHeight = source?.height\n\n                val normalizedId = d.name ?: \"t3_${d.id}\"\n\n                PostEntity(\n                    id = normalizedId,\n                    title = d.title.orEmpty(),\n                    author = d.author,\n                    subreddit = d.subreddit,\n                    createdUtc = d.createdUTC ?: 0L,\n                    ups = d.ups?.toInt(),\n                    thumbnail = d.thumbnail,\n                    permalink = d.permalink.orEmpty(),\n                    url = d.url,\n                    previewImageUrl = previewUrl,\n                    previewWidth = previewWidth,\n                    previewHeight = previewHeight,\n                    sortKey = apiIndex,\n                    saved = d.saved,\n                    likes = d.likes,\n                    selftext = d.selftext\n                )\n            }\n\n\n            val after = response.data.after\n            val keys = entities.map {\n                RemoteKeyEntity(\n                    postId = it.id,\n                    prevKey = null,\n                    nextKey = after\n                )\n            }\n\n            val authorNames = entities.mapNotNull { it.author }.distinct()\n\n            db.withTransaction {\n\n                if (loadType == LoadType.REFRESH) {\n                    db.remoteKeyDao().clearRemoteKeys()\n                    db.postDao().clearAll()\n                    // optional: db.userDao().clearAll() if you want wipe\n                }\n\n                db.postDao().insertAll(entities)\n                db.remoteKeyDao().insertAll(keys)\n\n                val existingUsers = authorNames.mapNotNull { db.userDao().getUser(it) }\n                    .associateBy { it.name }\n\n                val missingAuthors = authorNames.filter { it !in existingUsers }\n\n                val newUsers = missingAuthors.mapNotNull { author ->\n                    try {\n                        val about = redditApi.fetchUserInfo(author)\n                        UserEntity(\n                            name = about.data.name.toString(),\n                            iconUrl = about.data.icon_img,\n                            snoovatarUrl = about.data.snoovatar_img\n                        )\n                    } catch (_: Exception) {\n                        null\n                    }\n                }\n\n                if (newUsers.isNotEmpty()) {\n                    db.userDao().insertAll(newUsers)\n                }\n            }\n\n\n            MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)\n        } catch (e: Exception) {\n            MediatorResult.Error(e)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/paging/SearchRemoteMediator.kt",
    "content": "package com.pineapple.app.network.paging\n\nimport androidx.paging.ExperimentalPagingApi\nimport androidx.paging.LoadType\nimport androidx.paging.PagingState\nimport androidx.paging.RemoteMediator\nimport androidx.room.withTransaction\nimport com.pineapple.app.network.api.RedditApi\nimport com.pineapple.app.network.caching.AppDatabase\nimport com.pineapple.app.network.caching.entity.PostEntity\nimport com.pineapple.app.network.caching.entity.RemoteKeyEntity\nimport com.pineapple.app.network.caching.entity.SearchRemoteKeyEntity\nimport com.pineapple.app.network.caching.entity.SearchResultEntity\nimport com.pineapple.app.network.caching.entity.UserEntity\nimport com.pineapple.app.network.model.cache.PostWithUser\n\nprivate const val SEARCH_SORT_OFFSET = 1_000_000\n\n@OptIn(ExperimentalPagingApi::class)\nclass SearchRemoteMediator(\n    private val redditApi: RedditApi,\n    private val db: AppDatabase,\n    private val query: String,\n    private val sort: String?,\n    private val time: String?\n) : RemoteMediator<Int, PostWithUser>() {\n\n    override suspend fun load(\n        loadType: LoadType,\n        state: PagingState<Int, PostWithUser>\n    ): MediatorResult {\n        return try {\n            val pageKeyData = when (loadType) {\n                LoadType.REFRESH -> null\n                LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)\n                LoadType.APPEND -> {\n                    val lastItem = state.lastItemOrNull()\n                    if (lastItem == null) null\n                    else db.searchRemoteKeyDao().remoteKeysPostId(query, lastItem.post.id)\n                }\n            }\n\n            val response = redditApi.searchPosts(\n                query = query,\n                sort = sort,\n                time = time,\n                after = pageKeyData,\n                rawJson = 1,\n                limit = state.config.pageSize\n            )\n\n            val posts = response.data.children\n            val endOfPaginationReached = posts.isEmpty()\n\n            val startIndex = when (loadType) {\n                LoadType.REFRESH -> 0\n                LoadType.APPEND -> {\n                    // continue from max sortKey for search (relative)\n                    db.searchResultDao().getPostIdsForQuery(query).size\n                }\n                else -> 0\n            }\n\n            val entities = posts.mapIndexed { index, item ->\n                val d = item.data\n                val apiIndex = startIndex + index\n\n                val source = d.preview?.images?.firstOrNull()?.source\n                val previewUrl = source?.url?.replace(\"amp;\", \"\")\n\n                // offset the sortKey so search-inserted posts don't collide with home feed ordering\n                val postSortKey = apiIndex + SEARCH_SORT_OFFSET\n\n                val normalizedId = d.name ?: \"t3_${d.id}\"\n\n                PostEntity(\n                    id = normalizedId,\n                    title = d.title.orEmpty(),\n                    author = d.author,\n                    subreddit = d.subreddit,\n                    createdUtc = d.createdUTC ?: 0L,\n                    ups = d.ups?.toInt(),\n                    thumbnail = d.thumbnail,\n                    permalink = d.permalink.orEmpty(),\n                    url = d.url,\n                    previewImageUrl = previewUrl,\n                    previewWidth = source?.width,\n                    previewHeight = source?.height,\n                    sortKey = postSortKey,\n                    saved = d.saved,\n                    likes = d.likes,\n                    selftext = d.selftext\n                )\n            }\n\n            val after = response.data.after\n            val keys = entities.map {\n                SearchRemoteKeyEntity(query = query, postId = it.id, prevKey = null, nextKey = after)\n            }\n\n            val authorNames = entities.mapNotNull { it.author }.distinct()\n\n            db.withTransaction {\n                if (loadType == LoadType.REFRESH) {\n                    // clear only search-specific tables to avoid wiping main feed cache\n                    db.searchRemoteKeyDao().clearRemoteKeysForQuery(query)\n                    db.searchResultDao().clearQuery(query)\n                }\n                db.postDao().insertAll(entities)\n\n                // insert mapping rows for this query so we can page search results separately\n                val mappings = entities.mapIndexed { index, post ->\n                    SearchResultEntity(query = query, postId = post.id, sortKey = index + startIndex)\n                }\n                db.searchResultDao().insertAll(mappings)\n\n                db.searchRemoteKeyDao().insertAll(keys)\n\n                val existingUsers = authorNames.mapNotNull { db.userDao().getUser(it) }\n                    .associateBy { it.name }\n\n                val missingAuthors = authorNames.filter { it !in existingUsers }\n\n                val newUsers = missingAuthors.mapNotNull { author ->\n                    try {\n                        val about = redditApi.fetchUserInfo(author)\n                        UserEntity(name = about.data.name.toString(), iconUrl = about.data.icon_img, snoovatarUrl = about.data.snoovatar_img)\n                    } catch (_: Exception) {\n                        null\n                    }\n                }\n\n                if (newUsers.isNotEmpty()) db.userDao().insertAll(newUsers)\n            }\n\n            MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)\n        } catch (e: Exception) {\n            MediatorResult.Error(e)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/repository/RedditAuthRepository.kt",
    "content": "package com.pineapple.app.network.repository\n\nimport com.pineapple.app.consts.MMKVKey\nimport com.pineapple.app.network.api.RedditApi\nimport com.pineapple.app.network.api.RedditTokenApi\nimport com.pineapple.app.network.caching.AppDatabase\nimport com.tencent.mmkv.MMKV\nimport okhttp3.Credentials\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\nconst val USER_AGENT = \"android:com.pineapple.app:v1.0-beta (TEST)\"\n\n@Singleton\nclass RedditAuthRepository @Inject constructor(\n    private val tokenApi: RedditTokenApi,\n    private val mmkv: MMKV,\n) {\n\n    private var _accessToken: String? = null\n    private var _refreshToken: String? = null\n    private var _storedClientId: String? = null\n    private var _tokenType = \"bearer\"\n\n    val accessToken: String? get() = _accessToken\n    val clientId: String? get() = _storedClientId\n\n    val isUserless: Boolean get() = mmkv.decodeBool(MMKVKey.USER_GUEST, true)\n    val isAuthenticated: Boolean get() = _accessToken != null && !isTokenExpired()\n\n    init {\n        loadStoredTokens()\n        if (isTokenExpired()) {\n            _accessToken = null\n            _refreshToken = null\n        }\n    }\n\n    /**\n     * Ensures we have a valid access token in memory/storage.\n     * The interceptor will read [accessToken] and prepend the type.\n     */\n    suspend fun ensureValidToken(clientId: String? = _storedClientId) {\n        _storedClientId = clientId ?: _storedClientId\n        if (_accessToken.isNullOrBlank() || isTokenExpired()) {\n            if (_refreshToken != null && !isUserless) {\n                refreshAccessToken()\n            } else if (!isUserless || mmkv.decodeString(MMKVKey.API_LOGIN_AUTH_CODE)\n                    ?.isNotBlank() == true\n            ) {\n                authenticateUser()\n            } else {\n                authenticateUserless()\n            }\n        }\n    }\n\n    /**\n     * Get an access token if we have gotten an auth code from Reddit OAuth login flow\n     */\n    suspend fun authenticateUser() {\n        val authCode = mmkv.decodeString(MMKVKey.API_LOGIN_AUTH_CODE)\n            ?: throw Exception(\"No auth code available for user login\")\n        val response = tokenApi.authenticateUser(\n            basicAuth = Credentials.basic(_storedClientId!!, \"\"),\n            authCode = authCode\n        )\n        if (response.isSuccessful) {\n            val auth = response.body()!!\n            saveTokens(auth.accessToken, auth.refreshToken, auth.expires, auth.tokenType)\n            mmkv.encode(MMKVKey.USER_GUEST, false)\n        } else {\n            throw Exception(\"User auth failed: ${response.message()}\")\n        }\n    }\n\n    /**\n     * Get an access token without logging in with Reddit\n     */\n    suspend fun authenticateUserless(\n        clientId: String? = _storedClientId,\n        testingClientID: Boolean = false\n    ) {\n        _storedClientId = clientId\n        val response = tokenApi.authenticateUserless(\n            basicAuth = Credentials.basic(_storedClientId!!, \"\")\n        )\n        if (response.isSuccessful) {\n            if (!testingClientID) {\n                val auth = response.body()!!\n                saveTokens(auth.accessToken, auth.refreshToken, auth.expires, auth.tokenType)\n            }\n        } else {\n            throw Exception(\"Userless auth failed: ${response.message()}\")\n        }\n    }\n\n    /**\n     * Using a previously obtained refresh token, get a new access token that is valid\n     */\n    private suspend fun refreshAccessToken() {\n        val response = tokenApi.refreshAccessToken(\n            basicAuth = Credentials.basic(_storedClientId!!, \"\"),\n            refreshToken = _refreshToken!!\n        )\n        if (response.isSuccessful) {\n            val auth = response.body()!!\n            saveTokens(auth.accessToken, auth.refreshToken, auth.expires, auth.tokenType)\n        } else {\n            _refreshToken = null\n            authenticateUserless()\n        }\n    }\n\n    /**\n     * Update the MMKV table with our most up to date token and authentication information\n     */\n    private fun saveTokens(\n        accessToken: String,\n        refreshToken: String?,\n        expiresIn: Long,\n        tokenType: String?\n    ) {\n        _accessToken = accessToken\n        _refreshToken = refreshToken\n        _tokenType = tokenType ?: \"bearer\"\n        mmkv.encode(MMKVKey.ACCESS_TOKEN, accessToken)\n        mmkv.encode(MMKVKey.REFRESH_TOKEN, refreshToken)\n        mmkv.encode(MMKVKey.TOKEN_EXPIRES, System.currentTimeMillis() + expiresIn * 1000)\n        mmkv.encode(MMKVKey.CLIENT_ID, _storedClientId)\n        mmkv.encode(MMKVKey.TOKEN_TYPE, _tokenType)\n    }\n\n    /**\n     * Load any stored tokens from MMKV into memory\n     */\n    private fun loadStoredTokens() {\n        _accessToken = mmkv.decodeString(MMKVKey.ACCESS_TOKEN)\n        _refreshToken = mmkv.decodeString(MMKVKey.REFRESH_TOKEN)\n        _storedClientId = mmkv.decodeString(MMKVKey.CLIENT_ID)\n        _tokenType = mmkv.decodeString(MMKVKey.TOKEN_TYPE, \"bearer\") ?: \"bearer\"\n    }\n\n    /**\n     * Check the time that our access token expires against the current time to determine\n     * if we need to request a new one or refresh it\n     */\n    private fun isTokenExpired(): Boolean {\n        return System.currentTimeMillis() > mmkv.decodeLong(MMKVKey.TOKEN_EXPIRES, 0)\n    }\n\n    /**\n     * Optional helper if the interceptor wants the full header value.\n     */\n    fun authorizationHeaderOrNull(): String? =\n        _accessToken?.let { \"$_tokenType $it\" }\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/repository/RedditRepository.kt",
    "content": "package com.pineapple.app.network.repository\n\nimport com.pineapple.app.network.api.RedditApi\nimport com.pineapple.app.network.caching.AppDatabase\nimport com.pineapple.app.network.caching.entity.CommentEntity\nimport com.pineapple.app.network.caching.entity.PostEntity\nimport com.pineapple.app.network.caching.entity.SubredditEntity\nimport com.pineapple.app.network.caching.entity.UserEntity\nimport com.pineapple.app.network.model.cache.PostWithUser\nimport com.pineapple.app.network.model.reddit.SubredditData\nimport com.pineapple.app.network.model.reddit.UserAbout\nimport com.pineapple.app.network.model.reddit.CommentPreData\nimport com.pineapple.app.utilities.toSubredditEntity\nimport com.tencent.mmkv.MMKV\nimport kotlinx.coroutines.flow.Flow\nimport javax.inject.Inject\nimport javax.inject.Singleton\nimport com.google.gson.JsonElement\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport com.google.gson.Gson\nimport com.google.gson.reflect.TypeToken\nimport com.pineapple.app.network.model.reddit.Listing\nimport com.google.gson.JsonObject\n\n\n@Singleton\nclass RedditRepository @Inject constructor(\n    private val redditApi: RedditApi,\n    private val mmkv: MMKV,\n    db: AppDatabase\n) {\n\n    private val subredditDao = db.subredditDao()\n    private val postDao = db.postDao()\n    private val userDao = db.userDao()\n    private val commentDao = db.commentDao()\n    private val gson = Gson()\n\n    fun observePostWithUser(postId: String): Flow<PostWithUser?> =\n        postDao.getPostWithUserFlow(\"t3_$postId\")\n\n    /**\n     * Refresh replies for a given comment by fetching the post's comments and extracting replies\n     * that belong to the supplied commentId. Inserts reply CommentEntity rows with parentId set.\n     * Returns the number of replies parsed/inserted, or -1 on error.\n     */\n    suspend fun refreshRepliesForComment(postId: String, commentId: String): Int = withContext(Dispatchers.IO) {\n        try {\n            val response = redditApi.fetchCommentsByPostId(postId)\n\n            // defensively extract children list from the second element of the response\n            val commentChildren = run {\n                val second = response.getOrNull(1)\n                when (second) {\n                    is com.pineapple.app.network.model.reddit.Listing<*> -> (second.data.children as? List<*>) ?: emptyList()\n                    is Map<*, *> -> ((second[\"data\"] as? Map<*, *>)?.get(\"children\") as? List<*>) ?: emptyList()\n                    else -> emptyList<Any>()\n                }\n            }\n\n            // compute a starting sortKey once to avoid suspending calls during traversal\n            var sortCounter = commentDao.maxSortKeyForPost(postId)?.plus(1) ?: 0\n\n            // traverse the commentChildren and collect all nested replies that are direct children of the desired comment\n            val out = mutableListOf<CommentEntity>()\n\n            fun traverseAndCollect(item: Any?, parentFullname: String?) {\n                if (item == null) return\n                when (item) {\n                    is com.pineapple.app.network.model.reddit.ListingItem<*> -> {\n                        val inner = item.children\n                        inner.forEach { cpAny -> traverseAndCollect(cpAny, parentFullname) }\n                    }\n                    is CommentPreData -> {\n                        val d = item.data\n                        val thisFull = \"t1_${d.id}\"\n                        // Process nested replies if present\n                        var replyChildren: List<CommentPreData> = emptyList()\n                        d.replies?.let { je ->\n                            if (je.isJsonObject) {\n                                try {\n                                    val type = object : TypeToken<Listing<CommentPreData>>() {}.type\n                                    val listing = gson.fromJson<Listing<CommentPreData>>(je, type)\n                                    replyChildren = listing.data.children\n                                    replyChildren.forEach {\n                                        traverseAndCollect(it, thisFull)\n                                    }\n                                } catch (e: Exception) {\n                                    // ignore\n                                }\n                            }\n                        }\n                    }\n                    is Map<*, *> -> {\n                        val data = item[\"data\"] as? Map<*, *>\n                        if (data == null) return\n                        val id = data[\"id\"] as? String ?: return\n                        val thisFull = \"t1_$id\"\n                        val parentIdRaw = data[\"parent_id\"] as? String\n                        val parentFull = parentIdRaw\n                        \n                        // Parse replies to calculate count\n                        var replyCount = 0\n                        val repliesAny = data[\"replies\"]\n                         if (repliesAny is Map<*, *>) {\n                             val rdata = (repliesAny[\"data\"] as? Map<*, *>)?.get(\"children\") as? List<*>\n                             replyCount = rdata?.size ?: 0\n                         }\n                        \n                        // if the parent matches the target comment's fullname (t1_$commentId), collect this as a reply\n                        if (parentFull == \"t1_$commentId\") {\n                            val author = data[\"author\"] as? String\n                            val body = data[\"body\"] as? String\n                            val bodyHtml = data[\"body_html\"] as? String\n                            val ups = try { ((data[\"ups\"] as? Number)?.toLong() ?: 0L).toInt() } catch (_: Throwable) { 0 }\n                            val created = (data[\"created_utc\"] as? Number)?.toLong()\n                            val saved = data[\"saved\"] as? Boolean\n                            val likes = data[\"likes\"] as? Boolean\n                            val permalink = data[\"permalink\"] as? String\n                            val sortKey = sortCounter++\n                            out.add(\n                                CommentEntity(\n                                    id = thisFull,\n                                    postId = postId,\n                                    parentId = \"t1_$commentId\",\n                                    author = author,\n                                    body = body,\n                                    bodyHtml = bodyHtml,\n                                    ups = ups,\n                                    sortKey = sortKey,\n                                    depth = 1,\n                                    replyCount = replyCount,\n                                    createdUtc = created,\n                                    saved = saved,\n                                    likes = likes,\n                                    permalink = permalink\n                                )\n                            )\n                        }\n\n                        // traverse nested replies if present\n                        if (repliesAny is Map<*, *>) {\n                            val rdata = (repliesAny[\"data\"] as? Map<*, *>)?.get(\"children\") as? List<*>\n                            rdata?.forEach { traverseAndCollect(it, thisFull) }\n                        }\n                    }\n                    is JsonElement -> {\n                        if (item.isJsonObject) {\n                            val obj = item.asJsonObject\n                            val dataObj = obj.getAsJsonObject(\"data\")\n                            val id = dataObj.get(\"id\")?.asString ?: return\n                            val parentRaw = dataObj.get(\"parent_id\")?.asString\n                            \n                            var replyCount = 0\n                            val repliesElement = dataObj.get(\"replies\")\n                            if (repliesElement != null && repliesElement.isJsonObject) {\n                                 val repliesObj = repliesElement.asJsonObject\n                                 val repliesData = repliesObj.getAsJsonObject(\"data\")\n                                 val repliesChildren = repliesData?.getAsJsonArray(\"children\")\n                                 replyCount = repliesChildren?.size() ?: 0\n                            }\n                            \n                            if (parentRaw == \"t1_$commentId\") {\n                                val author = dataObj.get(\"author\")?.asString\n                                val body = dataObj.get(\"body\")?.asString\n                                val bodyHtml = dataObj.get(\"body_html\")?.asString\n                                val ups = try { dataObj.get(\"ups\")?.asInt ?: 0 } catch (_: Throwable) { 0 }\n                                val created = dataObj.get(\"created_utc\")?.asLong\n                                val saved = dataObj.get(\"saved\")?.asBoolean\n                                val likes = dataObj.get(\"likes\")?.asBoolean\n                                val permalink = dataObj.get(\"permalink\")?.asString\n                                val sortKey = sortCounter++\n                                out.add(\n                                    CommentEntity(\n                                        id = \"t1_$id\",\n                                        postId = postId,\n                                        parentId = \"t1_$commentId\",\n                                        author = author,\n                                        body = body,\n                                        bodyHtml = bodyHtml,\n                                        ups = ups,\n                                        sortKey = sortKey,\n                                        depth = 1,\n                                        replyCount = replyCount,\n                                        createdUtc = created,\n                                        saved = saved,\n                                        likes = likes,\n                                        permalink = permalink\n                                    )\n                                )\n                            }\n\n                            // traverse nested children listing if present\n                            val data = obj.getAsJsonObject(\"data\")\n                            val children = data?.getAsJsonArray(\"children\")\n                            children?.forEach { traverseAndCollect(it, \"t1_$id\") }\n                        }\n                    }\n                    else -> {\n                        // unknown shape - ignore\n                    }\n                }\n            }\n\n            commentChildren.forEach { traverseAndCollect(it, null) }\n\n            if (out.isNotEmpty()) {\n                commentDao.upsertAll(out)\n            }\n\n            // return number of replies parsed/inserted\n            return@withContext out.size\n\n        } catch (_: Exception) {\n            // swallow\n        }\n\n        return@withContext -1\n    }\n\n    suspend fun refreshPostAndAuthor(postId: String) {\n\n        // Try to find cached post first\n        val cached = postDao.getPost(\"t3_$postId\")\n\n        // Decide how to call the API to get fresh data\n        val raw = try {\n            if (cached == null) {\n                // If we don't have subreddit/slug info, fetch by id endpoint\n                redditApi.fetchPostById(postId)\n            } else {\n                val subreddit = cached.subreddit ?: \"\"\n                val splitPerma = cached.permalink.split(\"/\")\n                val slug = splitPerma.getOrNull(splitPerma.size - 2) ?: \"\"\n                redditApi.fetchPost(\n                    subreddit = subreddit,\n                    postID = postId,\n                    post = slug\n                )\n            }\n        } catch (_: Exception) {\n            // If API fails and we have cached data, don't crash — just return\n            if (cached == null) return else null\n        }\n\n        if (raw == null) return\n\n        // Parse response into your PostEntity and update Room\n        val postListing = raw.firstOrNull() ?: return\n        val freshPostData = postListing.data.children.firstOrNull()?.children?.firstOrNull() ?: return\n\n        // Determine a sortKey: preserve cached if present, otherwise append to end\n        val sortKey = cached?.sortKey ?: ((postDao.maxSortKey() ?: 0) + 1)\n\n        val freshEntity = PostEntity(\n            id = freshPostData.name ?: \"t3_$postId\",\n            title = freshPostData.title.orEmpty(),\n            author = freshPostData.author,\n            subreddit = freshPostData.subreddit,\n            createdUtc = (freshPostData.createdUTC ?: 0.0).toLong(),\n            ups = freshPostData.ups?.toInt() ?: cached?.ups?.toInt(),\n            thumbnail = freshPostData.thumbnail ?: cached?.thumbnail,\n            permalink = freshPostData.permalink ?: cached?.permalink ?: \"\",\n            url = freshPostData.url ?: cached?.url,\n            previewImageUrl = freshPostData.preview?.images?.firstOrNull()?.source?.url\n                ?.replace(\"amp;\", \"\") ?: cached?.previewImageUrl,\n            previewWidth = freshPostData.preview?.images?.firstOrNull()?.source?.width\n                ?: cached?.previewWidth,\n            previewHeight = freshPostData.preview?.images?.firstOrNull()?.source?.height\n                ?: cached?.previewHeight,\n            sortKey = sortKey,\n            saved = freshPostData.saved ?: cached?.saved,\n            likes = freshPostData.likes ?: cached?.likes,\n            selftext = freshPostData.selftext ?: cached?.selftext\n        )\n\n        postDao.upsert(freshEntity)\n\n        // 2) Refresh author info\n        val authorName = freshEntity.author ?: return\n        val userAbout = try {\n            redditApi.fetchUserInfo(user = authorName)\n        } catch (_: Exception) {\n            null\n        }\n        // Map to UserEntity and upsert into userDao\n        if (userAbout != null) {\n            val about = userAbout.data\n            val userEntity = UserEntity(\n                name = about.name ?: authorName,\n                iconUrl = about.icon_img ?: \"\",\n                snoovatarUrl = about.snoovatar_img ?: \"\"\n            )\n            userDao.insertAll(listOf(userEntity))\n        }\n    }\n\n    // New: Update bookmark state via API and persist to cache\n    suspend fun savePost(postIdNoPrefix: String) {\n        val fullId = \"t3_$postIdNoPrefix\"\n        try {\n            redditApi.savePost(fullId)\n        } catch (_: Exception) {\n            // ignore API errors for now\n        }\n\n        val cached = postDao.getPost(fullId)\n        if (cached != null) {\n            val updated = cached.copy(saved = true)\n            postDao.upsert(updated)\n        }\n    }\n\n    suspend fun unsavePost(postIdNoPrefix: String) {\n        val fullId = \"t3_$postIdNoPrefix\"\n        try {\n            redditApi.unsavePost(fullId)\n        } catch (_: Exception) {\n            // ignore API errors for now\n        }\n\n        val cached = postDao.getPost(fullId)\n        if (cached != null) {\n            val updated = cached.copy(saved = false)\n            postDao.upsert(updated)\n        }\n    }\n\n   suspend fun castVoteAndCache(postIdNoPrefix: String, direction: Int, prefix: String = \"t3_\") {\n        val fullId = \"$prefix$postIdNoPrefix\"\n        try {\n            redditApi.castVote(fullId, direction)\n        } catch (_: Exception) {\n            // ignore API errors\n        }\n\n        // determine target dao/entity based on fullname prefix\n        when {\n            fullId.startsWith(\"t3_\") -> {\n                val cached = postDao.getPost(fullId) ?: return\n                val prevLikes = cached.likes\n                val prevValue = when (prevLikes) {\n                    true -> 1\n                    false -> -1\n                    null -> 0\n                }\n                val newValue = when (direction) {\n                    1 -> 1\n                    -1 -> -1\n                    else -> 0\n                }\n                val delta = newValue - prevValue\n                val newUps = (cached.ups ?: 0) + delta\n                val newLikes = when (direction) {\n                    1 -> true\n                    -1 -> false\n                    else -> null\n                }\n                val updated = cached.copy(likes = newLikes, ups = newUps)\n                postDao.upsert(updated)\n            }\n\n            fullId.startsWith(\"t1_\") -> {\n                val cached = commentDao.getComment(fullId) ?: return\n                val prevLikes = cached.comment.likes\n                val prevValue = when (prevLikes) {\n                    true -> 1\n                    false -> -1\n                    null -> 0\n                }\n                val newValue = when (direction) {\n                    1 -> 1\n                    -1 -> -1\n                    else -> 0\n                }\n                val delta = newValue - prevValue\n                val newUps = (cached.comment.ups ?: 0) + delta\n                val newLikes = when (direction) {\n                    1 -> true\n                    -1 -> false\n                    else -> null\n                }\n                val updated = cached.comment.copy(likes = newLikes, ups = newUps)\n                commentDao.upsert(updated)\n            }\n        }\n    }\n\n    fun observePopularSubreddits(): Flow<List<SubredditEntity>> =\n        subredditDao.getPopularSubreddits()\n\n    fun observeSubscribedSubreddits(): Flow<List<SubredditEntity>> =\n        subredditDao.getSubscribedSubreddits()\n\n    suspend fun refreshPopularSubreddits(force: Boolean = false) {\n        if (!force && !shouldRefreshPopularSubreddits()) return\n\n        val listing = redditApi.fetchTopSubreddits(limit = 50)\n        val entities = listing.data.children.map { it.toSubredditEntity(isSubscribed = false) }\n        subredditDao.upsertAll(entities)\n        mmkv.putLong(\"popular_subreddits_last_fetch\", System.currentTimeMillis())\n    }\n\n    suspend fun refreshSubscribedSubreddits(force: Boolean = false) {\n\n        if (!force && !shouldRefreshSubscribedSubreddits()) return\n\n        val listing = redditApi.fetchSubscribedSubreddits(limit = 100)\n        val entities = listing.data.children.map { it.toSubredditEntity(isSubscribed = true) }\n        subredditDao.markAllUnsubscribed()\n        subredditDao.upsertAll(entities)\n        mmkv.putLong(\"subscribed_subreddits_last_fetch\", System.currentTimeMillis())\n    }\n\n    private fun shouldRefreshPopularSubreddits(): Boolean {\n        val last = mmkv.getLong(\"popular_subreddits_last_fetch\", 0L)\n        return System.currentTimeMillis() - last > 3 * 60 * 60 * 1000 // 3h\n    }\n\n    private fun shouldRefreshSubscribedSubreddits(): Boolean {\n        val last = mmkv.getLong(\"subscribed_subreddits_last_fetch\", 0L)\n        return System.currentTimeMillis() - last > 30 * 60 * 1000 // 30min, up to you\n    }\n\n    // Suggest top communities: return the SubredditData model directly\n    suspend fun suggestCommunities(query: String, limit: Int = 3): List<SubredditData> {\n        return try {\n            val resp = redditApi.searchCommunities(query = query, limit = limit)\n            resp.data.children.take(limit).map { it.data }\n        } catch (_: Exception) {\n            emptyList()\n        }\n    }\n\n    // Suggest top users: return the UserAbout model directly\n    suspend fun suggestUsers(query: String, limit: Int = 3): List<UserAbout> {\n        return try {\n            val resp = redditApi.searchUsers(query = query, limit = limit)\n            resp.data.children.take(limit).map { it.data }\n        } catch (_: Exception) {\n            emptyList()\n        }\n    }\n\n    suspend fun refreshCommentsForPost(postId: String) {\n        try {\n            val response = redditApi.fetchCommentsByPostId(postId)\n\n            // defensively extract children list from the second element of the response\n            val commentChildren = run {\n                val second = response.getOrNull(1)\n                when (second) {\n                    is com.pineapple.app.network.model.reddit.Listing<*> -> (second.data.children as? List<*>) ?: emptyList()\n                    is Map<*, *> -> ((second[\"data\"] as? Map<*, *>)?.get(\"children\") as? List<*>) ?: emptyList()\n                    else -> emptyList<Any>()\n                }\n            }\n\n            val startIndex = commentDao.maxSortKeyForPost(postId)?.plus(1) ?: 0\n\n            val out = mutableListOf<CommentEntity>()\n            var counter = startIndex\n\n            // parse a variety of possible shapes for individual comment items\n            commentChildren.forEach { itemAny ->\n                when (itemAny) {\n                    is com.pineapple.app.network.model.reddit.ListingItem<*> -> {\n                        val inner = itemAny.children\n                        inner.forEach { cpAny ->\n                            val cp = cpAny as? CommentPreData ?: return@forEach\n                            if (cp.kind != \"t1\") return@forEach\n                            val d = cp.data\n                            val id = d.id.ifEmpty { return@forEach }\n                            val author = d.author\n                            val body = d.body\n                            val bodyHtml = d.body_html\n                            val ups = try { d.ups?.toInt() ?: 0 } catch (_: Throwable) { 0 }\n                            val created = d.created_utc?.toLong()\n                            val saved = d.saved\n                            val likes = d.likes\n                            val permalink = d.permalink\n                            val sortKey = counter++\n                            \n                            // Parse replies to calculate count\n                            var replyCount = 0\n                            d.replies?.let { je ->\n                                if (je.isJsonObject) {\n                                    try {\n                                        val type = object : TypeToken<Listing<CommentPreData>>() {}.type\n                                        val listing = gson.fromJson<Listing<CommentPreData>>(je, type)\n                                        val replyChildren = listing.data.children\n                                        replyCount = replyChildren.count { it.kind == \"t1\" }\n                                    } catch (e: Exception) {\n                                        // ignore\n                                    }\n                                }\n                            }\n                            \n                            out.add(\n                                CommentEntity(\n                                    id = \"t1_$id\",\n                                    postId = postId,\n                                    parentId = null,\n                                    author = author,\n                                    body = body,\n                                    bodyHtml = bodyHtml,\n                                    ups = ups,\n                                    sortKey = sortKey,\n                                    depth = 0,\n                                    replyCount = replyCount,\n                                    createdUtc = created,\n                                    saved = saved,\n                                    likes = likes,\n                                    permalink = permalink\n                                )\n                            )\n                        }\n                    }\n                    is CommentPreData -> {\n                        val cp = itemAny\n                        if (cp.kind != \"t1\") return@forEach\n                        val d = cp.data\n                        val id = d.id.ifEmpty { return@forEach }\n                        val author = d.author\n                        val body = d.body\n                        val bodyHtml = d.body_html\n                        val ups = try { d.ups?.toInt() ?: 0 } catch (_: Throwable) { 0 }\n                        val created = d.created_utc?.toLong()\n                        val saved = d.saved\n                        val likes = d.likes\n                        val permalink = d.permalink\n                        val sortKey = counter++\n                        \n                        // Parse replies to calculate count\n                        var replyCount = 0\n                        d.replies?.let { je ->\n                            if (je.isJsonObject) {\n                                try {\n                                    val type = object : TypeToken<Listing<CommentPreData>>() {}.type\n                                    val listing = gson.fromJson<Listing<CommentPreData>>(je, type)\n                                    val replyChildren = listing.data.children\n                                    replyCount = replyChildren.count { it.kind == \"t1\" }\n                                } catch (e: Exception) {\n                                    // ignore\n                                }\n                            }\n                        }\n\n                        out.add(\n                            CommentEntity(\n                                id = \"t1_$id\",\n                                postId = postId,\n                                parentId = null,\n                                author = author,\n                                body = body,\n                                bodyHtml = bodyHtml,\n                                ups = ups,\n                                sortKey = sortKey,\n                                depth = 0,\n                                replyCount = replyCount,\n                                createdUtc = created,\n                                saved = saved,\n                                likes = likes,\n                                permalink = permalink\n                            )\n                        )\n                    }\n                    is Map<*, *> -> {\n                        val kind = itemAny[\"kind\"] as? String\n                        val data = itemAny[\"data\"] as? Map<*, *>\n                        if (kind != \"t1\" || data == null) return@forEach\n                        val id = data[\"id\"] as? String ?: return@forEach\n                        val author = data[\"author\"] as? String\n                        val body = data[\"body\"] as? String\n                        val bodyHtml = data[\"body_html\"] as? String ?: \"\"\n                        val ups = try { ((data[\"ups\"] as? Number)?.toLong() ?: 0L).toInt() } catch (_: Throwable) { 0 }\n                        val created = (data[\"created_utc\"] as? Number)?.toLong()\n                        val saved = data[\"saved\"] as? Boolean\n                        val likes = data[\"likes\"] as? Boolean\n                        val permalink = data[\"permalink\"] as? String\n                        val sortKey = counter++\n                        \n                        // Parse replies to calculate count\n                        var replyCount = 0\n                        val repliesAny = data[\"replies\"]\n                         if (repliesAny is Map<*, *>) {\n                             val rdata = (repliesAny[\"data\"] as? Map<*, *>)?.get(\"children\") as? List<*>\n                             replyCount = rdata?.size ?: 0\n                         }\n\n                        out.add(\n                            CommentEntity(\n                                id = \"t1_$id\",\n                                postId = postId,\n                                parentId = null,\n                                author = author,\n                                body = body,\n                                bodyHtml = bodyHtml,\n                                ups = ups,\n                                sortKey = sortKey,\n                                depth = 0,\n                                replyCount = replyCount,\n                                createdUtc = created,\n                                saved = saved,\n                                likes = likes,\n                                permalink = permalink\n                            )\n                        )\n                    }\n                    else -> {\n                        // unknown shape - skip\n                    }\n                }\n            }\n\n            // persist\n            if (out.isNotEmpty()) {\n                commentDao.upsertAll(out)\n            }\n\n            // Insert placeholder users to avoid fetching each user synchronously (improves performance)\n            val authorNames = out.mapNotNull { it.author }.distinct()\n            val existingUsers = authorNames.mapNotNull { userDao.getUser(it) }.associateBy { it.name }\n            val missing = authorNames.filter { it !in existingUsers }\n\n            if (missing.isNotEmpty()) {\n                val placeholders = missing.map { name ->\n                    UserEntity(name = name, iconUrl = \"\", snoovatarUrl = \"\")\n                }\n                userDao.insertAll(placeholders)\n\n                // Removed background prefetch of up to 20 profiles.\n                // Adopting on-demand fetch: UI components should request full user info (avatars) when rows are visible.\n            }\n\n        } catch (_: Exception) {\n            // swallow - minimal behavior\n        }\n    }\n\n    /**\n     * Fetch a user's about info from the API and cache it locally.\n     * This is intended for on-demand fetching (e.g. when a comment row becomes visible).\n     */\n    suspend fun fetchAndCacheUser(username: String) {\n        if (username.isBlank()) return\n        // if user already exists and has an icon, skip\n        val existing = userDao.getUser(username)\n        if (existing != null && !existing.iconUrl.isNullOrEmpty()) return\n\n        try {\n            val about = redditApi.fetchUserInfo(username)\n            val u = about.data\n            val userEntity = UserEntity(\n                name = u.name ?: username,\n                iconUrl = u.icon_img ?: \"\",\n                snoovatarUrl = u.snoovatar_img ?: \"\"\n            )\n            userDao.insertAll(listOf(userEntity))\n        } catch (_: Exception) {\n            // minimal: ignore failures; UI can retry or show placeholder\n        }\n    }\n\n    fun observeRepliesForComment(parentCommentFullId: String) =\n        commentDao.getRepliesForCommentFlow(parentCommentFullId)\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/network/serialization/RedditRepliesAdapter.kt",
    "content": "package com.pineapple.app.network.serialization\n\nimport com.google.gson.*\nimport com.pineapple.app.network.model.reddit.CommentDataNull\nimport java.lang.reflect.Type\n\nclass RedditRepliesAdapter : JsonDeserializer<CommentDataNull?> {\n    override fun deserialize(\n        json: JsonElement,\n        typeOfT: Type,\n        context: JsonDeserializationContext\n    ): CommentDataNull? {\n        // If it's a primitive (like the string \"\"), return null\n        if (json.isJsonPrimitive) {\n            return null\n        }\n        // If it's an object, parse it normally\n        return context.deserialize(json, CommentDataNull::class.java)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/components/ButtonComponents.kt",
    "content": "@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)\n\npackage com.pineapple.app.ui.components\n\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FilledTonalIconToggleButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButtonColors\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.IconToggleButtonColors\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.unit.Dp\nimport com.pineapple.app.ui.theme.FullCornerRadius\nimport com.pineapple.app.ui.theme.MediumCornerRadius\n\n/**\n * Tonal icon toggle button that animates its shape based off of the checked status, using\n * Material 3 Expressive motion physics\n * @param checked Whether the button is checked or not\n * @param onCheckedChange Lambda that is triggered when the button is clicked\n * @param modifier [Modifier] to be applied to the button\n * @param checkedRadius Corner radius when the button is checked\n * @param uncheckedRadius Corner radius when the button is unchecked\n * @param checkedIcon Icon to be displayed when the button is checked\n * @param uncheckedIcon Icon to be displayed when the button is unchecked\n * @param contentDescription Content description for the icon (both states)\n */\n@Composable\nfun AnimatedTonalToggleIconButton(\n    checked: Boolean,\n    onCheckedChange: (Boolean) -> Unit,\n    modifier: Modifier = Modifier,\n    checkedRadius: Dp = FullCornerRadius,\n    uncheckedRadius: Dp = MediumCornerRadius,\n    checkedIcon: Painter,\n    uncheckedIcon: Painter,\n    contentDescription: String,\n    colors: IconToggleButtonColors = IconButtonDefaults.filledTonalIconToggleButtonColors()\n) {\n    val targetRadius = if (checked) checkedRadius else uncheckedRadius\n    val shapeRadius by animateDpAsState(\n        targetValue = targetRadius,\n        animationSpec = MaterialTheme.motionScheme.fastSpatialSpec(),\n        label = \"shapeRadius\"\n    )\n    FilledTonalIconToggleButton(\n        checked = checked,\n        onCheckedChange = onCheckedChange,\n        shape = RoundedCornerShape(shapeRadius),\n        modifier = modifier,\n        colors = colors\n    ) {\n        Icon(\n            painter = if (checked) checkedIcon else uncheckedIcon,\n            contentDescription = contentDescription\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/components/CardComponents.kt",
    "content": "@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)\n\npackage com.pineapple.app.ui.components\n\nimport android.content.Intent\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FilledTonalIconButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport coil3.compose.AsyncImage\nimport com.pineapple.app.R\nimport com.pineapple.app.network.model.cache.CommentWithUser\nimport com.pineapple.app.network.model.reddit.PostData\nimport com.pineapple.app.network.model.reddit.UserAboutListing\nimport com.pineapple.app.ui.theme.FullCornerRadius\nimport com.pineapple.app.ui.theme.MediumCornerRadius\nimport com.pineapple.app.utilities.convertUnixToRelativeTime\nimport com.pineapple.app.utilities.prettyNumber\n\n/**\n * A compact card representing a post to be used in list views\n * @param postData The data of the post to be displayed\n * @param modifier The modifier to be applied to the card\n * @param userInfo User info used to display the author's avatar\n * @param onClick Lambda to be invoked when the card is clicked\n * @param onMoreClick Lambda to be invoked when the more options button is clicked\n * @param onSaveClick Lambda to be invoked when the save button is clicked\n * @param onUpvote Lambda to be invoked when the upvote button is clicked\n * @param onDownvote Lambda to be invoked when the downvote button is clicked\n */\n@Composable\nfun PostCard(\n    postData: PostData,\n    modifier: Modifier = Modifier,\n    userInfo: UserAboutListing? = null,\n    onClick: () -> Unit,\n    onMoreClick: () -> Unit,\n    onSaveClick: (Boolean, () -> Unit) -> Unit,\n    onUpvote: (Boolean, () -> Unit) -> Unit,\n    onDownvote: (Boolean, () -> Unit) -> Unit\n) {\n    val context = LocalContext.current\n    val haptics = LocalHapticFeedback.current\n\n    var bookmarkedState by rememberSaveable { mutableStateOf(postData.saved) }\n    var upvoteState by rememberSaveable { mutableStateOf(postData.likes == true) }\n    var downvoteState by rememberSaveable { mutableStateOf(postData.likes == false) }\n\n    val imageData = postData.preview?.images?.get(0)?.source\n    val width = imageData?.width?.toFloat() ?: 0f\n    val height = imageData?.height?.toFloat() ?: 0f\n    val imageUrl = imageData?.url?.replace(\"amp;\", \"\")?.ifEmpty { postData.url }\n    val computedAspectRatio = if (width > 0f && height > 0f) {\n        (width / height).coerceIn(0.2f, 4f)\n    } else null\n\n    Card(\n        colors = CardDefaults.cardColors(\n            containerColor = MaterialTheme.colorScheme.surfaceContainerLow\n        ),\n        modifier = modifier.fillMaxWidth()\n            .clip(MaterialTheme.shapes.medium)\n            .combinedClickable(\n                enabled = true,\n                onLongClick = {\n                    haptics.performHapticFeedback(HapticFeedbackType.SegmentTick)\n                    onMoreClick()\n                },\n                onClick = onClick\n            ),\n    ) {\n        Row(\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = Modifier.fillMaxWidth().padding(horizontal = 13.dp, vertical = 13.dp)\n        ) {\n            Row(verticalAlignment = Alignment.CenterVertically) {\n                userInfo?.let {\n                    AsyncImage(\n                        model = it.data.snoovatar_img?.ifBlank { null } ?: it.data.icon_img,\n                        contentDescription = null,\n                        modifier = Modifier.clip(CircleShape)\n                            .size(35.dp)\n                    )\n                }\n                Column(modifier = Modifier.padding(start = 10.dp)) {\n                    postData.author?.let {\n                        Text(\n                            text = \"u/$it\",\n                            style = MaterialTheme.typography.titleSmall\n                        )\n                    }\n                    postData.subredditNamePrefixed?.let {\n                        Text(\n                            text = it,\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onSurface\n                        )\n                    }\n                }\n            }\n            postData.createdUTC?.let {\n                Text(\n                    text = it.convertUnixToRelativeTime(),\n                    style = MaterialTheme.typography.labelMedium,\n                    color = MaterialTheme.colorScheme.outline\n                )\n            }\n        }\n        postData.title?.let {\n            Text(\n                text = it,\n                modifier = Modifier.padding(start = 13.dp, end = 13.dp, bottom = 5.dp),\n                style = MaterialTheme.typography.bodyLarge\n            )\n        }\n        if (imageUrl !== null && imageUrl.isNotEmpty()) {\n            var aspectRatio by rememberSaveable(postData.id + \"_ratio\") {\n                mutableStateOf<Float?>(computedAspectRatio)\n            }\n            aspectRatio?.let {\n                MeasuredAsyncImage(\n                    imageUrl = imageUrl,\n                    aspectRatio = it,\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(vertical = 5.dp, horizontal = 13.dp)\n                        .clip(MaterialTheme.shapes.medium)\n                )\n            }\n        }\n        Row(\n            horizontalArrangement = Arrangement.SpaceBetween,\n            modifier = Modifier.fillMaxWidth()\n                .padding(horizontal = 13.dp, vertical = 10.dp)\n        ) {\n            Row {\n                FilledTonalIconButton(\n                    onClick = { onMoreClick() },\n                    shape = MaterialTheme.shapes.medium,\n                    modifier = Modifier\n                        .padding(end = 3.dp)\n                        .width(33.dp)\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.ic_more_vert),\n                        contentDescription = stringResource(R.string.ic_more_vert_cdesc)\n                    )\n                }\n                FilledTonalIconButton(\n                    onClick = {\n                        postData.permalink?.let { url ->\n                            val sendIntent = Intent().apply {\n                                action = Intent.ACTION_SEND\n                                type = \"text/plain\"\n                                putExtra(Intent.EXTRA_TEXT, \"https://reddit.com$url\")\n                            }\n                            val shareIntent = Intent.createChooser(sendIntent, \"Share post\")\n                            context.startActivity(shareIntent)\n                        }\n                    },\n                    shape = MaterialTheme.shapes.medium\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.ic_share),\n                        contentDescription = stringResource(R.string.ic_share_cdesc)\n                    )\n                }\n                AnimatedTonalToggleIconButton(\n                    checked = bookmarkedState == true,\n                    onCheckedChange = { onSaveClick(it) { bookmarkedState = it } },\n                    checkedIcon = painterResource(R.drawable.ic_bookmark_filled),\n                    uncheckedIcon = painterResource(R.drawable.ic_bookmark),\n                    contentDescription = stringResource(R.string.ic_bookmark_cdesc)\n                )\n            }\n            Row(verticalAlignment = Alignment.CenterVertically) {\n                AnimatedTonalToggleIconButton(\n                    checked = downvoteState,\n                    onCheckedChange = {\n                        onDownvote(it) {\n                            downvoteState = it\n                            upvoteState = false\n                        }\n                    },\n                    checkedIcon = painterResource(R.drawable.ic_downvote),\n                    uncheckedIcon = painterResource(R.drawable.ic_downvote),\n                    contentDescription = stringResource(R.string.ic_downvote_cdesc),\n                    modifier = Modifier.width(33.dp),\n                    uncheckedRadius = 30.dp,\n                    checkedRadius = MediumCornerRadius\n                )\n                postData.ups?.toInt()?.prettyNumber()?.let {\n                    Text(\n                        text = it,\n                        style = MaterialTheme.typography.labelLarge,\n                        modifier = Modifier.padding(horizontal = 10.dp)\n                    )\n                }\n                AnimatedTonalToggleIconButton(\n                    checked = upvoteState,\n                    onCheckedChange = {\n                        onUpvote(it) {\n                            upvoteState = it\n                            downvoteState = false\n                        }\n                    },\n                    checkedIcon = painterResource(R.drawable.ic_upvote),\n                    uncheckedIcon = painterResource(R.drawable.ic_upvote),\n                    contentDescription = stringResource(R.string.ic_upvote_cdesc),\n                    modifier = Modifier.width(33.dp),\n                    uncheckedRadius = 30.dp,\n                    checkedRadius = MediumCornerRadius\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun CommentCard(\n    commentWithUser: CommentWithUser?,\n    modifier: Modifier = Modifier,\n    showingTrailingButtons: Boolean = true,\n    onMoreClick: () -> Unit = { },\n    onUpvote: (Boolean, () -> Unit) -> Unit = { _, _, -> },\n    onDownvote: (Boolean, () -> Unit) -> Unit = { _, _, -> },\n    containerColor: Color = MaterialTheme.colorScheme.surfaceContainer\n) {\n    val author = commentWithUser?.comment?.author\n    var downvoteState by rememberSaveable { mutableStateOf(commentWithUser?.comment?.likes == false) }\n    var upvoteState by rememberSaveable { mutableStateOf(commentWithUser?.comment?.likes == true) }\n\n    Column(modifier) {\n        Row {\n            commentWithUser?.user?.let {\n                AsyncImage(\n                    model = it.snoovatarUrl?.ifBlank { null }\n                        ?: it.iconUrl,\n                    contentDescription = null,\n                    placeholder = painterResource(R.drawable.generic_avatar),\n                    modifier = Modifier\n                        .clip(CircleShape)\n                        .size(17.dp)\n                )\n            }\n            Column {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.SpaceBetween,\n                    modifier = Modifier.fillMaxWidth()\n                ) {\n                    Row {\n                        Text(\n                            text = author?.let { \"u/$it\" } ?: \"u/[deleted]\",\n                            style = MaterialTheme.typography.labelSmall,\n                            modifier = Modifier\n                                .padding(start = 7.dp)\n                                .align(Alignment.CenterVertically)\n                        )\n                        commentWithUser?.comment?.createdUtc?.let { created ->\n                            val rel = try {\n                                created.convertUnixToRelativeTime()\n                            } catch (_: Exception) {\n                                \"\"\n                            }\n                            if (rel.isNotEmpty()) {\n                                Text(\n                                    text = rel,\n                                    style = MaterialTheme.typography.bodySmall,\n                                    color = MaterialTheme.colorScheme.outline,\n                                    modifier = Modifier.padding(\n                                        start = 7.dp\n                                    )\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        Row(modifier = Modifier.padding(top = 7.dp)) {\n            var cardHeightPx by remember { mutableIntStateOf(0) }\n            val density = androidx.compose.ui.platform.LocalDensity.current\n            val requiredPx = with(density) { (40.dp + 5.dp + 40.dp).toPx().toInt() }\n\n            Box(modifier = Modifier.weight(1f, false)) {\n                Column {\n                    Card(\n                        modifier = Modifier.clip(MaterialTheme.shapes.medium)\n                            .combinedClickable(\n                                enabled = true,\n                                onClick = { },\n                                onLongClick = onMoreClick\n                            ).onSizeChanged { cardHeightPx = it.height },\n                        shape = MaterialTheme.shapes.medium,\n                        colors = CardDefaults.cardColors(\n                            containerColor = containerColor\n                        )\n                    ) {\n                        Text(\n                            text = commentWithUser?.comment?.body.toString()\n                                .trimIndent().trimStart(),\n                            style = MaterialTheme.typography.bodyMedium,\n                            modifier = Modifier.padding(10.dp)\n                        )\n                    }\n                }\n            }\n\n            if (showingTrailingButtons) {\n                if (cardHeightPx >= requiredPx) {\n                    Column(\n                        modifier = Modifier.padding(start = 5.dp),\n                        verticalArrangement = Arrangement.spacedBy(5.dp)\n                    ) {\n                        AnimatedTonalToggleIconButton(\n                            checked = upvoteState,\n                            onCheckedChange = { checked ->\n                                onUpvote(checked) {\n                                    upvoteState = checked\n                                    if (checked) {\n                                        downvoteState = false\n                                    }\n                                }\n                            },\n                            checkedIcon = painterResource(R.drawable.ic_upvote),\n                            uncheckedIcon = painterResource(R.drawable.ic_upvote),\n                            contentDescription = stringResource(R.string.ic_upvote_cdesc),\n                            modifier = Modifier.size(30.dp, 40.dp),\n                            uncheckedRadius = MediumCornerRadius,\n                            checkedRadius = FullCornerRadius,\n                            colors = IconButtonDefaults.filledTonalIconToggleButtonColors(\n                                containerColor = MaterialTheme.colorScheme.surfaceContainer,\n                                contentColor = MaterialTheme.colorScheme.onSurfaceVariant,\n                                checkedContentColor = MaterialTheme.colorScheme.onPrimary\n                            )\n                        )\n                        AnimatedTonalToggleIconButton(\n                            checked = downvoteState,\n                            onCheckedChange = { checked ->\n                                onDownvote(checked) {\n                                    downvoteState = checked\n                                    if (checked) {\n                                        upvoteState = false\n                                    }\n                                }\n                            },\n                            checkedIcon = painterResource(R.drawable.ic_downvote),\n                            uncheckedIcon = painterResource(R.drawable.ic_downvote),\n                            contentDescription = stringResource(R.string.ic_downvote_cdesc),\n                            modifier = Modifier.size(30.dp, 40.dp),\n                            uncheckedRadius = MediumCornerRadius,\n                            checkedRadius = FullCornerRadius,\n                            colors = IconButtonDefaults.filledTonalIconToggleButtonColors(\n                                containerColor = MaterialTheme.colorScheme.surfaceContainer,\n                                contentColor = MaterialTheme.colorScheme.onSurfaceVariant,\n                                checkedContentColor = MaterialTheme.colorScheme.onPrimary\n                            )\n                        )\n                    }\n                } else {\n                    FilledTonalIconButton(\n                        onClick = { onMoreClick() },\n                        modifier = Modifier\n                            .padding(start = 5.dp)\n                            .size(30.dp, 40.dp),\n                        colors = IconButtonDefaults.filledTonalIconButtonColors(\n                            containerColor = MaterialTheme.colorScheme.surfaceContainer\n                        ),\n                        shape = MaterialTheme.shapes.medium\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.ic_more_vert),\n                            contentDescription = stringResource(R.string.ic_more_vert_cdesc)\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/components/ListComponents.kt",
    "content": "@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)\n\npackage com.pineapple.app.ui.components\n\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.ListItemDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.pineapple.app.ui.theme.ExtraLargeCornerRadius\nimport com.pineapple.app.ui.theme.ExtraSmallCornerRadius\n\n/**\n * A representation of a list item in the [TonalActionSectionList]\n */\ndata class TonalActionSectionItem(\n    val text: String,\n    val icon: Painter,\n    val contentDescription: String,\n    val onCLick: () -> Unit = { },\n    val iconSize: Dp = 24.dp,\n    val shouldTintIcon: Boolean = true\n)\n\n/**\n * List of clickable options styled as tonal cards, with rounding applied to first and last\n * items but not inner elements to replicate the Material 3 style used in the system settings app\n * @param items The list of [TonalActionSectionItem] to display\n * @param modifier The Modifier to be applied to this component\n * @param singleSelect Whether only a single item can be selected at a time\n * @param selectedIndexInitial The index of the initially selected item, if [singleSelect] is true\n * @param onSelectChange Callback invoked when the selected item changes, providing the new index\n * and corresponding [TonalActionSectionItem] (in that order)\n */\n@Composable\nfun TonalActionSectionList(\n    items: List<TonalActionSectionItem>,\n    modifier: Modifier = Modifier,\n    singleSelect: Boolean = false,\n    selectedIndexInitial: Int = 0,\n    onSelectChange: (Int, TonalActionSectionItem) -> Unit = { _, _ -> },\n    containerColor: Color = MaterialTheme.colorScheme.surfaceContainerLow,\n    listItemContainerColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh,\n    listItemContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant\n) {\n    var selectedIndex by rememberSaveable { mutableIntStateOf(selectedIndexInitial) }\n    Surface(\n        modifier = modifier,\n        shape = MaterialTheme.shapes.large,\n        color = containerColor\n    ) {\n        Column {\n            items.forEachIndexed { index, item ->\n                val isSelected = singleSelect && selectedIndex == index\n\n                val containerColor by animateColorAsState(\n                    targetValue = if (isSelected) {\n                        MaterialTheme.colorScheme.primary\n                    } else {\n                        listItemContainerColor\n                    },\n                    label = \"containerColor\",\n                    animationSpec = MaterialTheme.motionScheme.defaultEffectsSpec()\n                )\n                val contentColor by animateColorAsState(\n                    targetValue = if (isSelected) {\n                        MaterialTheme.colorScheme.onPrimary\n                    } else {\n                        listItemContentColor\n                    },\n                    label = \"contentColor\",\n                    animationSpec = MaterialTheme.motionScheme.defaultEffectsSpec()\n                )\n                val cornerRadius by animateDpAsState(\n                    targetValue = if (isSelected) ExtraLargeCornerRadius else ExtraSmallCornerRadius,\n                    label = \"cornerRadius\",\n                    animationSpec = MaterialTheme.motionScheme.defaultSpatialSpec()\n                )\n                val animatedShape = RoundedCornerShape(cornerRadius)\n\n                ListItem(\n                    headlineContent = {\n                        Text(item.text)\n                    },\n                    leadingContent = {\n                        if (item.shouldTintIcon) {\n                            Icon(\n                                painter = item.icon,\n                                contentDescription = item.contentDescription,\n                                tint = contentColor\n                            )\n                        } else {\n                            Image(\n                                painter = item.icon,\n                                contentDescription = item.contentDescription,\n                                modifier = Modifier.clip(CircleShape)\n                                    .size(item.iconSize)\n                            )\n                        }\n                    },\n                    modifier = Modifier\n                        .padding(bottom = if (index == items.lastIndex) 0.dp else 3.dp)\n                        .clip(animatedShape)\n                        .clickable {\n                            if (singleSelect) {\n                                if (selectedIndex != index) {\n                                    selectedIndex = index\n                                    onSelectChange(index, item)\n                                }\n                            }\n                            item.onCLick()\n                        },\n                    colors = ListItemDefaults.colors(\n                        containerColor = containerColor,\n                        contentColor = contentColor\n                    )\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/components/MediaComponents.kt",
    "content": "package com.pineapple.app.ui.components\n\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport coil3.compose.AsyncImage\nimport coil3.compose.AsyncImagePainter\nimport coil3.compose.rememberAsyncImagePainter\nimport coil3.request.CachePolicy\nimport coil3.request.ImageRequest\nimport coil3.request.crossfade\nimport com.pineapple.app.R\n\n/**\n * Wrapper to the AsyncImage composable intended for images that are used in scrolling\n * layouts, which keeps a fixed size even before the image is loaded to eliminate jank\n * caused by changing layout sizes.\n * @param imageUrl The URL of the image to load.\n * @param aspectRatio The aspect ratio (width / height) to use for the image.\n * @param modifier The modifier to be applied to the image.\n * @param contentDescription The content description for the image.\n */\n@Composable\nfun MeasuredAsyncImage(\n    imageUrl: String,\n    aspectRatio: Float?,\n    modifier: Modifier = Modifier,\n    contentDescription: String? = null\n) {\n    val context = LocalContext.current\n    val painter = rememberAsyncImagePainter(\n        model = ImageRequest.Builder(context)\n            .data(imageUrl)\n            .crossfade(true)\n            .memoryCachePolicy(CachePolicy.ENABLED)\n            .diskCachePolicy(CachePolicy.ENABLED)\n            .build()\n    )\n    val ratio = aspectRatio ?: (16f / 9f)\n\n    // Keep the AnimatedContent transition, but apply the external modifier to the inner Box\n    AnimatedContent(\n        targetState = painter.state,\n        transitionSpec = {\n            fadeIn(animationSpec = tween(250)) togetherWith fadeOut(animationSpec = tween(150))\n        },\n        contentAlignment = Alignment.TopCenter,\n        label = \"ImageLoad\"\n    ) { state ->\n        // Apply the caller-provided modifier to the measured container so size is stable\n        Box(\n            modifier = modifier\n                .aspectRatio(ratio),\n            contentAlignment = Alignment.TopCenter\n        ) {\n            when (state.collectAsState().value) {\n                is AsyncImagePainter.State.Loading -> {\n                    // Placeholder image that fills the container\n                    AsyncImage(\n                        model = R.drawable.async_image_placeholder,\n                        contentDescription = contentDescription,\n                        contentScale = ContentScale.Crop,\n                        modifier = Modifier.fillMaxSize()\n                    )\n                }\n                is AsyncImagePainter.State.Success -> {\n                    // Loaded image fills the container immediately\n                    Image(\n                        painter = painter,\n                        contentDescription = contentDescription,\n                        contentScale = ContentScale.Crop,\n                        modifier = Modifier.fillMaxSize()\n                    )\n                }\n                else -> {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxSize()\n                            .background(MaterialTheme.colorScheme.surfaceContainerLowest)\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/modal/CommentDetailSheet.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage com.pineapple.app.ui.modal\n\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.Context\nimport android.content.Intent\nimport android.widget.Toast\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilledTonalIconButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport com.pineapple.app.R\nimport com.pineapple.app.network.model.cache.CommentWithUser\nimport com.pineapple.app.ui.components.AnimatedTonalToggleIconButton\nimport com.pineapple.app.ui.components.CommentCard\nimport com.pineapple.app.ui.theme.MediumCornerRadius\nimport com.pineapple.app.utilities.prettyNumber\n\n@Composable\nfun CommentDetailSheet(\n    commentWithUser: CommentWithUser,\n    onDismissRequest: () -> Unit,\n    onDownvote: (Boolean, () -> Unit) -> Unit,\n    onUpvote: (Boolean, () -> Unit) -> Unit,\n    onSaveClick: (Boolean, () -> Unit) -> Unit,\n    onViewUserClick: () -> Unit\n) {\n    var upvoteState by remember { mutableStateOf(commentWithUser.comment.likes == true) }\n    var downvoteState by remember { mutableStateOf(commentWithUser.comment.likes == false) }\n    var bookmarkedState by remember { mutableStateOf(commentWithUser.comment.saved) }\n    val context = LocalContext.current\n\n    ModalBottomSheet(\n        onDismissRequest = onDismissRequest,\n        containerColor = MaterialTheme.colorScheme.surfaceContainerLow\n    ) {\n        Column {\n            CommentCard(\n                commentWithUser = commentWithUser,\n                showingTrailingButtons = false,\n                containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,\n                modifier = Modifier.padding(horizontal = 15.dp)\n            )\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.SpaceBetween,\n                modifier = Modifier.padding(start = 12.dp, end = 15.dp, top = 10.dp, bottom = 10.dp)\n                    .fillMaxWidth()\n            ) {\n                Row {\n                    FilledTonalIconButton(\n                        onClick = {\n                            commentWithUser.comment.permalink?.let { url ->\n                                val sendIntent = Intent().apply {\n                                    action = Intent.ACTION_SEND\n                                    type = \"text/plain\"\n                                    putExtra(Intent.EXTRA_TEXT, \"https://reddit.com$url\")\n                                }\n                                val shareIntent = Intent.createChooser(sendIntent, \"Share post\")\n                                context.startActivity(shareIntent)\n                            }\n                        },\n                        shape = MaterialTheme.shapes.medium\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.ic_share),\n                            contentDescription = stringResource(R.string.ic_share_cdesc)\n                        )\n                    }\n                    AnimatedTonalToggleIconButton(\n                        checked = bookmarkedState == true,\n                        onCheckedChange = { onSaveClick(it) { bookmarkedState = it } },\n                        checkedIcon = painterResource(R.drawable.ic_bookmark_filled),\n                        uncheckedIcon = painterResource(R.drawable.ic_bookmark),\n                        contentDescription = stringResource(R.string.ic_bookmark_cdesc)\n                    )\n                    FilledTonalIconButton(\n                        onClick = onViewUserClick,\n                        shape = MaterialTheme.shapes.medium\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.ic_person),\n                            contentDescription = stringResource(R.string.ic_person_cdesc)\n                        )\n                    }\n                    FilledTonalIconButton(\n                        onClick = {\n                            commentWithUser.comment.body?.let { bodyText ->\n                                val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n                                val clip = ClipData.newPlainText(\n                                    \"Comment Body\",\n                                    bodyText\n                                )\n                                clipboard.setPrimaryClip(clip)\n                            }\n                            // Maybe only this for devices that don't have the clipboard popup\n                            // Toast.makeText(context, R.string.post_copied_comment, Toast.LENGTH_SHORT).show()\n                            onDismissRequest()\n                        },\n                        shape = MaterialTheme.shapes.medium\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.ic_copy),\n                            contentDescription = stringResource(R.string.ic_copy_cdesc)\n                        )\n                    }\n                }\n                Row(verticalAlignment = Alignment.CenterVertically) {\n                    AnimatedTonalToggleIconButton(\n                        checked = downvoteState,\n                        onCheckedChange = {\n                            onDownvote(it) {\n                                downvoteState = it\n                                upvoteState = false\n                            }\n                        },\n                        checkedIcon = painterResource(R.drawable.ic_downvote),\n                        uncheckedIcon = painterResource(R.drawable.ic_downvote),\n                        contentDescription = stringResource(R.string.ic_downvote_cdesc),\n                        modifier = Modifier.width(33.dp),\n                        uncheckedRadius = 30.dp,\n                        checkedRadius = MediumCornerRadius\n                    )\n                    commentWithUser.comment.ups?.toInt()?.prettyNumber()?.let {\n                        Text(\n                            text = it,\n                            style = MaterialTheme.typography.labelLarge,\n                            modifier = Modifier.padding(horizontal = 10.dp)\n                        )\n                    }\n                    AnimatedTonalToggleIconButton(\n                        checked = upvoteState,\n                        onCheckedChange = {\n                            onUpvote(it) {\n                                upvoteState = it\n                                downvoteState = false\n                            }\n                        },\n                        checkedIcon = painterResource(R.drawable.ic_upvote),\n                        uncheckedIcon = painterResource(R.drawable.ic_upvote),\n                        contentDescription = stringResource(R.string.ic_upvote_cdesc),\n                        modifier = Modifier.width(33.dp),\n                        uncheckedRadius = 30.dp,\n                        checkedRadius = MediumCornerRadius\n                    )\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/modal/CommentRepliesSheet.kt",
    "content": "package com.pineapple.app.ui.modal\n\nimport androidx.compose.runtime.Composable\n\n@Composable\nfun CommentRepliesSheet() {\n\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/modal/PostOptionSheet.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class)\n\npackage com.pineapple.app.ui.modal\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport com.pineapple.app.R\nimport com.pineapple.app.network.model.reddit.PostData\nimport com.pineapple.app.ui.components.TonalActionSectionItem\nimport com.pineapple.app.ui.components.TonalActionSectionList\n\n/**\n * Modal bottom sheet displaying extended options, intended to be called from a post card\n * @param postData Data of the post for which options are being displayed\n * @param onDismissRequest Callback when the sheet is dismissed\n * @param onViewUser Callback to view the post author's profile\n * @param onViewCommunity Callback to view the post's community\n * @param onOpenExternal Callback to open the post in an external browser\n * @param onReport Callback to report the post\n */\n@Composable\nfun PostOptionSheet(\n    postData: PostData,\n    onDismissRequest: () -> Unit,\n    onViewUser: () -> Unit,\n    onViewCommunity: () -> Unit,\n    onOpenExternal: () -> Unit,\n    onReport: () -> Unit\n) {\n    ModalBottomSheet(\n        onDismissRequest = onDismissRequest,\n        containerColor = MaterialTheme.colorScheme.surfaceContainerLow\n    ) {\n        TonalActionSectionList(\n            items = listOf(\n                TonalActionSectionItem(\n                    text = \"Go to ${postData.subredditNamePrefixed}\",\n                    icon = painterResource(id = R.drawable.ic_community),\n                    contentDescription = stringResource(R.string.ic_community_cdesc),\n                    onCLick = onViewCommunity\n                ),\n                TonalActionSectionItem(\n                    text = \"View u/${postData.author}\",\n                    icon = painterResource(id = R.drawable.ic_person),\n                    contentDescription = stringResource(R.string.ic_person_cdesc),\n                    onCLick = onViewUser\n                ),\n                TonalActionSectionItem(\n                    text = \"Open in browser\",\n                    icon = painterResource(id = R.drawable.ic_open_external),\n                    contentDescription = stringResource(R.string.ic_open_external_cdesc),\n                    onCLick = onOpenExternal\n                ),\n                TonalActionSectionItem(\n                    text = \"Report post\",\n                    icon = painterResource(id = R.drawable.ic_flag),\n                    contentDescription = stringResource(R.string.ic_flag_cdesc),\n                    onCLick = onReport\n                )\n            ),\n            modifier = Modifier.padding(start = 20.dp, end = 20.dp, bottom = 15.dp)\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/modal/SortPostSheet.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n\npackage com.pineapple.app.ui.modal\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport com.pineapple.app.R\nimport com.pineapple.app.consts.PostFilterSort\nimport com.pineapple.app.consts.PostFilterTime\nimport com.pineapple.app.ui.components.TonalActionSectionItem\nimport com.pineapple.app.ui.components.TonalActionSectionList\n\n/**\n * Modal bottom sheet that allows users to filter and sort a list of posts in the same style\n * that is available in reddit (hot, new, top etc. and by day, week, month, etc.)\n * @param currentTimeSelection The currently selected time filter\n * @param currentSortSelection The currently selected sort filter\n * @param onDismissRequest Callback invoked when the sheet is dismissed, passing in the time and\n *                         sort selections (in that order)\n * @see [PostFilterSort] and [PostFilterTime]\n */\n@Composable\nfun SortPostSheet(\n    currentTimeSelection: String,\n    currentSortSelection: String,\n    onDismissRequest: (String, String) -> Unit\n) {\n    var selectedSortType by rememberSaveable { mutableStateOf(currentSortSelection) }\n    var selectedSortTime by rememberSaveable { mutableStateOf(currentTimeSelection) }\n    val showExtended = selectedSortType == PostFilterSort.SORT_TOP\n            || selectedSortType == PostFilterSort.SORT_CONTROVERSIAL\n    ModalBottomSheet(\n        onDismissRequest = {\n            onDismissRequest(selectedSortTime, selectedSortType)\n        },\n        containerColor = MaterialTheme.colorScheme.surfaceContainerLow\n    ) {\n        Column(modifier = Modifier.padding(horizontal = 20.dp)) {\n            Text(\n                text = stringResource(R.string.sort_sheet_sort_header),\n                style = MaterialTheme.typography.bodyMedium,\n            )\n            TonalActionSectionList(\n                items = listOf(\n                    TonalActionSectionItem(\n                        text = stringResource(R.string.sort_sheet_hot),\n                        icon = painterResource(R.drawable.ic_fire),\n                        contentDescription = stringResource(R.string.ic_fire_cdesc)\n                    ),\n                    TonalActionSectionItem(\n                        text = stringResource(R.string.sort_sheet_new),\n                        icon = painterResource(R.drawable.ic_shine),\n                        contentDescription = stringResource(R.string.ic_shine_cdesc)\n                    ),\n                    TonalActionSectionItem(\n                        text = stringResource(R.string.sort_sheet_rising),\n                        icon = painterResource(R.drawable.ic_trending),\n                        contentDescription = stringResource(R.string.ic_trending_cdesc)\n                    ),\n                    TonalActionSectionItem(\n                        text = stringResource(R.string.sort_sheet_controversial),\n                        icon = painterResource(R.drawable.ic_angry),\n                        contentDescription = stringResource(R.string.ic_angry_cdesc)\n                    ),\n                    TonalActionSectionItem(\n                        text = stringResource(R.string.sort_sheet_top),\n                        icon = painterResource(R.drawable.ic_arrow_up),\n                        contentDescription = stringResource(R.string.ic_arrow_up_cdesc)\n                    )\n                ),\n                singleSelect = true,\n                selectedIndexInitial = when (currentSortSelection) {\n                    PostFilterSort.SORT_HOT -> 0\n                    PostFilterSort.SORT_NEW -> 1\n                    PostFilterSort.SORT_RISING -> 2\n                    PostFilterSort.SORT_CONTROVERSIAL -> 3\n                    PostFilterSort.SORT_TOP -> 4\n                    else -> 0\n                },\n                onSelectChange = { index, item ->\n                    selectedSortType = when (index) {\n                        0 -> {\n                            selectedSortTime = PostFilterTime.TIME_DAY\n                            PostFilterSort.SORT_HOT\n                        }\n                        1 -> {\n                            selectedSortTime = PostFilterTime.TIME_DAY\n                            PostFilterSort.SORT_NEW\n                        }\n                        2 -> {\n                            selectedSortTime = PostFilterTime.TIME_DAY\n                            PostFilterSort.SORT_RISING\n                        }\n                        3 -> PostFilterSort.SORT_CONTROVERSIAL\n                        4 -> PostFilterSort.SORT_TOP\n                        else -> PostFilterSort.SORT_HOT\n                    }\n                },\n                modifier = Modifier.padding(\n                    top = 15.dp,\n                    bottom = animateDpAsState(targetValue = if (showExtended) 0.dp else 15.dp).value\n                )\n            )\n            AnimatedVisibility(\n                visible = showExtended,\n                modifier = Modifier.fillMaxWidth(),\n                enter = fadeIn(animationSpec = MaterialTheme.motionScheme.defaultEffectsSpec())\n                        + expandVertically(animationSpec = MaterialTheme.motionScheme.defaultSpatialSpec()),\n                exit = fadeOut(animationSpec = MaterialTheme.motionScheme.defaultEffectsSpec())\n                        + shrinkVertically(animationSpec = MaterialTheme.motionScheme.defaultSpatialSpec())\n            ) {\n                Column(modifier = Modifier.padding(bottom = 15.dp)) {\n                    Text(\n                        text = stringResource(R.string.sort_sheet_time_header),\n                        style = MaterialTheme.typography.bodyMedium,\n                        modifier = Modifier.padding(top = 20.dp)\n                    )\n                    TonalActionSectionList(\n                        items = listOf(\n                            TonalActionSectionItem(\n                                text = stringResource(R.string.sort_sheet_day),\n                                icon = painterResource(R.drawable.ic_calendar_day),\n                                contentDescription = stringResource(R.string.ic_calendar_day_cdesc)\n                            ),\n                            TonalActionSectionItem(\n                                text = stringResource(R.string.sort_sheet_week),\n                                icon = painterResource(R.drawable.ic_week),\n                                contentDescription = stringResource(R.string.ic_week_cdesc)\n                            ),\n                            TonalActionSectionItem(\n                                text = stringResource(R.string.sort_sheet_month),\n                                icon = painterResource(R.drawable.ic_calendar_month),\n                                contentDescription = stringResource(R.string.ic_calendar_month_cdesc)\n                            ),\n                            TonalActionSectionItem(\n                                text = stringResource(R.string.sort_sheet_year),\n                                icon = painterResource(R.drawable.ic_hourglass),\n                                contentDescription = stringResource(R.string.ic_hourglass_cdesc)\n                            ),\n                            TonalActionSectionItem(\n                                text = stringResource(R.string.sort_sheet_all),\n                                icon = painterResource(R.drawable.ic_history),\n                                contentDescription = stringResource(R.string.ic_history_cdesc)\n                            )\n                        ),\n                        singleSelect = true,\n                        selectedIndexInitial = when (currentTimeSelection) {\n                            PostFilterTime.TIME_DAY -> 0\n                            PostFilterTime.TIME_WEEK -> 1\n                            PostFilterTime.TIME_MONTH -> 2\n                            PostFilterTime.TIME_YEAR -> 3\n                            PostFilterTime.TIME_ALL -> 4\n                            else -> 0\n                        },\n                        onSelectChange = { index, item ->\n                            selectedSortTime = when (index) {\n                                0 -> PostFilterTime.TIME_DAY\n                                1 -> PostFilterTime.TIME_WEEK\n                                2 -> PostFilterTime.TIME_MONTH\n                                3 -> PostFilterTime.TIME_YEAR\n                                4 -> PostFilterTime.TIME_ALL\n                                else -> PostFilterTime.TIME_DAY\n                            }\n                        },\n                        modifier = Modifier.padding(top = 15.dp)\n                    )\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/state/AuthViewState.kt",
    "content": "package com.pineapple.app.ui.state\n\n/**\n * Object to reflect a state of the reddit authentication process\n */\nsealed class AuthViewState {\n    object Idle : AuthViewState()\n    object Loading : AuthViewState()\n    object Success : AuthViewState()\n    data class Error(val message: String) : AuthViewState()\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/theme/Shape.kt",
    "content": "package com.pineapple.app.ui.theme\n\nimport androidx.compose.ui.unit.dp\n\nval ExtraSmallCornerRadius = 4.dp\nval SmallCornerRadius = 8.dp\nval MediumCornerRadius = 12.dp\nval LargeCornerRadius = 16.dp\nval ExtraLargeCornerRadius = 28.dp\nval FullCornerRadius = 100.dp"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/theme/Theme.kt",
    "content": "package com.pineapple.app.ui.theme\n\nimport android.os.Build\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.MotionScheme\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.material3.dynamicLightColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.platform.LocalContext\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun PineappleTheme(\n    darkTheme: Boolean = isSystemInDarkTheme(),\n    dynamicColor: Boolean = true,\n    content: @Composable () -> Unit\n) {\n    val colorScheme = when {\n        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {\n            val context = LocalContext.current\n            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)\n        }\n\n        darkTheme -> darkColorScheme()\n        else -> lightColorScheme()\n    }\n\n    MaterialTheme(\n        colorScheme = colorScheme,\n       // typography = PineappleTypography,\n        motionScheme = MotionScheme.expressive(),\n        content = content\n    )\n\n}\n\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/theme/Type.kt",
    "content": "package com.pineapple.app.ui.theme\n\nimport androidx.compose.material3.Typography\nimport androidx.compose.ui.text.font.Font\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport com.pineapple.app.R\n\nval GoogleSans = FontFamily(\n    Font(R.font.google_sans_regular, weight = FontWeight.Normal),\n    Font(R.font.google_sans_medium, weight = FontWeight.Medium),\n    Font(R.font.google_sans_semibold, weight = FontWeight.SemiBold),\n    Font(R.font.google_sans_bold, weight = FontWeight.Bold)\n)\n\nval PineappleTypography = Typography(\n    displayLarge = Typography().displayLarge.copy(fontFamily = GoogleSans),\n    displayMedium = Typography().displayMedium.copy(fontFamily = GoogleSans),\n    displaySmall = Typography().displaySmall.copy(fontFamily = GoogleSans),\n    headlineLarge = Typography().headlineLarge.copy(fontFamily = GoogleSans),\n    headlineMedium = Typography().headlineMedium.copy(fontFamily = GoogleSans),\n    headlineSmall = Typography().headlineSmall.copy(fontFamily = GoogleSans),\n    titleLarge = Typography().titleLarge.copy(fontFamily = GoogleSans),\n    titleMedium = Typography().titleMedium.copy(fontFamily = GoogleSans),\n    titleSmall = Typography().titleSmall.copy(fontFamily = GoogleSans),\n    bodyLarge = Typography().bodyLarge.copy(fontFamily = GoogleSans),\n    bodyMedium = Typography().bodyMedium.copy(fontFamily = GoogleSans),\n    bodySmall = Typography().bodySmall.copy(fontFamily = GoogleSans),\n    labelLarge = Typography().labelLarge.copy(fontFamily = GoogleSans),\n    labelMedium = Typography().labelMedium.copy(fontFamily = GoogleSans),\n    labelSmall = Typography().labelSmall.copy(fontFamily = GoogleSans)\n)"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/view/AccountPage.kt",
    "content": "package com.pineapple.app.ui.view\n\nimport androidx.compose.runtime.Composable\n\n@Composable\nfun AccountPage() {\n\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/view/BrowsePage.kt",
    "content": "package com.pineapple.app.ui.view\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material3.pulltorefresh.PullToRefreshBox\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport androidx.paging.LoadState\nimport androidx.paging.compose.collectAsLazyPagingItems\nimport com.pineapple.app.consts.NavDestinationKey\nimport com.pineapple.app.network.model.reddit.PostData\nimport com.pineapple.app.ui.components.PostCard\nimport com.pineapple.app.ui.viewmodel.BrowseViewModel\nimport com.pineapple.app.utilities.toPostData\nimport com.pineapple.app.utilities.toUserAboutListing\n\n@Composable\nfun BrowsePage(\n    onRequestUserAuth: () -> Unit,\n    onRequestPostDetailSheet: (PostData) -> Unit,\n    navController: NavController\n) {\n    val viewModel: BrowseViewModel = hiltViewModel()\n    val pagingItems = viewModel.pagedPosts.collectAsLazyPagingItems()\n    val pullRefreshState = rememberPullToRefreshState()\n\n    LaunchedEffect(pagingItems.loadState.refresh) {\n        val refresh = pagingItems.loadState.refresh\n        if (refresh !is LoadState.Loading && viewModel.shouldScrollToTopAfterRefresh) {\n            viewModel.postListState.animateScrollToItem(0)\n        }\n    }\n\n    PullToRefreshBox(\n        onRefresh = {\n            pagingItems.refresh()\n        },\n        isRefreshing = false,\n        state = pullRefreshState,\n        modifier = Modifier.fillMaxSize()\n    ) {\n        Column(modifier = Modifier.fillMaxSize()) {\n            LazyColumn(\n                modifier = Modifier.fillMaxSize(),\n                state = viewModel.postListState\n            ) {\n                items(\n                    count = pagingItems.itemCount,\n                    key = { index ->\n                        pagingItems[index]?.post?.id ?: index\n                    }\n                ) { index ->\n                    val item = pagingItems[index] ?: return@items\n                    val postData = item.post.toPostData()\n                    val userInfo = item.user?.toUserAboutListing()\n\n                    PostCard(\n                        postData = postData,\n                        modifier = Modifier.padding(\n                            vertical = 5.dp,\n                            horizontal = 10.dp\n                        ),\n                        userInfo = userInfo,\n                        onClick = {\n                            viewModel.shouldScrollToTopAfterRefresh = false\n                            android.util.Log.e(\"BrowsePage\", \"navigating to post detail for id=${postData.id}\")\n                            navController.navigate(\"${NavDestinationKey.PostView}/${postData.id}\")\n                        },\n                        onMoreClick = {\n                            onRequestPostDetailSheet(postData)\n                        },\n                        onSaveClick = { newState, onSuccess ->\n                            if (!viewModel.isUserless) {\n                                postData.id?.let {\n                                    onSuccess()\n                                    viewModel.updatePostFavorite(newState, it)\n                                }\n                            } else {\n                                onRequestUserAuth()\n                            }\n                        },\n                        onUpvote = { intention, onSuccess ->\n                            if (!viewModel.isUserless) {\n                                postData.id?.let { postID ->\n                                    onSuccess()\n                                    viewModel.updatePostVote(\n                                        postId = postID,\n                                        direction = if (intention) 1 else 0\n                                    )\n                                }\n                            } else {\n                                onRequestUserAuth()\n                            }\n                        },\n                        onDownvote = { intention, onSuccess ->\n                            if (!viewModel.isUserless) {\n                                postData.id?.let { postID ->\n                                    onSuccess()\n                                    viewModel.updatePostVote(\n                                        postId = postID,\n                                        direction = if (intention) -1 else 0\n                                    )\n                                }\n                            } else {\n                                onRequestUserAuth()\n                            }\n                        }\n                    )\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/view/ChatPage.kt",
    "content": "package com.pineapple.app.ui.view\n\nimport androidx.compose.runtime.Composable\n\n@Composable\nfun ChatPage() {\n\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/view/CommunityView.kt",
    "content": "package com.pineapple.app.ui.view\n\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.navigation.NavController\nimport com.pineapple.app.ui.theme.PineappleTheme\n\n@Composable\nfun CommunityView(navController: NavController, community: String) {\n    PineappleTheme {\n        Text(community)\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/view/HomeView.kt",
    "content": "@file:OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)\n\npackage com.pineapple.app.ui.view\n\nimport android.content.Intent\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandIn\nimport androidx.compose.animation.shrinkOut\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.CenterAlignedTopAppBar\nimport androidx.compose.material3.DrawerValue\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalDrawerSheet\nimport androidx.compose.material3.ModalNavigationDrawer\nimport androidx.compose.material3.NavigationBar\nimport androidx.compose.material3.NavigationBarItem\nimport androidx.compose.material3.NavigationDrawerItem\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberDrawerState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.core.net.toUri\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport androidx.paging.LoadState\nimport androidx.paging.compose.collectAsLazyPagingItems\nimport coil3.compose.AsyncImage\nimport com.pineapple.app.R\nimport com.pineapple.app.consts.NavDestinationKey\nimport com.pineapple.app.consts.PageDestinationKey\nimport com.pineapple.app.ui.modal.PostOptionSheet\nimport com.pineapple.app.ui.modal.SortPostSheet\nimport com.pineapple.app.ui.theme.PineappleTheme\nimport com.pineapple.app.ui.viewmodel.BrowseViewModel\nimport com.pineapple.app.ui.viewmodel.HomeViewModel\nimport kotlinx.coroutines.launch\n\n@Composable\nfun HomeView(navController: NavController) {\n    val viewModel: HomeViewModel = hiltViewModel()\n    val browseViewModel: BrowseViewModel = hiltViewModel()\n    val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()\n    val drawerState = rememberDrawerState(DrawerValue.Closed)\n    val scope = rememberCoroutineScope()\n\n    val pagingItems = browseViewModel.pagedPosts.collectAsLazyPagingItems()\n    val topSubreddits = viewModel.topSubreddits.collectAsState(initial = emptyList())\n    val subscribedSubreddits = viewModel.subscribedSubreddits.collectAsState(initial = emptyList())\n\n    PineappleTheme {\n        ModalNavigationDrawer(\n            drawerState = drawerState,\n            drawerContent = {\n                ModalDrawerSheet(modifier = Modifier.fillMaxWidth(0.7F)) {\n                   Column(modifier = Modifier.verticalScroll(rememberScrollState())) {\n                       Icon(\n                           painter = painterResource(R.drawable.ic_pineapple_logo),\n                           contentDescription = null,\n                           tint = MaterialTheme.colorScheme.primary,\n                           modifier = Modifier.padding(top = 20.dp, start = 20.dp)\n                               .height(100.dp)\n                       )\n                       Column(\n                           modifier = Modifier.padding(start = 15.dp, end = 15.dp, top = 30.dp)\n                       ) {\n                           NavigationDrawerItem(\n                               label = {\n                                   Text(stringResource(R.string.home_nav_home))\n                               },\n                               icon = {\n                                   Icon(\n                                       painter = painterResource(R.drawable.ic_browse),\n                                       contentDescription = stringResource(R.string.ic_browse_cdesc)\n                                   )\n                               },\n                               selected = viewModel.currentNavPage == PageDestinationKey.BROWSE,\n                               onClick = {\n                                   viewModel.currentNavPage = PageDestinationKey.BROWSE\n                                   scope.launch {\n                                       drawerState.close()\n                                   }\n                               }\n                           )\n                           NavigationDrawerItem(\n                               label = {\n                                   Text(stringResource(R.string.home_nav_account))\n                               },\n                               icon = {\n                                   Icon(\n                                       painter = painterResource(R.drawable.ic_person),\n                                       contentDescription = stringResource(R.string.ic_person_cdesc)\n                                   )\n                               },\n                               selected = viewModel.currentNavPage == PageDestinationKey.ACCOUNT,\n                               onClick = {\n                                   viewModel.currentNavPage = PageDestinationKey.ACCOUNT\n                                   scope.launch {\n                                       drawerState.close()\n                                   }\n                               },\n                               modifier = Modifier.padding(top = 2.dp)\n                           )\n                           NavigationDrawerItem(\n                               label = {\n                                   Text(stringResource(R.string.home_drawer_settings))\n                               },\n                               icon = {\n                                   Icon(\n                                       painter = painterResource(R.drawable.ic_settings),\n                                       contentDescription = stringResource(R.string.ic_settings_cdesc)\n                                   )\n                               },\n                               selected = false,\n                               onClick = {\n                                   scope.launch {\n                                       drawerState.close()\n                                   }\n                               },\n                               modifier = Modifier.padding(top = 2.dp)\n                           )\n                       }\n                       val isShowingPopular = viewModel.isUserless || subscribedSubreddits.value.isEmpty()\n                       val subreddits = if (isShowingPopular) topSubreddits.value else subscribedSubreddits.value\n                       Text(\n                           text = if (isShowingPopular) {\n                               stringResource(R.string.home_drawer_communities_uless)\n                           } else {\n                               stringResource(R.string.home_drawer_communities)\n                           },\n                           style = MaterialTheme.typography.titleSmall,\n                           color = MaterialTheme.colorScheme.onSurfaceVariant,\n                           modifier = Modifier.padding(top = 20.dp, start = 20.dp)\n                       )\n                       Column(modifier = Modifier.padding(horizontal = 15.dp, vertical = 15.dp)) {\n                           subreddits.forEach { subreddit ->\n                               NavigationDrawerItem(\n                                   label = {\n                                       Text(\"r/${subreddit.name}\")\n                                   },\n                                   icon = {\n                                       if (subreddit.iconUrl.isNotEmpty()) {\n                                           AsyncImage(\n                                               model = subreddit.iconUrl,\n                                               contentDescription = null,\n                                               modifier = Modifier.clip(CircleShape).size(25.dp)\n                                           )\n                                       } else {\n                                           Box(\n                                               modifier = Modifier.clip(CircleShape)\n                                                   .size(25.dp)\n                                                   .background(MaterialTheme.colorScheme.primaryContainer)\n                                           ) {\n                                               Icon(\n                                                   painter = painterResource(R.drawable.ic_community),\n                                                   contentDescription = stringResource(R.string.ic_community_cdesc),\n                                                   modifier = Modifier.align(Alignment.Center)\n                                                       .size(18.dp)\n                                               )\n                                           }\n                                       }\n                                   },\n                                   selected = false,\n                                   onClick = {\n                                       scope.launch {\n                                           drawerState.close()\n                                       }\n                                   },\n                                   modifier = Modifier.padding(top = 2.dp)\n                               )\n                           }\n                       }\n                   }\n                }\n            }\n        ) {\n            Scaffold(\n                bottomBar = {\n                    NavigationBar {\n                        NavigationBarItem(\n                            selected = viewModel.currentNavPage == PageDestinationKey.BROWSE,\n                            onClick = {\n                                viewModel.currentNavPage = PageDestinationKey.BROWSE\n                            },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.ic_browse),\n                                    contentDescription = stringResource(R.string.ic_browse_cdesc)\n                                )\n                            },\n                            label = {\n                                Text(stringResource(R.string.home_nav_home))\n                            }\n                        )\n                        NavigationBarItem(\n                            selected = viewModel.currentNavPage == PageDestinationKey.SEARCH,\n                            onClick = {\n                                viewModel.currentNavPage = PageDestinationKey.SEARCH\n                            },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.ic_search),\n                                    contentDescription = stringResource(R.string.ic_search_cdesc)\n                                )\n                            },\n                            label = {\n                                Text(stringResource(R.string.home_nav_search))\n                            }\n                        )\n                        NavigationBarItem(\n                            selected = viewModel.currentNavPage == PageDestinationKey.CHATS,\n                            onClick = {\n                                viewModel.currentNavPage = PageDestinationKey.CHATS\n                            },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.ic_forum),\n                                    contentDescription = stringResource(R.string.ic_chats_cdesc)\n                                )\n                            },\n                            label = {\n                                Text(stringResource(R.string.home_nav_chats))\n                            }\n                        )\n                        NavigationBarItem(\n                            selected = viewModel.currentNavPage == PageDestinationKey.ACCOUNT,\n                            onClick = {\n                                viewModel.currentNavPage = PageDestinationKey.ACCOUNT\n                            },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.ic_person),\n                                    contentDescription = stringResource(R.string.ic_person_cdesc)\n                                )\n                            },\n                            label = {\n                                Text(stringResource(R.string.home_nav_account))\n                            }\n                        )\n                    }\n                },\n                topBar = {\n                    AnimatedContent(\n                        targetState = viewModel.currentNavPage != PageDestinationKey.SEARCH,\n                        modifier = Modifier.fillMaxWidth()\n                    ) { showAppBar ->\n                        if (showAppBar) {\n                            Column {\n                                CenterAlignedTopAppBar(\n                                    title = {\n                                        AnimatedContent(targetState = viewModel.currentNavPage) { page ->\n                                            when (page) {\n                                                PageDestinationKey.BROWSE -> {\n                                                    Text(stringResource(R.string.home_title))\n                                                }\n                                                PageDestinationKey.CHATS -> {\n                                                    Text(stringResource(R.string.home_nav_chats))\n                                                }\n                                                PageDestinationKey.ACCOUNT -> {\n                                                    Text(stringResource(R.string.home_nav_account))\n                                                }\n                                            }\n                                        }\n                                    },\n                                    navigationIcon = {\n                                        IconButton(\n                                            onClick = {\n                                                scope.launch {\n                                                    drawerState.open()\n                                                }\n                                            }\n                                        ) {\n                                            Icon(\n                                                painter = painterResource(R.drawable.ic_menu),\n                                                contentDescription = stringResource(R.string.ic_menu_cdesc)\n                                            )\n                                        }\n                                    },\n                                    actions = {\n                                        AnimatedContent(\n                                            targetState = viewModel.currentNavPage == PageDestinationKey.BROWSE\n                                        ) { show ->\n                                            if (show) {\n                                                IconButton(\n                                                    onClick = {\n                                                        viewModel.showPostFilterSheet = true\n                                                    }\n                                                ) {\n                                                    Icon(\n                                                        painter = painterResource(R.drawable.ic_filter),\n                                                        contentDescription = stringResource(R.string.ic_filter_cdesc)\n                                                    )\n                                                }\n                                            }\n                                        }\n                                    },\n                                    scrollBehavior = scrollBehavior\n                                )\n                                AnimatedVisibility(\n                                    visible = pagingItems.loadState.refresh is LoadState.Loading,\n                                    modifier = Modifier.fillMaxWidth()\n                                        .padding(horizontal = 5.dp)\n                                ) {\n                                    LinearProgressIndicator()\n                                }\n                            }\n                        }\n                    }\n                },\n                snackbarHost = {\n                    SnackbarHost(viewModel.snackbarState)\n                },\n                floatingActionButton = {\n                    if (viewModel.currentNavPage == PageDestinationKey.BROWSE){\n                        FloatingActionButton(\n                            onClick = { },\n                            shape = MaterialTheme.shapes.large,\n                            modifier = Modifier.size(65.dp)\n                        ) {\n                            Icon(\n                                painter = painterResource(R.drawable.ic_plus),\n                                contentDescription = stringResource(R.string.ic_plus_cdesc)\n                            )\n                        }\n                    }\n                },\n                modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)\n            ) { paddingValues ->\n                AnimatedContent(\n                    modifier = Modifier.padding(paddingValues).fillMaxSize(),\n                    targetState = viewModel.currentNavPage\n                ) { page ->\n                    when (page) {\n                        PageDestinationKey.BROWSE -> {\n                            BrowsePage(\n                                onRequestUserAuth = {\n                                    viewModel.encourageUserAuthSnackbar()\n                                },\n                                onRequestPostDetailSheet = { postData ->\n                                    viewModel.openPostOptionSheet(postData)\n                                },\n                                navController = navController\n                            )\n                        }\n                        PageDestinationKey.SEARCH -> SearchPage(navController)\n                        PageDestinationKey.CHATS -> ChatPage()\n                        PageDestinationKey.ACCOUNT -> AccountPage()\n                    }\n                }\n            }\n        }\n\n        if (viewModel.showPostFilterSheet) {\n            SortPostSheet(\n                onDismissRequest = { time, sort ->\n                    viewModel.apply {\n                        showPostFilterSheet = false\n                        browseViewModel.updateFilters(sort, time)\n                    }\n                },\n                currentSortSelection = browseViewModel.currentFilterSort,\n                currentTimeSelection = browseViewModel.currentFilterTime\n            )\n        }\n\n        if (viewModel.showPostOptionSheet) {\n            viewModel.currentPostOptionData?.let { postData ->\n                PostOptionSheet(\n                    postData = postData,\n                    onDismissRequest = {\n                        viewModel.showPostOptionSheet = false\n                    },\n                    onViewUser = {\n                        viewModel.showPostOptionSheet = false\n                        navController.navigate(\"${NavDestinationKey.UserView}/${postData.author}\")\n                    },\n                    onViewCommunity = {\n                        viewModel.showPostOptionSheet = false\n                        navController.navigate(\"${NavDestinationKey.CommunityView}/${postData.subreddit}\")\n                    },\n                    onReport = {\n                        Intent(Intent.ACTION_VIEW).apply {\n                            this.data = (\"https://www.reddit.com/report\"\n                                    + \"?url=https://www.reddit.com${postData.permalink}\").toUri()\n                            navController.context.startActivity(this)\n                        }\n                    },\n                    onOpenExternal = {\n                        Intent(Intent.ACTION_VIEW).apply {\n                            this.data = \"https://www.reddit.com${postData.permalink}\".toUri()\n                            navController.context.startActivity(this)\n                        }\n                    }\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/view/KeyProviderView.kt",
    "content": "package com.pineapple.app.ui.view\n\nimport android.content.Intent\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.FloatingActionButtonDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeTopAppBar\nimport androidx.compose.material3.LoadingIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TextField\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.core.net.toUri\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport com.pineapple.app.ui.state.AuthViewState\nimport com.pineapple.app.R\nimport com.pineapple.app.consts.MMKVKey\nimport com.pineapple.app.consts.NavDestinationKey\nimport com.pineapple.app.consts.OnboardingLoginType\nimport com.pineapple.app.ui.theme.PineappleTheme\nimport com.pineapple.app.ui.viewmodel.KeyProviderViewModel\nimport java.util.UUID\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun KeyProviderView(navController: NavController, loginType: String) {\n    val viewModel: KeyProviderViewModel = hiltViewModel()\n    val viewState = viewModel.viewState.collectAsState()\n\n    LaunchedEffect(viewState.value) {\n        if (viewState.value is AuthViewState.Success) {\n            when (loginType) {\n                OnboardingLoginType.Guest -> {\n                    viewModel.mmkv.encode(MMKVKey.ONBOARDING_COMPLETE, true)\n                    navController.navigate(NavDestinationKey.HomeView)\n                }\n                OnboardingLoginType.RedditAuth -> {\n                    viewModel.launchRedditAuthFlow(navController.context)\n                }\n            }\n        }\n    }\n\n    PineappleTheme {\n        AnimatedContent(viewState.value is AuthViewState.Loading) { loading ->\n            if (!loading) {\n                Scaffold(\n                    topBar = {\n                        LargeTopAppBar(\n                            title = {\n                                Text(\n                                    text = stringResource(R.string.provide_key_title_text),\n                                    style = MaterialTheme.typography.displaySmall\n                                )\n                            },\n                            navigationIcon = {\n                                IconButton(onClick = { navController.popBackStack() }) {\n                                    Icon(\n                                        painter = painterResource(R.drawable.ic_back),\n                                        contentDescription = stringResource(R.string.ic_back_cdesc)\n                                    )\n                                }\n                            }\n                        )\n                    },\n                    floatingActionButton = {\n                        FloatingActionButton(\n                            onClick = {\n                                viewModel.submitClientSecret()\n                            },\n                            containerColor = MaterialTheme.colorScheme.secondaryContainer,\n                            contentColor = MaterialTheme.colorScheme.onSecondaryContainer,\n                            elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),\n                            shape = MaterialTheme.shapes.extraLarge\n                        ) {\n                            Icon(\n                                painter = painterResource(R.drawable.ic_forward),\n                                contentDescription = stringResource(R.string.ic_forward_cdesc),\n                                modifier = Modifier\n                                    .padding(30.dp)\n                                    .size(26.dp)\n                            )\n                        }\n                    }\n                ) { paddingValues ->\n                    Column(\n                        modifier = Modifier.padding(paddingValues)\n                    ) {\n                        Text(\n                            text = stringResource(R.string.provide_key_subtitle_text),\n                            style = MaterialTheme.typography.bodyLarge,\n                            modifier = Modifier.padding(vertical = 10.dp, horizontal = 20.dp)\n                        )\n                        TextField(\n                            value = viewModel.clientSecretTextFieldValue,\n                            onValueChange = {\n                                viewModel.clientSecretTextFieldValue = it\n                            },\n                            label = {\n                                Text(stringResource(R.string.provide_key_entry_hint))\n                            },\n                            modifier = Modifier\n                                .padding(top = 30.dp, start = 20.dp, end = 20.dp)\n                                .fillMaxWidth(),\n                            singleLine = true,\n                            supportingText = {\n                                if (viewState.value is AuthViewState.Error) {\n                                    Text(\n                                        text = (viewState.value as AuthViewState.Error).message\n                                    )\n                                }\n                            },\n                            isError = viewState.value is AuthViewState.Error\n                        )\n                        TextButton(\n                            onClick = {\n                                navController.context.startActivity(\n                                    Intent(Intent.ACTION_VIEW, \"https://reddit.com/prefs/apps\".toUri())\n                                )\n                            },\n                            modifier = Modifier.padding(top = 25.dp, start = 10.dp)\n                        ) {\n                            Icon(\n                                painter = painterResource(R.drawable.ic_reddit),\n                                contentDescription = stringResource(R.string.ic_reddit_cdesc)\n                            )\n                            Text(\n                                text = stringResource(R.string.provide_key_dev_button),\n                                modifier = Modifier.padding(start = 15.dp)\n                            )\n                        }\n                        TextButton(\n                            onClick = {\n                                // Link to some markdown file in the github\n                            },\n                            modifier = Modifier.padding(start = 10.dp)\n                        ) {\n                            Icon(\n                                painter = painterResource(R.drawable.ic_help),\n                                contentDescription = stringResource(R.string.ic_help_cdesc)\n                            )\n                            Text(\n                                text = stringResource(R.string.provide_key_what_button),\n                                modifier = Modifier.padding(start = 15.dp)\n                            )\n                        }\n                    }\n                }\n            } else {\n                Box(modifier = Modifier.fillMaxSize()) {\n                    LoadingIndicator(\n                        modifier = Modifier\n                            .align(Alignment.Center)\n                            .size(100.dp)\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/view/PostView.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n\npackage com.pineapple.app.ui.view\n\nimport android.content.Intent\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FilledTonalIconButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LoadingIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.core.net.toUri\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport androidx.paging.compose.collectAsLazyPagingItems\nimport coil3.compose.AsyncImage\nimport com.pineapple.app.R\nimport com.pineapple.app.consts.NavDestinationKey\nimport com.pineapple.app.ui.components.AnimatedTonalToggleIconButton\nimport com.pineapple.app.ui.components.CommentCard\nimport com.pineapple.app.ui.components.MeasuredAsyncImage\nimport com.pineapple.app.ui.modal.CommentDetailSheet\nimport com.pineapple.app.ui.modal.PostOptionSheet\nimport com.pineapple.app.ui.theme.MediumCornerRadius\nimport com.pineapple.app.ui.theme.PineappleTheme\nimport com.pineapple.app.ui.viewmodel.BrowseViewModel\nimport com.pineapple.app.ui.viewmodel.PostViewModel\nimport com.pineapple.app.utilities.convertUnixToRelativeTime\nimport com.pineapple.app.utilities.prettyNumber\nimport com.pineapple.app.utilities.toPostData\n\n@Composable\nfun PostView(navController: NavController, postID: String) {\n    val viewModel: PostViewModel = hiltViewModel()\n    val browseViewModel: BrowseViewModel = hiltViewModel()\n    val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()\n    val postWithUser = viewModel.postState.collectAsState()\n    val comments = viewModel.comments.collectAsLazyPagingItems()\n    val isLoading = viewModel.isLoading.collectAsState()\n    val context = LocalContext.current\n\n    LaunchedEffect(postID) {\n        viewModel.loadPost(postID)\n    }\n\n    PineappleTheme {\n        Scaffold(\n            topBar = {\n                TopAppBar(\n                    title = { },\n                    navigationIcon = {\n                        IconButton(\n                            onClick = { navController.popBackStack() }\n                        ) {\n                            Icon(\n                                painter = painterResource(R.drawable.ic_back),\n                                contentDescription = stringResource(R.string.ic_back_cdesc)\n                            )\n                        }\n                    },\n                    actions = {\n                        IconButton(\n                            onClick = {\n                                viewModel.showingMoreSheet = true\n                            }\n                        ) {\n                            Icon(\n                                painter = painterResource(R.drawable.ic_more_vert),\n                                contentDescription = stringResource(R.string.ic_more_vert_cdesc)\n                            )\n                        }\n                    },\n                    scrollBehavior = scrollBehavior\n                )\n            },\n            snackbarHost = {\n                SnackbarHost(viewModel.snackbarState)\n            },\n            modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)\n        ) { paddingValues ->\n            Column(modifier = Modifier.padding(paddingValues)) {\n                AnimatedContent(isLoading.value, label = \"Post Loading Animation\") {\n                    if (it) {\n                        Box(Modifier.fillMaxSize()) {\n                            LoadingIndicator(\n                                Modifier\n                                    .size(75.dp)\n                                    .align(Alignment.Center)\n                            )\n                        }\n                    } else {\n                        postWithUser.value?.let { post ->\n                            LazyColumn(\n                                modifier = Modifier\n                                    .fillMaxWidth()\n                            ) {\n                                item {\n                                    Column {\n                                        Row(\n                                            modifier = Modifier\n                                                .fillMaxWidth()\n                                                .padding(horizontal = 15.dp)\n                                                .clip(MaterialTheme.shapes.medium)\n                                                .background(MaterialTheme.colorScheme.surfaceContainerLow),\n                                            verticalAlignment = Alignment.CenterVertically,\n                                            horizontalArrangement = Arrangement.SpaceBetween\n                                        ) {\n                                            Row(verticalAlignment = Alignment.CenterVertically) {\n                                                post.user?.let {\n                                                    AsyncImage(\n                                                        model = it.snoovatarUrl?.ifBlank { null }\n                                                            ?: it.iconUrl,\n                                                        contentDescription = null,\n                                                        placeholder = painterResource(R.drawable.generic_avatar),\n                                                        modifier = Modifier\n                                                            .padding(15.dp)\n                                                            .clip(CircleShape)\n                                                            .size(35.dp)\n                                                    )\n                                                }\n                                                Column {\n                                                    post.post.author?.let {\n                                                        Text(\n                                                            text = \"u/$it\",\n                                                            style = MaterialTheme.typography.titleSmall\n                                                        )\n                                                    }\n                                                    post.post.subreddit?.let {\n                                                        Text(\n                                                            text = \"r/$it\",\n                                                            style = MaterialTheme.typography.bodyMedium,\n                                                            color = MaterialTheme.colorScheme.onSurface\n                                                        )\n                                                    }\n                                                }\n                                            }\n                                            Row(modifier = Modifier.padding(end = 15.dp)) {\n                                                FilledTonalIconButton(\n                                                    onClick = {\n                                                        navController.navigate(\"${NavDestinationKey.CommunityView}/${post.post.subreddit}\")\n                                                    },\n                                                    modifier = Modifier.width(33.dp)\n                                                ) {\n                                                    Icon(\n                                                        painter = painterResource(R.drawable.ic_community),\n                                                        contentDescription = stringResource(R.string.ic_community_cdesc)\n                                                    )\n                                                }\n                                                FilledTonalIconButton(\n                                                    onClick = {\n                                                        navController.navigate(\"${NavDestinationKey.UserView}/${post.post.author}\")\n                                                    },\n                                                    modifier = Modifier\n                                                        .padding(start = 10.dp)\n                                                        .width(33.dp)\n                                                ) {\n                                                    Icon(\n                                                        painter = painterResource(R.drawable.ic_person),\n                                                        contentDescription = stringResource(R.string.ic_person_cdesc)\n                                                    )\n                                                }\n                                            }\n                                        }\n                                        Text(\n                                            text = post.post.title,\n                                            style = MaterialTheme.typography.titleLarge,\n                                            modifier = Modifier.padding(\n                                                top = 15.dp,\n                                                start = 15.dp,\n                                                end = 15.dp\n                                            )\n                                        )\n\n                                        val width = post.post.previewWidth?.toFloat() ?: 0f\n                                        val height = post.post.previewHeight?.toFloat() ?: 0f\n                                        val imageUrl =\n                                            post.post.previewImageUrl?.replace(\"amp;\", \"\")\n                                                ?.ifEmpty { post.post.url }\n                                        val computedAspectRatio = if (width > 0f && height > 0f) {\n                                            (width / height).coerceIn(0.2f, 4f)\n                                        } else null\n\n                                        if (imageUrl !== null && imageUrl.isNotEmpty()) {\n                                            var aspectRatio by rememberSaveable(post.post.id + \"_ratio\") {\n                                                mutableStateOf<Float?>(computedAspectRatio)\n                                            }\n                                            aspectRatio?.let {\n                                                MeasuredAsyncImage(\n                                                    imageUrl = imageUrl,\n                                                    aspectRatio = it,\n                                                    modifier = Modifier\n                                                        .fillMaxWidth()\n                                                        .padding(horizontal = 15.dp)\n                                                        .padding(top = 15.dp)\n                                                        .clip(MaterialTheme.shapes.medium)\n                                                )\n                                            }\n                                        }\n\n                                        if (!post.post.selftext.isNullOrEmpty()) {\n                                            Text(\n                                                text = post.post.selftext,\n                                                modifier = Modifier.padding(15.dp),\n                                                style = MaterialTheme.typography.bodyLarge\n                                            )\n                                        }\n\n                                        Row(\n                                            modifier = Modifier\n                                                .fillMaxWidth()\n                                                .padding(horizontal = 15.dp)\n                                                .padding(top = if (post.post.selftext.isNullOrEmpty()) 15.dp else 0.dp)\n                                                .clip(MaterialTheme.shapes.medium)\n                                                .background(MaterialTheme.colorScheme.surfaceContainerLow),\n                                            verticalAlignment = Alignment.CenterVertically,\n                                            horizontalArrangement = Arrangement.SpaceBetween\n                                        ) {\n\n                                            var upvoteState by remember { mutableStateOf(post.post.likes == true) }\n                                            var downvoteState by remember { mutableStateOf(post.post.likes == false) }\n                                            var saveState by remember { mutableStateOf(post.post.saved == true) }\n\n                                            Row(\n                                                modifier = Modifier.padding(\n                                                    start = 5.dp,\n                                                    top = 5.dp,\n                                                    bottom = 5.dp\n                                                )\n                                            ) {\n                                                FilledTonalIconButton(\n                                                    onClick = {\n                                                        post.post.permalink.let { url ->\n                                                            val sendIntent = Intent().apply {\n                                                                action = Intent.ACTION_SEND\n                                                                type = \"text/plain\"\n                                                                putExtra(\n                                                                    Intent.EXTRA_TEXT,\n                                                                    \"https://reddit.com$url\"\n                                                                )\n                                                            }\n                                                            val shareIntent = Intent.createChooser(\n                                                                sendIntent,\n                                                                \"Share post\"\n                                                            )\n                                                            context.startActivity(shareIntent)\n                                                        }\n                                                    },\n                                                    shape = MaterialTheme.shapes.medium\n                                                ) {\n                                                    Icon(\n                                                        painter = painterResource(R.drawable.ic_share),\n                                                        contentDescription = stringResource(R.string.ic_share_cdesc)\n                                                    )\n                                                }\n                                                AnimatedTonalToggleIconButton(\n                                                    checked = saveState,\n                                                    onCheckedChange = { checked ->\n                                                        if (viewModel.isUserless) {\n                                                            viewModel.encourageUserAuthSnackbar()\n                                                        } else {\n                                                            saveState = checked\n                                                            val id =\n                                                                post.post.id.removePrefix(\"t3_\")\n                                                            browseViewModel.updatePostFavorite(\n                                                                checked,\n                                                                id\n                                                            )\n                                                        }\n                                                    },\n                                                    checkedIcon = painterResource(R.drawable.ic_bookmark_filled),\n                                                    uncheckedIcon = painterResource(R.drawable.ic_bookmark),\n                                                    contentDescription = stringResource(R.string.ic_bookmark_cdesc)\n                                                )\n                                            }\n                                            Row(\n                                                verticalAlignment = Alignment.CenterVertically,\n                                                modifier = Modifier.padding(end = 10.dp)\n                                            ) {\n                                                AnimatedTonalToggleIconButton(\n                                                    checked = downvoteState,\n                                                    onCheckedChange = { checked ->\n                                                        if (viewModel.isUserless) {\n                                                            viewModel.encourageUserAuthSnackbar()\n                                                        } else {\n                                                            downvoteState = checked\n                                                            if (upvoteState) {\n                                                                upvoteState = false\n                                                            }\n                                                            val id =\n                                                                post.post.id.removePrefix(\"t3_\")\n                                                            val dir = if (checked) -1 else 0\n                                                            viewModel.updateVote(dir, id)\n                                                        }\n                                                    },\n                                                    checkedIcon = painterResource(R.drawable.ic_downvote),\n                                                    uncheckedIcon = painterResource(R.drawable.ic_downvote),\n                                                    contentDescription = stringResource(R.string.ic_downvote_cdesc),\n                                                    modifier = Modifier.width(33.dp),\n                                                    uncheckedRadius = 30.dp,\n                                                    checkedRadius = MediumCornerRadius\n                                                )\n                                                post.post.ups?.toInt()?.prettyNumber()?.let {\n                                                    Text(\n                                                        text = it,\n                                                        style = MaterialTheme.typography.labelLarge,\n                                                        modifier = Modifier.padding(horizontal = 10.dp)\n                                                    )\n                                                }\n                                                AnimatedTonalToggleIconButton(\n                                                    checked = upvoteState,\n                                                    onCheckedChange = { checked ->\n                                                        if (viewModel.isUserless) {\n                                                            viewModel.encourageUserAuthSnackbar()\n                                                        } else {\n                                                            upvoteState = checked\n                                                            if (downvoteState) {\n                                                                downvoteState = false\n                                                            }\n                                                            val id =\n                                                                post.post.id.removePrefix(\"t3_\")\n                                                            val dir = if (checked) 1 else 0\n                                                            viewModel.updateVote(dir, id)\n                                                        }\n                                                    },\n                                                    checkedIcon = painterResource(R.drawable.ic_upvote),\n                                                    uncheckedIcon = painterResource(R.drawable.ic_upvote),\n                                                    contentDescription = stringResource(R.string.ic_upvote_cdesc),\n                                                    modifier = Modifier.width(33.dp),\n                                                    uncheckedRadius = 30.dp,\n                                                    checkedRadius = MediumCornerRadius\n                                                )\n                                            }\n                                        }\n                                    }\n                                }\n\n                                itemsIndexed(comments.itemSnapshotList) { _, commentWithUser ->\n                                    val author = commentWithUser?.comment?.author\n                                    LaunchedEffect(author) {\n                                        viewModel.fetchUserOnVisible(author)\n                                    }\n\n                                    val depth = commentWithUser?.comment?.depth ?: 0\n                                    val indentPerLevel = 10.dp\n                                    val barWidth = 2.dp\n\n                                    Box(modifier = Modifier.fillMaxWidth()) {\n                                        if (depth > 0) {\n                                            Row(\n                                                modifier = Modifier\n                                                    .matchParentSize()\n                                                    .padding(start = 15.dp)\n                                            ) {\n                                                for (i in 0 until depth) {\n                                                    Box(\n                                                        modifier = Modifier\n                                                            .fillMaxHeight()\n                                                            .width(barWidth)\n                                                            .background(MaterialTheme.colorScheme.surfaceContainerHighest)\n                                                    )\n                                                    Box(modifier = Modifier.width(indentPerLevel - barWidth))\n                                                }\n                                            }\n                                        }\n\n                                        CommentCard(\n                                            commentWithUser = commentWithUser,\n                                            onMoreClick = {\n                                                viewModel.apply {\n                                                    commentToShowMoreSheet = commentWithUser\n                                                    showingCommentMoreSheet = true\n                                                }\n                                            },\n                                            onUpvote = { upvoted, onSuccess ->\n                                                if (viewModel.isUserless) {\n                                                    viewModel.encourageUserAuthSnackbar()\n                                                } else {\n                                                    onSuccess()\n                                                    commentWithUser?.comment?.id?.removePrefix(\"t1_\")?.let {\n                                                        viewModel.updateVote(\n                                                            postId = it,\n                                                            direction = if (upvoted) 1 else 0\n                                                        )\n                                                    }\n                                                }\n                                            },\n                                            onDownvote = { downvoted, onSuccess ->\n                                                if (viewModel.isUserless) {\n                                                    viewModel.encourageUserAuthSnackbar()\n                                                } else {\n                                                    onSuccess()\n                                                    commentWithUser?.comment?.id?.removePrefix(\"t1_\")?.let {\n                                                        viewModel.updateVote(\n                                                            postId = it,\n                                                            direction = if (downvoted) -1 else 0\n                                                        )\n                                                    }\n                                                }\n                                            },\n                                            modifier = Modifier.padding(\n                                                start = 15.dp + (depth * indentPerLevel.value).dp,\n                                                end = 15.dp,\n                                                top = 10.dp,\n                                                bottom = 10.dp\n                                            )\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        if (viewModel.showingCommentMoreSheet) {\n            viewModel.commentToShowMoreSheet?.let {\n                CommentDetailSheet(\n                    commentWithUser = it,\n                    onDismissRequest = {\n                        viewModel.showingCommentMoreSheet = false\n                    },\n                    onDownvote = { downvoted, onSuccess ->\n                        if (viewModel.isUserless) {\n                            viewModel.apply {\n                                showingCommentMoreSheet = false\n                                encourageUserAuthSnackbar()\n                            }\n                        } else {\n                            onSuccess()\n                            it.comment.id.let {\n                                viewModel.updateVote(\n                                    postId = it,\n                                    direction = if (downvoted) -1 else 0\n                                )\n                            }\n                        }\n                    },\n                    onUpvote = { upvoted, onSuccess ->\n                        if (viewModel.isUserless) {\n                            viewModel.apply {\n                                showingCommentMoreSheet = false\n                                encourageUserAuthSnackbar()\n                            }\n                        } else {\n                            onSuccess()\n                            it.comment.id.removePrefix(\"t3_\").let {\n                                viewModel.updateVote(\n                                    postId = it,\n                                    direction = if (upvoted) 1 else 0\n                                )\n                            }\n                        }\n                    },\n                    onSaveClick = { saved, onSuccess ->\n                        if (viewModel.isUserless) {\n                            viewModel.apply {\n                                showingCommentMoreSheet = false\n                                encourageUserAuthSnackbar()\n                            }\n                        } else {\n                            val id = it.comment.id.removePrefix(\"t3_\")\n                            browseViewModel.updatePostFavorite(saved, id)\n                            onSuccess()\n                        }\n                    },\n                    onViewUserClick = {\n                        viewModel.showingCommentMoreSheet = false\n                        navController.navigate(\"${NavDestinationKey.UserView}/${it.comment.author}\")\n                    }\n                )\n            }\n        }\n\n        if (viewModel.showingMoreSheet) {\n            postWithUser.value?.let {\n                PostOptionSheet(\n                    postData = it.post.toPostData(),\n                    onDismissRequest = { viewModel.showingMoreSheet = false },\n                    onViewUser = {\n                        navController.navigate(\"${NavDestinationKey.UserView}/${it.post.author}\")\n                    },\n                    onViewCommunity = {\n                        navController.navigate(\"${NavDestinationKey.CommunityView}/${it.post.subreddit}\")\n                    },\n                    onOpenExternal = {\n                        Intent(Intent.ACTION_VIEW).apply {\n                            this.data = \"https://www.reddit.com${it.post.permalink}\".toUri()\n                            navController.context.startActivity(this)\n                        }\n                    },\n                    onReport = {\n                        Intent(Intent.ACTION_VIEW).apply {\n                            this.data = (\"https://www.reddit.com/report\"\n                                    + \"?url=https://www.reddit.com${it.post.permalink}\").toUri()\n                            navController.context.startActivity(this)\n                        }\n                    }\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/view/SearchPage.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n\npackage com.pineapple.app.ui.view\n\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LoadingIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SearchBar\nimport androidx.compose.material3.SearchBarDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.coerceAtLeast\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport androidx.paging.LoadState\nimport androidx.paging.compose.collectAsLazyPagingItems\nimport coil3.compose.rememberAsyncImagePainter\nimport coil3.request.CachePolicy\nimport coil3.request.ImageRequest\nimport com.pineapple.app.R\nimport com.pineapple.app.consts.NavDestinationKey\nimport com.pineapple.app.ui.components.TonalActionSectionItem\nimport com.pineapple.app.ui.components.TonalActionSectionList\nimport com.pineapple.app.ui.components.PostCard\nimport com.pineapple.app.ui.theme.PineappleTheme\nimport com.pineapple.app.ui.viewmodel.SearchViewModel\nimport com.pineapple.app.utilities.toPostData\nimport com.pineapple.app.utilities.toUserAboutListing\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.graphics.painter.Painter\n\n@Composable\nfun SearchPage(navController: NavController) {\n    val viewModel: SearchViewModel = hiltViewModel()\n    val context = LocalContext.current\n    val searchResults = viewModel.searchResults.collectAsLazyPagingItems()\n    val subredditSuggestions by viewModel.subredditSuggestions.collectAsState()\n    val userSuggestions by viewModel.userSuggestions.collectAsState()\n    val motionScheme = MaterialTheme.motionScheme\n    val searchFieldPadding by animateDpAsState(\n        targetValue = if (viewModel.expandedSearchField) 0.dp else 15.dp,\n        animationSpec = motionScheme.fastSpatialSpec(),\n        label = \"search_padding\"\n    )\n\n    PineappleTheme {\n        Column {\n            Column(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(\n                        top = searchFieldPadding.coerceAtLeast(0.dp),\n                        start = searchFieldPadding.coerceAtLeast(0.dp),\n                        end = searchFieldPadding.coerceAtLeast(0.dp)\n                    )\n            ) {\n                SearchBar(\n                    inputField = {\n                        SearchBarDefaults.InputField(\n                            query = viewModel.searchFieldValue,\n                            // when the user types and clears the field, clear the active search results\n                            onQueryChange = { newText ->\n                                viewModel.updateQueryText(newText)\n                                if (newText.isBlank()) {\n                                    viewModel.clearSearchQuery()\n                                }\n                            },\n                            onSearch = {\n                                viewModel.submitSearch()\n                            },\n                            expanded = viewModel.expandedSearchField,\n                            onExpandedChange = { viewModel.expandedSearchField = it },\n                            placeholder = {\n                                Text(stringResource(R.string.search_placeholder))\n                            },\n                            leadingIcon = {\n                                AnimatedContent(viewModel.expandedSearchField) { expanded ->\n                                    if (expanded) {\n                                        IconButton(\n                                            onClick = {\n                                               viewModel.expandedSearchField = false\n                                                viewModel.updateQueryText(\"\")\n                                                viewModel.clearSearchQuery()\n                                            }\n                                        ) {\n                                            Icon(\n                                                painter = painterResource(R.drawable.ic_back),\n                                                contentDescription = stringResource(R.string.ic_back_cdesc)\n                                            )\n                                        }\n                                    } else {\n                                        Icon(\n                                            painter = painterResource(R.drawable.ic_search),\n                                            contentDescription = stringResource(R.string.ic_search_cdesc)\n                                        )\n                                    }\n                                }\n                            },\n                            trailingIcon = {\n                                AnimatedContent(\n                                    targetState = viewModel.searchFieldValue.isNotEmpty(),\n                                    transitionSpec = {\n                                        scaleIn(motionScheme.fastSpatialSpec())\n                                            .togetherWith(scaleOut(motionScheme.fastSpatialSpec()))\n                                    }\n                                ) { show ->\n                                    if (show) {\n                                        IconButton(\n                                            onClick = {\n                                                viewModel.updateQueryText(\"\")\n                                                viewModel.clearSearchQuery()\n                                            }\n                                        ) {\n                                            Icon(\n                                                painter = painterResource(R.drawable.ic_close),\n                                                contentDescription = stringResource(R.string.ic_close_cdesc)\n                                            )\n                                        }\n                                    }\n                                }\n                            }\n                        )\n                    },\n                    expanded = viewModel.expandedSearchField,\n                    onExpandedChange = { viewModel.expandedSearchField = it },\n                    modifier = Modifier.fillMaxWidth(),\n                    content = {\n                        AnimatedContent(\n                            targetState = subredditSuggestions.isNotEmpty(),\n                            modifier = Modifier.fillMaxWidth()\n                        ) { suggestions ->\n                            if (suggestions) {\n                                Column {\n                                    val items = subredditSuggestions.map { sd ->\n                                        val name = sd.displayName\n                                        val iconUrl = sd.iconUrl\n\n                                        val painter: Painter =\n                                            if (iconUrl.isBlank() || iconUrl.contains(\n                                                    \"default\",\n                                                    ignoreCase = true\n                                                )\n                                            ) {\n                                                painterResource(R.drawable.generic_community)\n                                            } else {\n                                                rememberAsyncImagePainter(\n                                                    ImageRequest.Builder(context)\n                                                        .data(iconUrl)\n                                                        .diskCachePolicy(CachePolicy.ENABLED)\n                                                        .memoryCachePolicy(CachePolicy.ENABLED)\n                                                        .build()\n                                                )\n                                            }\n\n                                        TonalActionSectionItem(\n                                            text = \"r/$name\",\n                                            icon = painter,\n                                            contentDescription = \"Subreddit $name\",\n                                            onCLick = {\n                                                viewModel.updateQueryText(name)\n                                                viewModel.submitSearch()\n                                            },\n                                            shouldTintIcon = false\n                                        )\n                                    }\n                                    Text(\n                                        text = stringResource(R.string.search_communities),\n                                        style = MaterialTheme.typography.titleSmall,\n                                        modifier = Modifier.padding(\n                                            top = 15.dp,\n                                            start = 15.dp,\n                                            bottom = 10.dp\n                                        ),\n                                        color = MaterialTheme.colorScheme.onSurfaceVariant\n                                    )\n                                    TonalActionSectionList(\n                                        items = items,\n                                        modifier = Modifier.padding(horizontal = 15.dp),\n                                        containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,\n                                        listItemContainerColor = MaterialTheme.colorScheme.surfaceContainerLow\n                                    )\n                                }\n                            }\n                        }\n\n                        AnimatedContent(\n                            targetState = userSuggestions.isNotEmpty(),\n                            modifier = Modifier.fillMaxWidth()\n                        ) { suggestions ->\n                            if (suggestions) {\n                                Column {\n                                    val items = userSuggestions.map { ua ->\n                                        val name = ua.name ?: \"\"\n                                        val iconUrl = ua.icon_img ?: ua.snoovatar_img\n                                        val isDefaultIcon = ua.subreddit?.is_default_icon ?: false\n\n                                        val painter = if (isDefaultIcon || iconUrl.isNullOrBlank()) {\n                                            painterResource(R.drawable.generic_avatar)\n                                        } else {\n                                            rememberAsyncImagePainter(\n                                                ImageRequest.Builder(context)\n                                                    .data(iconUrl)\n                                                    .diskCachePolicy(CachePolicy.ENABLED)\n                                                    .memoryCachePolicy(CachePolicy.ENABLED)\n                                                    .build()\n                                            )\n                                        }\n\n                                        TonalActionSectionItem(\n                                            text = \"u/$name\",\n                                            icon = painter,\n                                            contentDescription = \"User $name\",\n                                            onCLick = {\n                                                viewModel.updateQueryText(name)\n                                                viewModel.submitSearch()\n                                            },\n                                            shouldTintIcon = false\n                                        )\n                                    }\n                                    Text(\n                                        text = stringResource(R.string.search_users),\n                                        style = MaterialTheme.typography.titleSmall,\n                                        modifier = Modifier.padding(\n                                            top = 15.dp,\n                                            start = 15.dp,\n                                            bottom = 10.dp\n                                        ),\n                                        color = MaterialTheme.colorScheme.onSurfaceVariant\n                                    )\n                                    TonalActionSectionList(\n                                        items = items,\n                                        modifier = Modifier.padding(horizontal = 15.dp),\n                                        containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,\n                                        listItemContainerColor = MaterialTheme.colorScheme.surfaceContainerLow\n                                    )\n                                }\n                            }\n                        }\n                    }\n                )\n            }\n\n            AnimatedContent(searchResults) { result ->\n                if (result.loadState.refresh is LoadState.Loading\n                    && viewModel.searchFieldValue.isNotEmpty()\n                ) {\n                    Box(Modifier.fillMaxSize()) {\n                        LoadingIndicator(Modifier.size(75.dp).align(Alignment.Center))\n                    }\n                } else if (result.itemCount != 0) {\n                    LazyColumn(\n                        modifier = Modifier.padding(horizontal = 10.dp),\n                        state = viewModel.postListState\n                    ) {\n                        itemsIndexed(result.itemSnapshotList.items) { index, item ->\n                            PostCard(\n                                postData = item.post.toPostData(),\n                                modifier = Modifier.padding(top = 10.dp),\n                                userInfo = item.user?.toUserAboutListing(),\n                                onClick = {\n                                    // pass id without the t3_ prefix (PostView expects raw id)\n                                    val rawId = item.post.id.removePrefix(\"t3_\")\n                                    navController.navigate(\"${NavDestinationKey.PostView}/$rawId\")\n                                },\n                                onMoreClick = { },\n                                onSaveClick = { _, _ -> },\n                                onUpvote = { _, _ -> },\n                                onDownvote = { _, _ -> },\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/view/UserView.kt",
    "content": "package com.pineapple.app.ui.view\n\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.navigation.NavController\nimport com.pineapple.app.ui.theme.PineappleTheme\n\n@Composable\nfun UserView(navController: NavController, user: String) {\n    PineappleTheme {\n        Text(user)\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/view/WelcomeView.kt",
    "content": "package com.pineapple.app.ui.view\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBarsPadding\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation.NavController\nimport com.pineapple.app.R\nimport com.pineapple.app.consts.NavDestinationKey\nimport com.pineapple.app.consts.OnboardingLoginType\nimport com.pineapple.app.ui.theme.PineappleTheme\n\n@Composable\nfun WelcomeView(navController: NavController) {\n    PineappleTheme {\n        Surface {\n\n            Column(\n                horizontalAlignment = Alignment.CenterHorizontally,\n                verticalArrangement = Arrangement.SpaceBetween,\n                modifier = Modifier.systemBarsPadding().fillMaxSize()\n            ) {\n                Column(\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                    modifier = Modifier.padding(start = 30.dp, end = 30.dp, top = 40.dp)\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.ic_pineapple_logo),\n                        contentDescription = stringResource(R.string.ic_pineapple_logo_cdesc),\n                        tint = MaterialTheme.colorScheme.primary\n                    )\n                    Text(\n                        text = stringResource(R.string.welcome_app_name),\n                        style = MaterialTheme.typography.displayMedium,\n                        color = MaterialTheme.colorScheme.onSurface,\n                        modifier = Modifier.padding(top = 20.dp)\n                    )\n                    Text(\n                        text = stringResource(R.string.welcome_slogan_text),\n                        style = MaterialTheme.typography.bodyLarge,\n                        color = MaterialTheme.colorScheme.onSurface,\n                        modifier = Modifier.padding(top = 10.dp),\n                        textAlign = TextAlign.Center\n                    )\n                }\n                Column(\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                    modifier = Modifier.padding(bottom = 30.dp)\n                ) {\n                    Button(\n                        onClick = {\n                            navController.navigate(\n                                \"${NavDestinationKey.KeyProviderView}/${OnboardingLoginType.RedditAuth}\"\n                            )\n                        },\n                        shape = MaterialTheme.shapes.large\n                    ) {\n                       Text(\n                           text = stringResource(R.string.welcome_sign_in_button),\n                           modifier = Modifier.padding(10.dp),\n                           style = MaterialTheme.typography.titleMedium\n                       )\n                    }\n\n                    Button(\n                        onClick = {\n                            navController.navigate(\n                                \"${NavDestinationKey.KeyProviderView}/${OnboardingLoginType.Guest}\"\n                            )\n                        },\n                        shape = MaterialTheme.shapes.medium,\n                        colors = ButtonDefaults.filledTonalButtonColors(),\n                        modifier = Modifier.padding(top = 10.dp)\n                    ) {\n                        Text(\n                            text = stringResource(R.string.welcome_continue_as_guest_button),\n                            style = MaterialTheme.typography.labelLarge\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/viewmodel/BrowseViewModel.kt",
    "content": "@file:OptIn(ExperimentalCoroutinesApi::class)\n\npackage com.pineapple.app.ui.viewmodel\n\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport androidx.paging.cachedIn\nimport com.pineapple.app.consts.MMKVKey\nimport com.pineapple.app.consts.PostFilterSort\nimport com.pineapple.app.consts.PostFilterTime\nimport com.pineapple.app.network.paging.PagingRepository\nimport com.pineapple.app.network.repository.RedditRepository\nimport com.tencent.mmkv.MMKV\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass BrowseViewModel @Inject constructor(\n    private val postRepository: PagingRepository,\n    private val mmkv: MMKV,\n    private val redditRepository: RedditRepository\n) : ViewModel() {\n\n    val isUserless by lazy { mmkv.getBoolean(MMKVKey.USER_GUEST, true) }\n\n    var currentFilterTime by mutableStateOf(PostFilterTime.TIME_DAY)\n    var currentFilterSort by mutableStateOf(PostFilterSort.SORT_HOT)\n\n    var shouldScrollToTopAfterRefresh by mutableStateOf(false)\n\n    val postListState = LazyListState()\n\n    fun updateFilters(sort: String, time: String) {\n        if (sort != currentFilterSort || time != currentFilterTime) {\n            currentFilterSort = sort\n            currentFilterTime = time\n            shouldScrollToTopAfterRefresh = true\n        }\n    }\n\n    val pagedPosts = snapshotFlow { currentFilterSort to currentFilterTime }\n        .flatMapLatest { (sort, time) ->\n            postRepository\n                .postsPager(\n                    subreddit = \"all\",\n                    sort = sort,\n                    time = time\n                ).flow\n        }\n        .cachedIn(viewModelScope)\n\n    /**\n     * Post an update to the reddit API that reflects the bookmark state of a post\n     * Also update local cache via repository so UI reacts immediately\n     * @param save: True if we save the post, false otherwise\n     * @param postID: The ID of the post to update (without prefix)\n     */\n    fun updatePostFavorite(save: Boolean, postID: String) {\n        viewModelScope.launch {\n            if (save) {\n                redditRepository.savePost(postID)\n            } else {\n                redditRepository.unsavePost(postID)\n            }\n        }\n    }\n\n    /**\n     * Post an update to the reddit API that reflects whether a post is up/downvoted\n     * Also update local cache via repository so UI reacts immediately\n     * @param direction: 1 for upvote, -1 for downvote, 0 for no vote or to remove vote\n     * @param postId: The ID of the post to update (without prefix)\n     */\n    fun updatePostVote(direction: Int, postId: String) {\n        viewModelScope.launch {\n            redditRepository.castVoteAndCache(postId, direction)\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/viewmodel/HomeViewModel.kt",
    "content": "@file:OptIn(ExperimentalCoroutinesApi::class)\n\npackage com.pineapple.app.ui.viewmodel\n\nimport android.content.Context\nimport android.content.Intent\nimport androidx.compose.material3.SnackbarDuration\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.SnackbarResult\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.core.net.toUri\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.pineapple.app.R\nimport com.pineapple.app.consts.MMKVKey\nimport com.pineapple.app.consts.PageDestinationKey\nimport com.pineapple.app.network.model.reddit.PostData\nimport com.pineapple.app.network.paging.PagingRepository\nimport com.pineapple.app.network.repository.RedditRepository\nimport com.tencent.mmkv.MMKV\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.launch\nimport java.util.UUID\nimport javax.inject.Inject\n\n@HiltViewModel\nclass HomeViewModel @Inject constructor(\n    @ApplicationContext private val context: Context,\n    private val repository: RedditRepository,\n    private val mmkv: MMKV\n) : ViewModel() {\n\n    val snackbarState = SnackbarHostState()\n    var showPostFilterSheet by mutableStateOf(false)\n    var showPostOptionSheet by mutableStateOf(false)\n    var currentPostOptionData by mutableStateOf<PostData?>(null)\n\n    var currentNavPage by mutableIntStateOf(PageDestinationKey.BROWSE)\n\n    val isUserless by lazy { mmkv.getBoolean(MMKVKey.USER_GUEST, true) }\n\n    val topSubreddits = repository.observePopularSubreddits()\n    val subscribedSubreddits = repository.observeSubscribedSubreddits()\n\n    init {\n        viewModelScope.launch {\n            repository.refreshPopularSubreddits()\n            if (!isUserless) {\n                repository.refreshSubscribedSubreddits()\n            }\n        }\n    }\n\n    /**\n     * Open the post overflow bottom sheet menu\n     */\n    fun openPostOptionSheet(postData: PostData) {\n        currentPostOptionData = postData\n        showPostOptionSheet = true\n    }\n\n    /**\n     * Open a snackbar notifying the user that the action they are trying to perform requires\n     * authentication, with a button allowing them to log in if they want to\n     */\n    fun encourageUserAuthSnackbar() {\n        viewModelScope.launch {\n            val result = snackbarState.showSnackbar(\n                message = context.getString(R.string.home_snackbar_auth_text),\n                actionLabel = context.getString(R.string.home_snackbar_auth_login),\n                duration = SnackbarDuration.Long\n            )\n            if (result == SnackbarResult.ActionPerformed) {\n                launchRedditAuthFlow(context)\n            }\n        }\n    }\n\n    /**\n     * Open the reddit authentication flow in the default browser, which will return to the app\n     * via a deep link once completed, containing the code in a query parameter. (handled in NavHost)\n     */\n    fun launchRedditAuthFlow(context: Context) {\n        Intent(Intent.ACTION_VIEW).apply {\n            data = (\"https://www.reddit.com/api/v1/authorize.compact\" +\n                            \"?client_id=${mmkv.decodeString(MMKVKey.CLIENT_ID)}\" +\n                            \"&response_type=code\" +\n                            \"&state=${UUID.randomUUID()}\" +\n                            \"&redirect_uri=pineapple://login\" +\n                            \"&duration=permanent\" +\n                            \"&scope=identity edit flair history modconfig modflair modlog \" +\n                            \"modposts, modwiki mysubreddits privatemessages read report save \" +\n                            \"submit subscribe vote wikiedit wikiread\"\n                    ).toUri()\n            flags = Intent.FLAG_ACTIVITY_NEW_TASK\n            context.startActivity(this)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/viewmodel/KeyProviderViewModel.kt",
    "content": "package com.pineapple.app.ui.viewmodel\n\nimport android.content.Context\nimport android.content.Intent\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.core.net.toUri\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.pineapple.app.ui.state.AuthViewState\nimport com.pineapple.app.consts.MMKVKey\nimport com.pineapple.app.network.repository.RedditAuthRepository\nimport com.pineapple.app.network.repository.RedditRepository\nimport com.tencent.mmkv.MMKV\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\nimport java.util.UUID\nimport javax.inject.Inject\n\n@HiltViewModel\nclass KeyProviderViewModel @Inject constructor(\n    private val repository: RedditAuthRepository,\n    val mmkv: MMKV\n) : ViewModel() {\n\n    var clientSecretTextFieldValue by mutableStateOf(TextFieldValue())\n\n    private val internalViewState = MutableStateFlow<AuthViewState>(AuthViewState.Idle)\n    val viewState: StateFlow<AuthViewState> = internalViewState.asStateFlow()\n\n    /**\n     * Attempt to get an authentication token (userless) to see if the provided client ID\n     * is a valid one. If the user chose to be a guest, this is the end of the authentication\n     * flow, however if they wanted to login, they will need to authenticate via Reddit OAuth next.\n     */\n    fun submitClientSecret() {\n        viewModelScope.launch {\n            internalViewState.value = AuthViewState.Loading\n            try {\n                repository.authenticateUserless(\n                    clientId = clientSecretTextFieldValue.text,\n                    testingClientID = true\n                )\n                mmkv.putString(MMKVKey.CLIENT_ID, clientSecretTextFieldValue.text)\n                internalViewState.value = AuthViewState.Success\n            } catch (_: Exception) {\n                internalViewState.value = AuthViewState.Error(\"Invalid client secret\")\n            }\n        }\n    }\n\n    /**\n     * Open the reddit authentication flow in the default browser, which will return to the app\n     * via a deep link once completed, containing the code in a query parameter. (handled in NavHost)\n     */\n    fun launchRedditAuthFlow(context: Context) {\n        Intent(Intent.ACTION_VIEW).apply {\n            data = (\"https://www.reddit.com/api/v1/authorize.compact\" +\n                    \"?client_id=${mmkv.decodeString(MMKVKey.CLIENT_ID)}\" +\n                    \"&response_type=code\" +\n                    \"&state=${UUID.randomUUID()}\" +\n                    \"&redirect_uri=pineapple://login\" +\n                    \"&duration=permanent\" +\n                    \"&scope=identity edit flair history modconfig modflair modlog \" +\n                    \"modposts, modwiki mysubreddits privatemessages read report save \" +\n                    \"submit subscribe vote wikiedit wikiread\"\n                    ).toUri()\n            flags = Intent.FLAG_ACTIVITY_NEW_TASK\n            context.startActivity(this)\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/viewmodel/PostViewModel.kt",
    "content": "package com.pineapple.app.ui.viewmodel\n\nimport android.content.Context\nimport android.content.Intent\nimport androidx.compose.material3.SnackbarDuration\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.SnackbarResult\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.core.net.toUri\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport androidx.paging.PagingData\nimport androidx.paging.cachedIn\nimport com.pineapple.app.R\nimport com.pineapple.app.consts.MMKVKey\nimport com.pineapple.app.network.model.cache.CommentWithUser\nimport com.pineapple.app.network.model.cache.PostWithUser\nimport com.pineapple.app.network.paging.PagingRepository\nimport com.pineapple.app.network.repository.RedditRepository\nimport com.tencent.mmkv.MMKV\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.collect\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.flow.collectLatest\nimport java.util.Collections\nimport java.util.UUID\nimport javax.inject.Inject\n\n@HiltViewModel\nclass PostViewModel @Inject constructor(\n    @ApplicationContext private val context: Context,\n    private val repository: RedditRepository,\n    private val pagingRepository: PagingRepository,\n    private val mmkv: MMKV\n) : ViewModel() {\n\n    val postState = MutableStateFlow<PostWithUser?>(null)\n    val isLoading = MutableStateFlow(true)\n\n    // replyCounts maps comment full-id (e.g. \"t1_abc\") to cached count (or -1 if unknown)\n    private val _replyCounts = MutableStateFlow<Map<String, Int>>(emptyMap())\n    val replyCounts: StateFlow<Map<String, Int>> = _replyCounts\n\n    /** Observe replies for a parent comment as stored in Room (commentDao) */\n    fun observeRepliesForComment(parentCommentFullId: String): Flow<List<CommentWithUser>> {\n        return repository.observeRepliesForComment(parentCommentFullId)\n    }\n\n    /**\n     * Load replies for a specific comment: request network fetch which will update Room\n     * and return the number of replies parsed; UI can observe `observeRepliesForComment` for inserted replies.\n     */\n    fun loadRepliesForComment(postId: String, commentIdNoPrefix: String) {\n        val full = if (commentIdNoPrefix.startsWith(\"t1_\")) commentIdNoPrefix else \"t1_$commentIdNoPrefix\"\n        viewModelScope.launch {\n            try {\n                // refreshRepliesForComment returns the number of replies parsed and inserted\n                val parsed = repository.refreshRepliesForComment(postId, commentIdNoPrefix)\n                if (parsed >= 0) {\n                    _replyCounts.value = _replyCounts.value + (full to parsed)\n                }\n            } catch (_: Throwable) {\n                // swallow minimal\n            }\n        }\n    }\n\n    var showingMoreSheet by mutableStateOf(false)\n\n    var showingCommentReplySheet by mutableStateOf(false)\n    var commentToShowReplySheet by mutableStateOf<CommentWithUser?>(null)\n\n    var showingCommentMoreSheet by mutableStateOf(false)\n    var commentToShowMoreSheet by mutableStateOf<CommentWithUser?>(null)\n\n    val isUserless by lazy { mmkv.getBoolean(MMKVKey.USER_GUEST, true) }\n\n    // Comments exposed as a StateFlow of PagingData so UI can `collectAsState()` easily\n    private val _comments = MutableStateFlow<PagingData<CommentWithUser>>(PagingData.empty())\n    val comments = _comments\n    private var commentsJob: Job? = null\n\n    // Deduplicate in-flight user fetches to avoid duplicate network calls during fast scrolls\n    private val fetchingUsers = Collections.synchronizedSet(mutableSetOf<String>())\n\n    val snackbarState = SnackbarHostState()\n\n    fun observePost(postId: String) {\n        viewModelScope.launch {\n            repository.observePostWithUser(postId)\n                .collect { postWithUser ->\n                    postState.value = postWithUser\n                    if (postWithUser != null) {\n                        isLoading.value = false\n                    }\n                }\n        }\n    }\n\n    fun refresh(postId: String) {\n        viewModelScope.launch {\n            try {\n                isLoading.value = true\n                repository.refreshPostAndAuthor(postId)\n            } finally {\n                isLoading.value = false\n            }\n        }\n    }\n\n    fun loadPost(postId: String) {\n        // Called from LaunchedEffect(postID)\n        observePost(postId)\n        refresh(postId)\n        // start collecting paged comments for this post so UI can observe `comments`\n        startCommentsForPost(postId)\n    }\n\n    // Start collecting comments PagingData into a StateFlow (cancels previous collection)\n    fun startCommentsForPost(postId: String) {\n        commentsJob?.cancel()\n        commentsJob = viewModelScope.launch {\n            pagingRepository.commentsPager(postId).flow\n                .cachedIn(viewModelScope)\n                .collectLatest { pagingData ->\n                    _comments.value = pagingData\n                }\n        }\n    }\n\n    // Trigger a refresh to fetch comments and cache them\n    fun refreshComments(postId: String) {\n        viewModelScope.launch {\n            repository.refreshCommentsForPost(postId)\n        }\n    }\n\n    /**\n     * Public helper: called by the UI when a comment row becomes visible.\n     * Dedupes in-flight requests by username and launches an IO coroutine to fetch and cache the user.\n     */\n    fun fetchUserOnVisible(username: String?) {\n        if (username.isNullOrBlank()) return\n        val first = fetchingUsers.add(username)\n        if (!first) return\n\n        viewModelScope.launch(Dispatchers.IO) {\n            try {\n                repository.fetchAndCacheUser(username)\n            } catch (_: Throwable) {\n                // swallow - minimal behavior (no logging)\n            } finally {\n                fetchingUsers.remove(username)\n            }\n        }\n    }\n\n    override fun onCleared() {\n        super.onCleared()\n        commentsJob?.cancel()\n    }\n\n    /**\n     * Open a snackbar notifying the user that the action they are trying to perform requires\n     * authentication, with a button allowing them to log in if they want to\n     */\n    fun encourageUserAuthSnackbar() {\n        viewModelScope.launch {\n            val result = snackbarState.showSnackbar(\n                message = context.getString(R.string.home_snackbar_auth_text),\n                actionLabel = context.getString(R.string.home_snackbar_auth_login),\n                duration = SnackbarDuration.Long\n            )\n            if (result == SnackbarResult.ActionPerformed) {\n                launchRedditAuthFlow(context)\n            }\n        }\n    }\n\n    /**\n     * Open the reddit authentication flow in the default browser, which will return to the app\n     * via a deep link once completed, containing the code in a query parameter. (handled in NavHost)\n     */\n    fun launchRedditAuthFlow(context: Context) {\n        Intent(Intent.ACTION_VIEW).apply {\n            data = (\"https://www.reddit.com/api/v1/authorize.compact\" +\n                    \"?client_id=${mmkv.decodeString(MMKVKey.CLIENT_ID)}\" +\n                    \"&response_type=code\" +\n                    \"&state=${UUID.randomUUID()}\" +\n                    \"&redirect_uri=pineapple://login\" +\n                    \"&duration=permanent\" +\n                    \"&scope=identity edit flair history modconfig modflair modlog \" +\n                    \"modposts, modwiki mysubreddits privatemessages read report save \" +\n                    \"submit subscribe vote wikiedit wikiread\"\n                    ).toUri()\n            flags = Intent.FLAG_ACTIVITY_NEW_TASK\n            context.startActivity(this)\n        }\n    }\n\n    /**\n     * Post an update to the reddit API that reflects whether a post/comment is up/downvoted\n     * Also update local cache via repository so UI reacts immediately\n     * @param direction: 1 for upvote, -1 for downvote, 0 for no vote or to remove vote\n     * @param postId: The ID of the post to update (without prefix)\n     */\n    fun updateVote(direction: Int, postId: String) {\n        viewModelScope.launch {\n            repository.castVoteAndCache(postId, direction, prefix = \"\")\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/ui/viewmodel/SearchViewModel.kt",
    "content": "@file:OptIn(ExperimentalCoroutinesApi::class, kotlinx.coroutines.FlowPreview::class)\n\npackage com.pineapple.app.ui.viewmodel\n\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport androidx.paging.PagingData\nimport androidx.paging.cachedIn\nimport com.pineapple.app.network.model.cache.PostWithUser\nimport com.pineapple.app.network.model.reddit.SubredditData\nimport com.pineapple.app.network.model.reddit.UserAbout\nimport com.pineapple.app.network.paging.PagingRepository\nimport com.pineapple.app.network.repository.RedditRepository\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.flowOf\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass SearchViewModel @Inject constructor(\n    private val pagingRepository: PagingRepository,\n    private val redditRepository: RedditRepository\n) : ViewModel() {\n\n    var expandedSearchField by mutableStateOf(false)\n    var searchFieldValue by mutableStateOf(\"\")\n\n    private val queryState = MutableStateFlow(\"\")\n\n    val postListState = LazyListState()\n\n    // Suggestions: use model types for clarity\n    val subredditSuggestions = MutableStateFlow<List<SubredditData>>(emptyList())\n    val userSuggestions = MutableStateFlow<List<UserAbout>>(emptyList())\n\n    init {\n        // fetch suggestions when queryState changes with a short debounce\n        viewModelScope.launch {\n            queryState\n                .debounce(300)\n                .distinctUntilChanged()\n                .collect { query ->\n                    if (query.isBlank()) {\n                        subredditSuggestions.value = emptyList()\n                        userSuggestions.value = emptyList()\n                    } else {\n                        // fetch suggestions (no caching)\n                        try {\n                            val subs = redditRepository.suggestCommunities(query, limit = 3)\n                            subredditSuggestions.value = subs\n                        } catch (_: Exception) {\n                            subredditSuggestions.value = emptyList()\n                        }\n                        try {\n                            val users = redditRepository.suggestUsers(query, limit = 3)\n                            userSuggestions.value = users\n                        } catch (_: Exception) {\n                            userSuggestions.value = emptyList()\n                        }\n                    }\n                }\n        }\n    }\n\n    val searchResults = queryState\n        .flatMapLatest { query ->\n            if (query.isBlank()) {\n                flowOf(PagingData.empty<PostWithUser>())\n            } else {\n                pagingRepository.searchPostsPager(query = query).flow\n            }\n        }\n        .cachedIn(viewModelScope)\n\n    // Called when user types in the search field - updates visible text and active query\n    fun updateQueryText(newText: String) {\n        searchFieldValue = newText\n        queryState.value = newText\n    }\n\n    fun submitSearch() {\n        expandedSearchField = false\n        queryState.value = searchFieldValue\n        postListState.requestScrollToItem(0)\n    }\n\n    // Clear the active search query\n    fun clearSearchQuery() {\n        queryState.value = \"\"\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/utilities/NumberUtilities.kt",
    "content": "package com.pineapple.app.utilities\n\nimport java.util.Locale\nimport kotlin.math.ln\nimport kotlin.math.pow\n\n/**\n * Convert a unix timestamp (UTC) to a pretty-formatted time relative to now (e.g., \"5m\" or \"16h\")\n */\nfun Long.convertUnixToRelativeTime() : String {\n    val longTime = this * 1000L\n    val currentTime = System.currentTimeMillis()\n    val secondMs = 1000L\n    val minuteMs = 60L * secondMs\n    val hourMs = 60L * minuteMs\n    val dayMs = 24L * hourMs\n    val weekMs = 7L * dayMs\n    val monthMs = 30L * dayMs\n    val yearMs = 12L * monthMs\n    val timeDifference = currentTime - longTime\n    return when {\n        timeDifference < minuteMs -> \"${timeDifference / secondMs}s\"\n        timeDifference < 60L * minuteMs -> \"${timeDifference / minuteMs}m\"\n        timeDifference < 24L * hourMs -> \"${timeDifference / hourMs}h\"\n        timeDifference < 30L * dayMs -> \"${timeDifference / dayMs}d\"\n        timeDifference < 7L * weekMs -> \"${timeDifference / weekMs}w\"\n        timeDifference < 12L * monthMs -> \"${timeDifference / monthMs}mo\"\n        timeDifference > yearMs -> \"${timeDifference / yearMs}y\"\n        else -> \"\"\n    }\n}\n\n/**\n * Convert an integer to a social media style pretty-formatted number (e.g., 1500 -> \"1.5K\")\n */\nfun Int.prettyNumber() : String {\n    if (this < 1000) return \"\" + this\n    val exp = (ln(this.toDouble()) / ln(1000.0)).toInt()\n    return java.lang.String.format(\n        Locale.ENGLISH, \"%.1f%c\",\n        (this / 1000.0.pow(exp.toDouble())), \"KMGTPE\"[exp - 1]\n    )\n}"
  },
  {
    "path": "app/src/main/java/com/pineapple/app/utilities/TypeUtilities.kt",
    "content": "package com.pineapple.app.utilities\n\nimport com.pineapple.app.network.caching.entity.PostEntity\nimport com.pineapple.app.network.caching.entity.SubredditEntity\nimport com.pineapple.app.network.caching.entity.UserEntity\nimport com.pineapple.app.network.model.reddit.Image\nimport com.pineapple.app.network.model.reddit.PostData\nimport com.pineapple.app.network.model.reddit.Preview\nimport com.pineapple.app.network.model.reddit.ResizedIcon\nimport com.pineapple.app.network.model.reddit.SubredditItem\nimport com.pineapple.app.network.model.reddit.UserAbout\nimport com.pineapple.app.network.model.reddit.UserAboutListing\n\n/**\n * Convert a cached PostEntity to a PostData object\n */\nfun PostEntity.toPostData(): PostData {\n    val source = if (previewImageUrl != null && previewWidth != null && previewHeight != null) {\n        ResizedIcon(\n            url = previewImageUrl,\n            width = previewWidth,\n            height = previewHeight\n        )\n    } else null\n    val preview = source?.let {\n        Preview(\n            images = arrayListOf(\n                Image(\n                    source = it,\n                    resolutions = arrayListOf()\n                )\n            ) as ArrayList<Image>?\n        )\n    }\n    return PostData(\n        id = id.removePrefix(\"t3_\"),\n        name = if (id.startsWith(\"t3_\")) id else \"t3_$id\",\n        title = title,\n        author = author,\n        subreddit = subreddit,\n        subredditNamePrefixed = subreddit?.let { \"r/$it\" },\n        createdUTC = createdUtc,\n        ups = ups?.toLong(),\n        thumbnail = thumbnail,\n        permalink = permalink,\n        url = url ?: previewImageUrl,\n        preview = preview,\n        saved = saved,\n        likes = likes,\n        selftext = selftext\n    )\n}\n\n/**\n * Convert a cached UserEntity to a UserAboutListing object\n */\nfun UserEntity.toUserAboutListing(): UserAboutListing {\n    return UserAboutListing(\n        // could be problematic in future but as of now we do not use kind\n        kind = \"\",\n        data = UserAbout(\n            name = name,\n            icon_img = iconUrl,\n            snoovatar_img = snoovatarUrl\n        )\n    )\n}\n\nfun SubredditItem.toSubredditEntity(isSubscribed: Boolean): SubredditEntity {\n    val d = this.data\n    return SubredditEntity(\n        id = d.url,\n        name = d.displayName,\n        title = d.title,\n        iconUrl = d.iconUrl,\n        subscribers = d.subscribers,\n        isNsfw = d.over18 == true,\n        isSubscribed = isSubscribed\n    )\n}"
  },
  {
    "path": "app/src/main/res/drawable/async_image_placeholder.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"203dp\"\n    android:height=\"100dp\"\n    android:viewportWidth=\"203\"\n    android:viewportHeight=\"100\">\n  <path\n      android:pathData=\"M10,0L193,0A10,10 0,0 1,203 10L203,90A10,10 0,0 1,193 100L10,100A10,10 0,0 1,0 90L0,10A10,10 0,0 1,10 0z\"\n      android:fillColor=\"#E8E7EFFF\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/generic_avatar.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- Fallback avatar that reuses ic_person vector/icon -->\n    <item>\n        <bitmap android:src=\"@drawable/ic_person\" android:gravity=\"center\" />\n    </item>\n</layer-list>\n\n"
  },
  {
    "path": "app/src/main/res/drawable/generic_community.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- Fallback community icon that reuses ic_community -->\n    <item>\n        <bitmap android:src=\"@drawable/ic_community\" android:gravity=\"center\" />\n    </item>\n</layer-list>\n\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_angry.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q133,0 226.5,-93.5T800,480q0,-133 -93.5,-226.5T480,160q-133,0 -226.5,93.5T160,480q0,133 93.5,226.5T480,800ZM540,436 L560,424q2,24 19,40t41,16q25,0 42.5,-17.5T680,420q0,-15 -7,-28.5T654,370l26,-15 -20,-35 -140,80 20,36ZM420,436 L440,400 300,320 280,355 306,370q-12,8 -19,21.5t-7,28.5q0,25 17.5,42.5T340,480q24,0 41,-16t19,-40l20,12ZM480,520q-71,0 -125,45.5T279,680h402q-22,-69 -76,-114.5T480,520ZM480,480Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_arrow_up.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M440,800v-487L216,537l-56,-57 320,-320 320,320 -56,57 -224,-224v487h-80Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_back.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:autoMirrored=\"true\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z\"/>\n    \n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_bookmark.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M5,21V5C5,4.45 5.196,3.979 5.588,3.588C5.979,3.196 6.45,3 7,3H17C17.55,3 18.021,3.196 18.413,3.588C18.804,3.979 19,4.45 19,5V21L12,18L5,21ZM7,17.95L12,15.8L17,17.95V5H7V17.95Z\"\n      android:fillColor=\"#374955\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_bookmark_filled.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z\"/>\n    \n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_browse.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M120,520v-320q0,-33 23.5,-56.5T200,120h240v400L120,520ZM360,440ZM520,120h240q33,0 56.5,23.5T840,200v160L520,360v-240ZM520,840v-400h320v320q0,33 -23.5,56.5T760,840L520,840ZM120,600h320v240L200,840q-33,0 -56.5,-23.5T120,760v-160ZM360,680ZM600,280ZM600,520ZM200,440h160v-240L200,200v240ZM600,280h160v-80L600,200v80ZM600,520v240h160v-240L600,520ZM200,680v80h160v-80L200,680Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_calendar_day.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M200,880q-33,0 -56.5,-23.5T120,800v-560q0,-33 23.5,-56.5T200,160h40v-80h80v80h320v-80h80v80h40q33,0 56.5,23.5T840,240v560q0,33 -23.5,56.5T760,880L200,880ZM200,800h560v-400L200,400v400ZM200,320h560v-80L200,240v80ZM200,320v-80,80Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_calendar_month.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M200,880q-33,0 -56.5,-23.5T120,800v-560q0,-33 23.5,-56.5T200,160h40v-80h80v80h320v-80h80v80h40q33,0 56.5,23.5T840,240v560q0,33 -23.5,56.5T760,880L200,880ZM200,800h560v-400L200,400v400ZM200,320h560v-80L200,240v80ZM200,320v-80,80ZM480,560q-17,0 -28.5,-11.5T440,520q0,-17 11.5,-28.5T480,480q17,0 28.5,11.5T520,520q0,17 -11.5,28.5T480,560ZM320,560q-17,0 -28.5,-11.5T280,520q0,-17 11.5,-28.5T320,480q17,0 28.5,11.5T360,520q0,17 -11.5,28.5T320,560ZM640,560q-17,0 -28.5,-11.5T600,520q0,-17 11.5,-28.5T640,480q17,0 28.5,11.5T680,520q0,17 -11.5,28.5T640,560ZM480,720q-17,0 -28.5,-11.5T440,680q0,-17 11.5,-28.5T480,640q17,0 28.5,11.5T520,680q0,17 -11.5,28.5T480,720ZM320,720q-17,0 -28.5,-11.5T280,680q0,-17 11.5,-28.5T320,640q17,0 28.5,11.5T360,680q0,17 -11.5,28.5T320,720ZM640,720q-17,0 -28.5,-11.5T600,680q0,-17 11.5,-28.5T640,640q17,0 28.5,11.5T680,680q0,17 -11.5,28.5T640,720Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_check.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M382,720 L154,492l57,-57 171,171 367,-367 57,57 -424,424Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_close.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"m256,760 l-56,-56 224,-224 -224,-224 56,-56 224,224 224,-224 56,56 -224,224 224,224 -56,56 -224,-224 -224,224Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_community.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M6,20C5.167,20 4.458,19.708 3.875,19.125C3.292,18.542 3,17.833 3,17C3,16.167 3.292,15.458 3.875,14.875C4.458,14.292 5.167,14 6,14C6.833,14 7.542,14.292 8.125,14.875C8.708,15.458 9,16.167 9,17C9,17.833 8.708,18.542 8.125,19.125C7.542,19.708 6.833,20 6,20ZM18,20C17.167,20 16.458,19.708 15.875,19.125C15.292,18.542 15,17.833 15,17C15,16.167 15.292,15.458 15.875,14.875C16.458,14.292 17.167,14 18,14C18.833,14 19.542,14.292 20.125,14.875C20.708,15.458 21,16.167 21,17C21,17.833 20.708,18.542 20.125,19.125C19.542,19.708 18.833,20 18,20ZM12,10C11.167,10 10.458,9.708 9.875,9.125C9.292,8.542 9,7.833 9,7C9,6.167 9.292,5.458 9.875,4.875C10.458,4.292 11.167,4 12,4C12.833,4 13.542,4.292 14.125,4.875C14.708,5.458 15,6.167 15,7C15,7.833 14.708,8.542 14.125,9.125C13.542,9.708 12.833,10 12,10Z\"\n      android:fillColor=\"#41484D\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_copy.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M360,720q-33,0 -56.5,-23.5T280,640v-480q0,-33 23.5,-56.5T360,80h360q33,0 56.5,23.5T800,160v480q0,33 -23.5,56.5T720,720L360,720ZM360,640h360v-480L360,160v480ZM200,880q-33,0 -56.5,-23.5T120,800v-560h80v560h440v80L200,880ZM360,640v-480,480Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_downvote.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M12,22L5,15L6.4,13.575L11,18.175V11H13V18.175L17.6,13.6L19,15L12,22ZM11,9V6H13V9H11ZM11,4V2H13V4H11Z\"\n      android:fillColor=\"#1F1F1F\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_filter.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M520,360v-80h120v-160h80v160h120v80L520,360ZM640,840v-400h80v400h-80ZM240,840v-160L120,680v-80h320v80L320,680v160h-80ZM240,520v-400h80v400h-80Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_fire.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M240,560q0,52 21,98.5t60,81.5q-1,-5 -1,-9v-9q0,-32 12,-60t35,-51l113,-111 113,111q23,23 35,51t12,60v9q0,4 -1,9 39,-35 60,-81.5t21,-98.5q0,-50 -18.5,-94.5T648,386q-20,13 -42,19.5t-45,6.5q-62,0 -107.5,-41T401,270q-39,33 -69,68.5t-50.5,72Q261,447 250.5,485T240,560ZM480,612 L423,668q-11,11 -17,25t-6,29q0,32 23.5,55t56.5,23q33,0 56.5,-23t23.5,-55q0,-16 -6,-29.5T537,668l-57,-56ZM480,120v132q0,34 23.5,57t57.5,23q18,0 33.5,-7.5T622,302l18,-22q74,42 117,117t43,163q0,134 -93,227T480,880q-134,0 -227,-93t-93,-227q0,-129 86.5,-245T480,120Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_flag.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M14,6l-1,-2L5,4v17h2v-7h5l1,2h7L20,6h-6zM18,14h-4l-1,-2L7,12L7,6h5l1,2h5v6z\"/>\n    \n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_forum.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M880,880 L720,720L320,720q-33,0 -56.5,-23.5T240,640v-40h440q33,0 56.5,-23.5T760,520v-280h40q33,0 56.5,23.5T880,320v560ZM160,487l47,-47h393v-280L160,160v327ZM80,680v-520q0,-33 23.5,-56.5T160,80h440q33,0 56.5,23.5T680,160v280q0,33 -23.5,56.5T600,520L240,520L80,680ZM160,440v-280,280Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_forward.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:autoMirrored=\"true\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z\"/>\n    \n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_help.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M11.95,18C12.3,18 12.596,17.879 12.837,17.638C13.079,17.396 13.2,17.1 13.2,16.75C13.2,16.4 13.079,16.104 12.837,15.863C12.596,15.621 12.3,15.5 11.95,15.5C11.6,15.5 11.304,15.621 11.063,15.863C10.821,16.104 10.7,16.4 10.7,16.75C10.7,17.1 10.821,17.396 11.063,17.638C11.304,17.879 11.6,18 11.95,18ZM11.05,14.15H12.9C12.9,13.6 12.962,13.167 13.087,12.85C13.212,12.533 13.567,12.1 14.15,11.55C14.583,11.117 14.925,10.704 15.175,10.313C15.425,9.921 15.55,9.45 15.55,8.9C15.55,7.967 15.208,7.25 14.525,6.75C13.842,6.25 13.033,6 12.1,6C11.15,6 10.379,6.25 9.788,6.75C9.196,7.25 8.783,7.85 8.55,8.55L10.2,9.2C10.283,8.9 10.471,8.575 10.762,8.225C11.054,7.875 11.5,7.7 12.1,7.7C12.633,7.7 13.033,7.846 13.3,8.137C13.567,8.429 13.7,8.75 13.7,9.1C13.7,9.433 13.6,9.746 13.4,10.038C13.2,10.329 12.95,10.6 12.65,10.85C11.917,11.5 11.467,11.992 11.3,12.325C11.133,12.658 11.05,13.267 11.05,14.15ZM12,22C10.617,22 9.317,21.737 8.1,21.212C6.883,20.688 5.825,19.975 4.925,19.075C4.025,18.175 3.313,17.117 2.787,15.9C2.263,14.683 2,13.383 2,12C2,10.617 2.263,9.317 2.787,8.1C3.313,6.883 4.025,5.825 4.925,4.925C5.825,4.025 6.883,3.313 8.1,2.787C9.317,2.263 10.617,2 12,2C13.383,2 14.683,2.263 15.9,2.787C17.117,3.313 18.175,4.025 19.075,4.925C19.975,5.825 20.688,6.883 21.212,8.1C21.737,9.317 22,10.617 22,12C22,13.383 21.737,14.683 21.212,15.9C20.688,17.117 19.975,18.175 19.075,19.075C18.175,19.975 17.117,20.688 15.9,21.212C14.683,21.737 13.383,22 12,22ZM12,20C14.233,20 16.125,19.225 17.675,17.675C19.225,16.125 20,14.233 20,12C20,9.767 19.225,7.875 17.675,6.325C16.125,4.775 14.233,4 12,4C9.767,4 7.875,4.775 6.325,6.325C4.775,7.875 4,9.767 4,12C4,14.233 4.775,16.125 6.325,17.675C7.875,19.225 9.767,20 12,20Z\"\n      android:fillColor=\"#1F1F1F\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_history.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M480,840q-138,0 -240.5,-91.5T122,520h82q14,104 92.5,172T480,760q117,0 198.5,-81.5T760,480q0,-117 -81.5,-198.5T480,200q-69,0 -129,32t-101,88h110v80L120,400v-240h80v94q51,-64 124.5,-99T480,120q75,0 140.5,28.5t114,77q48.5,48.5 77,114T840,480q0,75 -28.5,140.5t-77,114q-48.5,48.5 -114,77T480,840ZM592,648L440,496v-216h80v184l128,128 -56,56Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_hourglass.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M320,800h320v-120q0,-66 -47,-113t-113,-47q-66,0 -113,47t-47,113v120ZM480,440q66,0 113,-47t47,-113v-120L320,160v120q0,66 47,113t113,47ZM160,880v-80h80v-120q0,-61 28.5,-114.5T348,480q-51,-32 -79.5,-85.5T240,280v-120h-80v-80h640v80h-80v120q0,61 -28.5,114.5T612,480q51,32 79.5,85.5T720,680v120h80v80L160,880Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"350\"\n    android:viewportHeight=\"350\">\n  <path\n      android:pathData=\"M0,0h350v350h-350z\"\n      android:fillColor=\"#E0E2D5\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"122\"\n    android:viewportHeight=\"269\">\n  <group android:scaleX=\"0.22591008\"\n      android:scaleY=\"0.49811321\"\n      android:translateX=\"47.219486\"\n      android:translateY=\"67.50378\">\n    <path\n        android:pathData=\"M116.45,91.61C116.73,91.87 78.9,102.57 78.9,114.94L59.53,114.89L40.17,114.84C40.17,102.47 4.44,92.08 4.44,91.71C4.44,91.33 40.17,91.61 40.17,91.61C38.82,88.43 18.08,78.35 11.83,51.14C5.69,24.38 7.29,19.91 8.25,21.16C16.5,39.4 47.89,79.56 48.8,78.45C49.91,75.12 59.89,2.25 59.89,5.25C59.89,5.25 70.98,74.02 70.98,78.45L71.02,78.52C72.25,78.72 102.74,39.13 110.82,21.26C111.77,20.01 112.39,24.26 107.23,51.24C102.07,78.21 82.08,87.32 78.9,91.7C81.07,91.67 116.2,91.36 116.45,91.61Z\"\n        android:fillColor=\"#2D694B\"/>\n    <path\n        android:pathData=\"M40.17,91.61C40.17,91.61 4.44,91.33 4.44,91.71C4.44,92.08 40.17,102.47 40.17,114.84L59.53,114.89M40.17,91.61C38.82,88.43 18.08,78.35 11.83,51.14C5.69,24.38 7.29,19.91 8.25,21.16C16.5,39.4 47.89,79.56 48.8,78.45M40.17,91.61L59.53,114.89M40.17,91.61L48.8,78.45M48.8,78.45C49.91,75.12 59.89,2.25 59.89,5.25C59.89,5.25 70.98,74.02 70.98,78.45M48.8,78.45L59.89,89.54L70.98,78.45M70.98,78.45C70.98,80.37 102.57,39.5 110.82,21.26C111.77,20.01 112.39,24.26 107.23,51.24C102.07,78.22 82.07,87.32 78.9,91.71M70.98,78.45L78.9,91.71M78.9,91.71C76.53,91.71 116.18,91.35 116.45,91.61C116.73,91.87 78.9,102.57 78.9,114.94L59.53,114.89M78.9,91.71L59.57,114.84L59.53,114.89\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#00000000\"\n        android:strokeColor=\"#1A3A1B\"/>\n    <path\n        android:pathData=\"M83.2,114.96H38.01C19.2,114.96 3.94,130.9 3.94,150.56V228.5C3.94,248.16 19.2,264.1 38.01,264.1H83.2C102.02,264.1 117.27,248.16 117.27,228.5V150.56C117.27,130.9 102.02,114.96 83.2,114.96Z\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#ACB373\"\n        android:strokeColor=\"#5C5C39\"/>\n    <path\n        android:pathData=\"M32.64,262.97L116.91,146.43\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#00000000\"\n        android:strokeColor=\"#5C5C39\"/>\n    <path\n        android:pathData=\"M61.33,263.35L117.27,185.4\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#00000000\"\n        android:strokeColor=\"#5C5C39\"/>\n    <path\n        android:pathData=\"M90.37,261.74L97.6,239.56L117.63,222.4\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#00000000\"\n        android:strokeColor=\"#5C5C39\"/>\n    <path\n        android:pathData=\"M4.44,153.57L24.4,136.42L32.77,115.35\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#00000000\"\n        android:strokeColor=\"#5C5C39\"/>\n    <path\n        android:pathData=\"M13.27,250.61L105.79,125.07\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#00000000\"\n        android:strokeColor=\"#5C5C39\"/>\n    <path\n        android:pathData=\"M4.66,224.75L84.99,114.58\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#00000000\"\n        android:strokeColor=\"#5C5C39\"/>\n    <path\n        android:pathData=\"M3.94,188.4L58.46,114.21\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#00000000\"\n        android:strokeColor=\"#5C5C39\"/>\n    <path\n        android:pathData=\"M88.94,262.97L4.66,146.43\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#00000000\"\n        android:strokeColor=\"#5C5C39\"/>\n    <path\n        android:pathData=\"M60.25,263.35L4.3,185.4\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#00000000\"\n        android:strokeColor=\"#5C5C39\"/>\n    <path\n        android:pathData=\"M31.2,263.72L22.18,239.56L3.94,224.38\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#00000000\"\n        android:strokeColor=\"#5C5C39\"/>\n    <path\n        android:pathData=\"M117.27,154.3L96.49,137.53L90.02,114.96\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#00000000\"\n        android:strokeColor=\"#5C5C39\"/>\n    <path\n        android:pathData=\"M108.31,250.61L15.78,125.07\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#00000000\"\n        android:strokeColor=\"#5C5C39\"/>\n    <path\n        android:pathData=\"M116.91,224.75L36.58,114.58\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#00000000\"\n        android:strokeColor=\"#5C5C39\"/>\n    <path\n        android:pathData=\"M117.63,188.4L63.12,114.21\"\n        android:strokeWidth=\"5\"\n        android:fillColor=\"#00000000\"\n        android:strokeColor=\"#5C5C39\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_monochrome.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"96dp\"\n    android:height=\"96dp\"\n    android:viewportWidth=\"96\"\n    android:viewportHeight=\"96\">\n  <path\n      android:pathData=\"M51.945,44.686H44.163C40.923,44.686 38.296,47.431 38.296,50.817V64.239C38.296,67.625 40.923,70.37 44.163,70.37H51.945C55.185,70.37 57.812,67.625 57.812,64.239V50.817C57.812,47.431 55.185,44.686 51.945,44.686Z\"\n      android:fillColor=\"#36693E\"/>\n  <path\n      android:pathData=\"M57.671,40.525C57.718,40.57 51.203,42.413 51.203,44.543L47.868,44.534L44.533,44.526C44.533,42.396 38.38,40.606 38.38,40.542C38.38,40.477 44.533,40.525 44.533,40.525C44.301,39.978 40.73,38.241 39.654,33.555C38.596,28.948 38.872,28.178 39.037,28.393C40.457,31.533 45.863,38.45 46.02,38.259C46.211,37.686 47.93,25.137 47.93,25.653C47.93,25.653 49.84,37.495 49.84,38.259L49.847,38.27C50.059,38.306 55.308,31.487 56.7,28.41C56.865,28.195 56.972,28.926 56.083,33.572C55.194,38.217 51.752,39.786 51.204,40.541C51.578,40.535 57.627,40.482 57.671,40.525Z\"\n      android:fillColor=\"#36693E\"/>\n  <path\n      android:pathData=\"M44.533,40.525C44.533,40.525 38.38,40.477 38.38,40.542C38.38,40.606 44.533,42.396 44.533,44.526L47.868,44.534M44.533,40.525C44.301,39.978 40.73,38.241 39.654,33.555C38.596,28.948 38.872,28.178 39.037,28.393C40.457,31.533 45.863,38.45 46.02,38.259M44.533,40.525L47.868,44.534M44.533,40.525L46.02,38.259M46.02,38.259C46.211,37.686 47.93,25.137 47.93,25.653C47.93,25.653 49.84,37.495 49.84,38.259M46.02,38.259L47.93,40.169L49.84,38.259M49.84,38.259C49.84,38.589 55.28,31.55 56.7,28.41C56.865,28.195 56.972,28.926 56.083,33.572C55.193,38.219 51.75,39.787 51.203,40.542M49.84,38.259L51.203,40.542M51.203,40.542C50.795,40.542 57.625,40.48 57.671,40.525C57.718,40.57 51.203,42.413 51.203,44.543L47.868,44.534M51.203,40.542L47.876,44.526L47.868,44.534\"\n      android:strokeWidth=\"1.11111\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#36693E\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_menu.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z\"/>\n    \n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_more_vert.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z\"/>\n    \n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_open_external.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M5,21C4.45,21 3.979,20.804 3.588,20.413C3.196,20.021 3,19.55 3,19V5C3,4.45 3.196,3.979 3.588,3.588C3.979,3.196 4.45,3 5,3H12V5H5V19H19V12H21V19C21,19.55 20.804,20.021 20.413,20.413C20.021,20.804 19.55,21 19,21H5ZM9.7,15.7L8.3,14.3L17.6,5H14V3H21V10H19V6.4L9.7,15.7Z\"\n      android:fillColor=\"#41484D\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_person.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M12,12C10.9,12 9.958,11.608 9.175,10.825C8.392,10.042 8,9.1 8,8C8,6.9 8.392,5.958 9.175,5.175C9.958,4.392 10.9,4 12,4C13.1,4 14.042,4.392 14.825,5.175C15.608,5.958 16,6.9 16,8C16,9.1 15.608,10.042 14.825,10.825C14.042,11.608 13.1,12 12,12ZM4,20V17.2C4,16.633 4.146,16.112 4.438,15.637C4.729,15.163 5.117,14.8 5.6,14.55C6.633,14.033 7.683,13.646 8.75,13.387C9.817,13.129 10.9,13 12,13C13.1,13 14.183,13.129 15.25,13.387C16.317,13.646 17.367,14.033 18.4,14.55C18.883,14.8 19.271,15.163 19.563,15.637C19.854,16.112 20,16.633 20,17.2V20H4ZM6,18H18V17.2C18,17.017 17.954,16.85 17.862,16.7C17.771,16.55 17.65,16.433 17.5,16.35C16.6,15.9 15.692,15.563 14.775,15.337C13.858,15.113 12.933,15 12,15C11.067,15 10.142,15.113 9.225,15.337C8.308,15.563 7.4,15.9 6.5,16.35C6.35,16.433 6.229,16.55 6.137,16.7C6.046,16.85 6,17.017 6,17.2V18ZM12,10C12.55,10 13.021,9.804 13.413,9.413C13.804,9.021 14,8.55 14,8C14,7.45 13.804,6.979 13.413,6.588C13.021,6.196 12.55,6 12,6C11.45,6 10.979,6.196 10.587,6.588C10.196,6.979 10,7.45 10,8C10,8.55 10.196,9.021 10.587,9.413C10.979,9.804 11.45,10 12,10Z\"\n      android:fillColor=\"#41484D\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_pineapple_logo.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"95dp\"\n    android:height=\"208dp\"\n    android:viewportWidth=\"95\"\n    android:viewportHeight=\"208\">\n  <path\n      android:pathData=\"M64.48,89.09H29.46C14.88,89.09 3.06,101.44 3.06,116.68V177.08C3.06,192.31 14.88,204.66 29.46,204.66H64.48C79.06,204.66 90.88,192.31 90.88,177.08V116.68C90.88,101.44 79.06,89.09 64.48,89.09Z\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n  <path\n      android:pathData=\"M31.13,70.36C31.13,70.36 3.44,70.15 3.44,70.44C3.44,70.73 31.13,78.78 31.13,88.37L46.14,88.4M31.13,70.36C30.08,67.9 14.01,60.08 9.17,39C4.41,18.27 5.65,14.8 6.39,15.77C12.78,29.9 37.11,61.02 37.82,60.17M31.13,70.36L46.14,88.4M31.13,70.36L37.82,60.17M37.82,60.17C38.68,57.59 46.41,1.11 46.41,3.44C46.41,3.44 55.01,56.73 55.01,60.17M37.82,60.17L46.41,68.76L55.01,60.17M55.01,60.17C55.01,61.65 79.49,29.98 85.88,15.84C86.62,14.88 87.1,18.17 83.1,39.08C79.1,59.98 63.6,67.04 61.14,70.44M55.01,60.17L61.14,70.44M61.14,70.44C59.31,70.44 90.04,70.16 90.25,70.36C90.46,70.56 61.14,78.86 61.14,88.44L46.14,88.4M61.14,70.44L46.17,88.37L46.14,88.4\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n  <path\n      android:pathData=\"M25.29,203.79L90.6,113.48\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n  <path\n      android:pathData=\"M47.52,204.08L90.88,143.68\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n  <path\n      android:pathData=\"M70.04,202.84L75.64,185.65L91.16,172.35\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n  <path\n      android:pathData=\"M3.44,119.01L18.91,105.72L25.39,89.39\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n  <path\n      android:pathData=\"M10.28,194.21L81.99,96.93\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n  <path\n      android:pathData=\"M3.61,174.17L65.87,88.8\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n  <path\n      android:pathData=\"M3.06,146.01L45.3,88.51\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n  <path\n      android:pathData=\"M68.93,203.79L3.61,113.48\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n  <path\n      android:pathData=\"M46.69,204.08L3.34,143.68\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n  <path\n      android:pathData=\"M24.18,204.37L17.19,185.65L3.06,173.88\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n  <path\n      android:pathData=\"M90.88,119.58L74.78,106.58L69.76,89.09\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n  <path\n      android:pathData=\"M83.93,194.21L12.23,96.93\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n  <path\n      android:pathData=\"M90.6,174.17L28.35,88.8\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n  <path\n      android:pathData=\"M91.16,146.01L48.91,88.51\"\n      android:strokeWidth=\"5\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#31628D\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_plus.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M440,840v-320L120,520v-80h320v-320h80v320h320v80L520,520v320h-80Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_reddit.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M14,3C12.301,3 11,4.414 11,6V8.031C8.883,8.176 6.977,8.785 5.438,9.719C4.879,9.281 4.219,9.031 3.563,9.031C2.836,9.031 2.129,9.309 1.625,9.875C0.668,11.07 0.754,13.023 2.094,14.031C2.031,14.348 2,14.668 2,15C2,17.055 3.242,18.844 5.063,20.063C6.883,21.281 9.32,22 12,22C14.68,22 17.117,21.281 18.938,20.063C20.758,18.844 22,17.055 22,15C22,14.668 21.938,14.348 21.875,14.031C23.238,12.945 23.379,11.004 22.375,9.875C21.871,9.309 21.164,9.031 20.438,9.031C19.781,9.031 19.121,9.281 18.563,9.719C17.023,8.785 15.117,8.176 13,8.031V6C13,5.387 13.301,5 14,5C14.32,5 14.773,5.172 15.531,5.438C16.203,5.672 17.094,5.914 18.25,5.969C18.59,6.586 19.25,7 20,7C21.102,7 22,6.102 22,5C22,3.898 21.102,3 20,3C19.273,3 18.633,3.383 18.281,3.969C17.395,3.926 16.77,3.766 16.188,3.563C15.519,3.328 14.879,3 14,3ZM20,4C20.602,4 21,4.398 21,5C21,5.602 20.602,6 20,6C19.398,6 19,5.602 19,5C19,4.398 19.398,4 20,4ZM12,10C14.32,10 16.383,10.637 17.813,11.594C19.242,12.551 20,13.754 20,15C20,16.246 19.242,17.449 17.813,18.406C16.383,19.363 14.32,20 12,20C9.68,20 7.617,19.363 6.188,18.406C4.758,17.449 4,16.246 4,15C4,13.754 4.758,12.551 6.188,11.594C7.617,10.637 9.68,10 12,10ZM3.594,10.031C3.926,10.031 4.277,10.102 4.594,10.281C3.629,11.023 2.879,11.938 2.438,12.969C1.855,12.281 1.867,11.191 2.375,10.531C2.672,10.195 3.121,10.031 3.594,10.031ZM20.406,10.031C20.879,10.031 21.328,10.195 21.625,10.531C22.117,11.086 22.156,12.176 21.563,12.938C21.121,11.914 20.363,11.019 19.406,10.281C19.723,10.102 20.074,10.031 20.406,10.031ZM9,12C8.172,12 7.5,12.672 7.5,13.5C7.5,14.328 8.172,15 9,15C9.828,15 10.5,14.328 10.5,13.5C10.5,12.672 9.828,12 9,12ZM15,12C14.172,12 13.5,12.672 13.5,13.5C13.5,14.328 14.172,15 15,15C15.828,15 16.5,14.328 16.5,13.5C16.5,12.672 15.828,12 15,12ZM16.094,16.406C15.195,17.207 13.699,17.688 12,17.688C10.301,17.688 8.805,17.199 7.906,16.5C8.406,17.801 10,19 12,19C14,19 15.594,17.805 16.094,16.406Z\"\n      android:fillColor=\"#000000\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_search.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M784,840 L532,588q-30,24 -69,38t-83,14q-109,0 -184.5,-75.5T120,380q0,-109 75.5,-184.5T380,120q109,0 184.5,75.5T640,380q0,44 -14,83t-38,69l252,252 -56,56ZM380,560q75,0 127.5,-52.5T560,380q0,-75 -52.5,-127.5T380,200q-75,0 -127.5,52.5T200,380q0,75 52.5,127.5T380,560Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_settings.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"m370,880 l-16,-128q-13,-5 -24.5,-12T307,725l-119,50L78,585l103,-78q-1,-7 -1,-13.5v-27q0,-6.5 1,-13.5L78,375l110,-190 119,50q11,-8 23,-15t24,-12l16,-128h220l16,128q13,5 24.5,12t22.5,15l119,-50 110,190 -103,78q1,7 1,13.5v27q0,6.5 -2,13.5l103,78 -110,190 -118,-50q-11,8 -23,15t-24,12L590,880L370,880ZM440,800h79l14,-106q31,-8 57.5,-23.5T639,633l99,41 39,-68 -86,-65q5,-14 7,-29.5t2,-31.5q0,-16 -2,-31.5t-7,-29.5l86,-65 -39,-68 -99,42q-22,-23 -48.5,-38.5T533,266l-13,-106h-79l-14,106q-31,8 -57.5,23.5T321,327l-99,-41 -39,68 86,64q-5,15 -7,30t-2,32q0,16 2,31t7,30l-86,65 39,68 99,-42q22,23 48.5,38.5T427,694l13,106ZM482,620q58,0 99,-41t41,-99q0,-58 -41,-99t-99,-41q-59,0 -99.5,41T342,480q0,58 40.5,99t99.5,41ZM480,480Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_share.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M18,22C17.167,22 16.458,21.708 15.875,21.125C15.292,20.542 15,19.833 15,19C15,18.883 15.008,18.763 15.025,18.638C15.042,18.513 15.067,18.4 15.1,18.3L8.05,14.2C7.767,14.45 7.45,14.646 7.1,14.788C6.75,14.929 6.383,15 6,15C5.167,15 4.458,14.708 3.875,14.125C3.292,13.542 3,12.833 3,12C3,11.167 3.292,10.458 3.875,9.875C4.458,9.292 5.167,9 6,9C6.383,9 6.75,9.071 7.1,9.212C7.45,9.354 7.767,9.55 8.05,9.8L15.1,5.7C15.067,5.6 15.042,5.488 15.025,5.363C15.008,5.238 15,5.117 15,5C15,4.167 15.292,3.458 15.875,2.875C16.458,2.292 17.167,2 18,2C18.833,2 19.542,2.292 20.125,2.875C20.708,3.458 21,4.167 21,5C21,5.833 20.708,6.542 20.125,7.125C19.542,7.708 18.833,8 18,8C17.617,8 17.25,7.929 16.9,7.787C16.55,7.646 16.233,7.45 15.95,7.2L8.9,11.3C8.933,11.4 8.958,11.512 8.975,11.637C8.992,11.762 9,11.883 9,12C9,12.117 8.992,12.238 8.975,12.363C8.958,12.488 8.933,12.6 8.9,12.7L15.95,16.8C16.233,16.55 16.55,16.354 16.9,16.212C17.25,16.071 17.617,16 18,16C18.833,16 19.542,16.292 20.125,16.875C20.708,17.458 21,18.167 21,19C21,19.833 20.708,20.542 20.125,21.125C19.542,21.708 18.833,22 18,22ZM18,6C18.283,6 18.521,5.904 18.712,5.713C18.904,5.521 19,5.283 19,5C19,4.717 18.904,4.479 18.712,4.287C18.521,4.096 18.283,4 18,4C17.717,4 17.479,4.096 17.288,4.287C17.096,4.479 17,4.717 17,5C17,5.283 17.096,5.521 17.288,5.713C17.479,5.904 17.717,6 18,6ZM6,13C6.283,13 6.521,12.904 6.713,12.712C6.904,12.521 7,12.283 7,12C7,11.717 6.904,11.479 6.713,11.288C6.521,11.096 6.283,11 6,11C5.717,11 5.479,11.096 5.287,11.288C5.096,11.479 5,11.717 5,12C5,12.283 5.096,12.521 5.287,12.712C5.479,12.904 5.717,13 6,13ZM18,20C18.283,20 18.521,19.904 18.712,19.712C18.904,19.521 19,19.283 19,19C19,18.717 18.904,18.479 18.712,18.288C18.521,18.096 18.283,18 18,18C17.717,18 17.479,18.096 17.288,18.288C17.096,18.479 17,18.717 17,19C17,19.283 17.096,19.521 17.288,19.712C17.479,19.904 17.717,20 18,20Z\"\n      android:fillColor=\"#0E1D2A\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_shine.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M852,748 L732,628l56,-56 120,120 -56,56ZM708,268l-56,-56 120,-120 56,56 -120,120ZM252,268L132,148l56,-56 120,120 -56,56ZM108,748l-56,-56 120,-120 56,56 -120,120ZM354,673 L480,597 606,674 573,530 684,434 538,421 480,285 422,420 276,433 387,530 354,673ZM233,840l65,-281L80,370l288,-25 112,-265 112,265 288,25 -218,189 65,281 -247,-149 -247,149ZM480,479Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_trending.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"m136,720 l-56,-56 296,-298 160,160 208,-206L640,320v-80h240v240h-80v-104L536,640 376,480 136,720Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_upvote.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M11,5.825L6.4,10.4L5,9L12,2L19,9L17.6,10.425L13,5.825V13H11V5.825ZM11,18V15H13V18H11ZM11,22V20H13V22H11Z\"\n      android:fillColor=\"#1F1F1F\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_week.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:pathData=\"M160,720h160v-480L160,240v480ZM400,720h160v-480L400,240v480ZM640,720h160v-480L640,240v480ZM160,800q-33,0 -56.5,-23.5T80,720v-480q0,-33 23.5,-56.5T160,160h640q33,0 56.5,23.5T880,240v480q0,33 -23.5,56.5T800,800L160,800Z\"\n      android:fillColor=\"#1f1f1f\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-night/async_image_placeholder.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"203dp\"\n    android:height=\"100dp\"\n    android:viewportWidth=\"203\"\n    android:viewportHeight=\"100\">\n  <path\n      android:pathData=\"M10,0L193,0A10,10 0,0 1,203 10L203,90A10,10 0,0 1,193 100L10,100A10,10 0,0 1,0 90L0,10A10,10 0,0 1,10 0z\"\n      android:fillColor=\"#9A3A3A42\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-night-v34/async_image_placeholder.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"203dp\"\n    android:height=\"100dp\"\n    android:viewportWidth=\"203\"\n    android:viewportHeight=\"100\">\n  <path\n      android:pathData=\"M10,0L193,0A10,10 0,0 1,203 10L203,90A10,10 0,0 1,193 100L10,100A10,10 0,0 1,0 90L0,10A10,10 0,0 1,10 0z\"\n      android:fillColor=\"@android:color/system_surface_container_high_dark\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-night-v34/generic_avatar.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"40dp\"\n    android:height=\"40dp\"\n    android:viewportWidth=\"40\"\n    android:viewportHeight=\"40\">\n  <path\n      android:pathData=\"M20,0L20,0A20,20 0,0 1,40 20L40,20A20,20 0,0 1,20 40L20,40A20,20 0,0 1,0 20L0,20A20,20 0,0 1,20 0z\"\n      android:fillColor=\"@android:color/system_primary_container_dark\"/>\n  <path\n      android:pathData=\"M26,16C26,19.314 23.314,22 20,22C16.687,22 14,19.314 14,16C14,12.686 16.687,10 20,10C23.314,10 26,12.686 26,16ZM24,16C24,18.209 22.209,20 20,20C17.791,20 16,18.209 16,16C16,13.791 17.791,12 20,12C22.209,12 24,13.791 24,16Z\"\n      android:fillColor=\"@android:color/system_on_primary_container_dark\"\n      android:fillType=\"evenOdd\"/>\n  <path\n      android:pathData=\"M20,25C13.526,25 8.01,28.828 5.908,34.192C6.42,34.7 6.959,35.181 7.524,35.632C9.088,30.708 13.997,27 20,27C26.003,27 30.912,30.708 32.477,35.632C33.041,35.181 33.58,34.7 34.092,34.192C31.991,28.828 26.475,25 20,25Z\"\n      android:fillColor=\"@android:color/system_on_primary_container_dark\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-night-v34/generic_community.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"40dp\"\n    android:height=\"40dp\"\n    android:viewportWidth=\"40\"\n    android:viewportHeight=\"40\">\n  <path\n      android:pathData=\"M20,0L20,0A20,20 0,0 1,40 20L40,20A20,20 0,0 1,20 40L20,40A20,20 0,0 1,0 20L0,20A20,20 0,0 1,20 0z\"\n      android:fillColor=\"@android:color/system_primary_container_dark\"/>\n  <path\n      android:pathData=\"M14,28C13.167,28 12.458,27.708 11.875,27.125C11.292,26.542 11,25.833 11,25C11,24.167 11.292,23.458 11.875,22.875C12.458,22.292 13.167,22 14,22C14.833,22 15.542,22.292 16.125,22.875C16.708,23.458 17,24.167 17,25C17,25.833 16.708,26.542 16.125,27.125C15.542,27.708 14.833,28 14,28ZM26,28C25.167,28 24.458,27.708 23.875,27.125C23.292,26.542 23,25.833 23,25C23,24.167 23.292,23.458 23.875,22.875C24.458,22.292 25.167,22 26,22C26.833,22 27.542,22.292 28.125,22.875C28.708,23.458 29,24.167 29,25C29,25.833 28.708,26.542 28.125,27.125C27.542,27.708 26.833,28 26,28ZM20,18C19.167,18 18.458,17.708 17.875,17.125C17.292,16.542 17,15.833 17,15C17,14.167 17.292,13.458 17.875,12.875C18.458,12.292 19.167,12 20,12C20.833,12 21.542,12.292 22.125,12.875C22.708,13.458 23,14.167 23,15C23,15.833 22.708,16.542 22.125,17.125C21.542,17.708 20.833,18 20,18Z\"\n      android:fillColor=\"@android:color/system_on_primary_container_dark\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-v34/async_image_placeholder.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"203dp\"\n    android:height=\"100dp\"\n    android:viewportWidth=\"203\"\n    android:viewportHeight=\"100\">\n  <path\n      android:pathData=\"M10,0L193,0A10,10 0,0 1,203 10L203,90A10,10 0,0 1,193 100L10,100A10,10 0,0 1,0 90L0,10A10,10 0,0 1,10 0z\"\n      android:fillColor=\"@android:color/system_surface_container_high_light\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-v34/generic_avatar.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"40dp\"\n    android:height=\"40dp\"\n    android:viewportWidth=\"40\"\n    android:viewportHeight=\"40\">\n  <path\n      android:pathData=\"M20,0L20,0A20,20 0,0 1,40 20L40,20A20,20 0,0 1,20 40L20,40A20,20 0,0 1,0 20L0,20A20,20 0,0 1,20 0z\"\n      android:fillColor=\"@android:color/system_primary_container_light\"/>\n  <path\n      android:pathData=\"M26,16C26,19.314 23.314,22 20,22C16.687,22 14,19.314 14,16C14,12.686 16.687,10 20,10C23.314,10 26,12.686 26,16ZM24,16C24,18.209 22.209,20 20,20C17.791,20 16,18.209 16,16C16,13.791 17.791,12 20,12C22.209,12 24,13.791 24,16Z\"\n      android:fillColor=\"@android:color/system_on_primary_container_light\"\n      android:fillType=\"evenOdd\"/>\n  <path\n      android:pathData=\"M20,25C13.526,25 8.01,28.828 5.908,34.192C6.42,34.7 6.959,35.181 7.524,35.632C9.088,30.708 13.997,27 20,27C26.003,27 30.912,30.708 32.477,35.632C33.041,35.181 33.58,34.7 34.092,34.192C31.991,28.828 26.475,25 20,25Z\"\n      android:fillColor=\"@android:color/system_on_primary_container_light\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-v34/generic_community.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"40dp\"\n    android:height=\"40dp\"\n    android:viewportWidth=\"40\"\n    android:viewportHeight=\"40\">\n  <path\n      android:pathData=\"M20,0L20,0A20,20 0,0 1,40 20L40,20A20,20 0,0 1,20 40L20,40A20,20 0,0 1,0 20L0,20A20,20 0,0 1,20 0z\"\n      android:fillColor=\"@android:color/system_primary_container_light\"/>\n  <path\n      android:pathData=\"M14,28C13.167,28 12.458,27.708 11.875,27.125C11.292,26.542 11,25.833 11,25C11,24.167 11.292,23.458 11.875,22.875C12.458,22.292 13.167,22 14,22C14.833,22 15.542,22.292 16.125,22.875C16.708,23.458 17,24.167 17,25C17,25.833 16.708,26.542 16.125,27.125C15.542,27.708 14.833,28 14,28ZM26,28C25.167,28 24.458,27.708 23.875,27.125C23.292,26.542 23,25.833 23,25C23,24.167 23.292,23.458 23.875,22.875C24.458,22.292 25.167,22 26,22C26.833,22 27.542,22.292 28.125,22.875C28.708,23.458 29,24.167 29,25C29,25.833 28.708,26.542 28.125,27.125C27.542,27.708 26.833,28 26,28ZM20,18C19.167,18 18.458,17.708 17.875,17.125C17.292,16.542 17,15.833 17,15C17,14.167 17.292,13.458 17.875,12.875C18.458,12.292 19.167,12 20,12C20.833,12 21.542,12.292 22.125,12.875C22.708,13.458 23,14.167 23,15C23,15.833 22.708,16.542 22.125,17.125C21.542,17.708 20.833,18 20,18Z\"\n      android:fillColor=\"@android:color/system_on_primary_container_light\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@drawable/ic_launcher_monochrome\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@drawable/ic_launcher_monochrome\"/>\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"purple_200\">#FFBB86FC</color>\n    <color name=\"purple_500\">#FF6200EE</color>\n    <color name=\"purple_700\">#FF3700B3</color>\n    <color name=\"teal_200\">#FF03DAC5</color>\n    <color name=\"teal_700\">#FF018786</color>\n    <color name=\"black\">#FF000000</color>\n    <color name=\"white\">#FFFFFFFF</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n\n    <!-- Content descriptions -->\n    <string name=\"ic_pineapple_logo_cdesc\">Pineapple</string>\n    <string name=\"ic_back_cdesc\">Back</string>\n    <string name=\"ic_reddit_cdesc\">Reddit</string>\n    <string name=\"ic_help_cdesc\">Help</string>\n    <string name=\"ic_forward_cdesc\">Forward</string>\n    <string name=\"ic_filter_cdesc\">Filter</string>\n    <string name=\"ic_menu_cdesc\">Menu</string>\n    <string name=\"ic_more_vert_cdesc\">More</string>\n    <string name=\"ic_upvote_cdesc\">Upvote</string>\n    <string name=\"ic_downvote_cdesc\">Downvote</string>\n    <string name=\"ic_bookmark_cdesc\">Save</string>\n    <string name=\"ic_share_cdesc\">Share</string>\n    <string name=\"ic_flag_cdesc\">Flag</string>\n    <string name=\"ic_person_cdesc\">Person</string>\n    <string name=\"ic_open_external_cdesc\">Open</string>\n    <string name=\"ic_community_cdesc\">Community</string>\n    <string name=\"ic_check_cdesc\">Check</string>\n    <string name=\"ic_angry_cdesc\">Angry</string>\n    <string name=\"ic_arrow_up_cdesc\">Up</string>\n    <string name=\"ic_fire_cdesc\">Fire</string>\n    <string name=\"ic_trending_cdesc\">Trending</string>\n    <string name=\"ic_shine_cdesc\">Shine</string>\n    <string name=\"ic_week_cdesc\">Week</string>\n    <string name=\"ic_calendar_month_cdesc\">Month</string>\n    <string name=\"ic_calendar_day_cdesc\">Day</string>\n    <string name=\"ic_history_cdesc\">History</string>\n    <string name=\"ic_hourglass_cdesc\">Hourglass</string>\n    <string name=\"ic_browse_cdesc\">Browse</string>\n    <string name=\"ic_search_cdesc\">Search</string>\n    <string name=\"ic_chats_cdesc\">Chats</string>\n    <string name=\"ic_plus_cdesc\">Plus</string>\n    <string name=\"ic_settings_cdesc\">Settings</string>\n    <string name=\"ic_close_cdesc\">Close</string>\n    <string name=\"ic_copy_cdesc\">Copy</string>\n\n    <!-- Welcome view -->\n    <string name=\"welcome_app_name\">Pineapple</string>\n    <string name=\"welcome_slogan_text\">A Material version of the frontpage of the internet</string>\n    <string name=\"welcome_sign_in_button\">Sign in with Reddit</string>\n    <string name=\"welcome_continue_as_guest_button\">Continue as guest</string>\n\n    <!-- Key provider view -->\n    <string name=\"provide_key_title_text\">First, we\\'ll need your client ID</string>\n    <string name=\"provide_key_subtitle_text\">In order to keep this app free, each user must supply their own Reddit API client ID</string>\n    <string name=\"provide_key_entry_hint\">Client secret</string>\n    <string name=\"provide_key_dev_button\">Reddit Developer Dashboard</string>\n    <string name=\"provide_key_what_button\">What\\'s this?</string>\n    <string name=\"provide_key_entry_invalid\">Invalid client secret!</string>\n\n    <!-- Home view -->\n    <string name=\"home_title\">Home</string>\n    <string name=\"home_snackbar_auth_text\">You must log in to perform this action</string>\n    <string name=\"home_snackbar_auth_login\">Log in</string>\n    <string name=\"home_nav_home\">Browse</string>\n    <string name=\"home_nav_search\">Search</string>\n    <string name=\"home_nav_chats\">Chats</string>\n    <string name=\"home_nav_account\">Account</string>\n    <string name=\"home_drawer_settings\">Settings</string>\n    <string name=\"home_drawer_communities\">My communities</string>\n    <string name=\"home_drawer_communities_uless\">Popular communities</string>\n    \n    <!-- Search view -->\n    <string name=\"search_placeholder\">Search</string>\n    <string name=\"search_users\">Users</string>\n    <string name=\"search_communities\">Communities</string>\n    <string name=\"search_posts\">Posts</string>\n\n    <!-- Post view -->\n    <string name=\"post_copied_comment\">Copied comment</string>\n\n    <!-- Sort post sheet -->\n    <string name=\"sort_sheet_sort_header\">Sort posts by</string>\n    <string name=\"sort_sheet_time_header\">Time range</string>\n    <string name=\"sort_sheet_hot\">Hot</string>\n    <string name=\"sort_sheet_new\">New</string>\n    <string name=\"sort_sheet_rising\">Rising</string>\n    <string name=\"sort_sheet_controversial\">Controversial</string>\n    <string name=\"sort_sheet_top\">Top</string>\n    <string name=\"sort_sheet_day\">Day</string>\n    <string name=\"sort_sheet_week\">Week</string>\n    <string name=\"sort_sheet_month\">Month</string>\n    <string name=\"sort_sheet_year\">Year</string>\n    <string name=\"sort_sheet_all\">All time</string>\n\n</resources>"
  },
  {
    "path": "app/src/main/res/values/themes.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"Theme.Pineapple\" parent=\"android:Theme.Material.Light.NoActionBar\" />\n</resources>"
  },
  {
    "path": "app/src/main/res/xml/backup_rules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n   Sample backup rules file; uncomment and customize as necessary.\n   See https://developer.android.com/guide/topics/data/autobackup\n   for details.\n   Note: This file is ignored for devices older than API 31\n   See https://developer.android.com/about/versions/12/backup-restore\n-->\n<full-backup-content>\n    <!--\n   <include domain=\"sharedpref\" path=\".\"/>\n   <exclude domain=\"sharedpref\" path=\"device.xml\"/>\n-->\n</full-backup-content>"
  },
  {
    "path": "app/src/main/res/xml/data_extraction_rules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n   Sample data extraction rules file; uncomment and customize as necessary.\n   See https://developer.android.com/about/versions/12/backup-restore#xml-changes\n   for details.\n-->\n<data-extraction-rules>\n    <cloud-backup>\n        <!-- TODO: Use <include> and <exclude> to control what is backed up.\n        <include .../>\n        <exclude .../>\n        -->\n    </cloud-backup>\n    <!--\n    <device-transfer>\n        <include .../>\n        <exclude .../>\n    </device-transfer>\n    -->\n</data-extraction-rules>"
  },
  {
    "path": "app/src/test/java/com/pineapple/app/ExampleUnitTest.kt",
    "content": "package com.pineapple.app\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}"
  },
  {
    "path": "build.gradle.kts",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nplugins {\n    alias(libs.plugins.android.application) apply false\n    alias(libs.plugins.kotlin.android) apply false\n    alias(libs.plugins.kotlin.compose) apply false\n    alias(libs.plugins.hilt.android) apply false\n    alias(libs.plugins.ksp) apply false\n}"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\nagp = \"8.13.2\"\ncoil = \"3.1.0\"\nconverterGson = \"3.0.0\"\nhiltAndroid = \"2.57.1\"\nkotlin = \"2.0.21\"\ncoreKtx = \"1.17.0\"\njunit = \"4.13.2\"\njunitVersion = \"1.3.0\"\nespressoCore = \"3.7.0\"\nlifecycleRuntimeKtx = \"2.10.0\"\nactivityCompose = \"1.12.2\"\ncomposeBom = \"2025.12.01\"\nmmkv = \"2.3.0\"\nnavigationCompose = \"2.9.6\"\nmaterial3 = \"1.5.0-alpha11\"\nokhttp = \"4.12.0\"\nksp = \"2.0.21-1.0.27\"\nhilt = \"2.57.1\"\nhiltNavigationCompose = \"1.3.0\"\nroom = \"2.6.1\"\npaging = \"3.3.2\"\n\n[libraries]\nandroidx-core-ktx = { group = \"androidx.core\", name = \"core-ktx\", version.ref = \"coreKtx\" }\nandroidx-navigation-compose = { module = \"androidx.navigation:navigation-compose\", version.ref = \"navigationCompose\" }\ncoil-compose = { module = \"io.coil-kt.coil3:coil-compose\", version.ref = \"coil\" }\ncoil-network-okhttp = { module = \"io.coil-kt.coil3:coil-network-okhttp\", version.ref = \"coil\" }\nconverter-gson = { module = \"com.squareup.retrofit2:converter-gson\", version.ref = \"converterGson\" }\nhilt-android = { module = \"com.google.dagger:hilt-android\", version.ref = \"hiltAndroid\" }\nhilt-android-compiler = { module = \"com.google.dagger:hilt-android-compiler\", version.ref = \"hiltAndroid\" }\njunit = { group = \"junit\", name = \"junit\", version.ref = \"junit\" }\nandroidx-junit = { group = \"androidx.test.ext\", name = \"junit\", version.ref = \"junitVersion\" }\nandroidx-espresso-core = { group = \"androidx.test.espresso\", name = \"espresso-core\", version.ref = \"espressoCore\" }\nandroidx-lifecycle-runtime-ktx = { group = \"androidx.lifecycle\", name = \"lifecycle-runtime-ktx\", version.ref = \"lifecycleRuntimeKtx\" }\nandroidx-activity-compose = { group = \"androidx.activity\", name = \"activity-compose\", version.ref = \"activityCompose\" }\nandroidx-compose-bom = { group = \"androidx.compose\", name = \"compose-bom\", version.ref = \"composeBom\" }\nandroidx-ui = { group = \"androidx.compose.ui\", name = \"ui\" }\nandroidx-ui-graphics = { group = \"androidx.compose.ui\", name = \"ui-graphics\" }\nandroidx-ui-tooling = { group = \"androidx.compose.ui\", name = \"ui-tooling\" }\nandroidx-ui-tooling-preview = { group = \"androidx.compose.ui\", name = \"ui-tooling-preview\" }\nandroidx-ui-test-manifest = { group = \"androidx.compose.ui\", name = \"ui-test-manifest\" }\nandroidx-ui-test-junit4 = { group = \"androidx.compose.ui\", name = \"ui-test-junit4\" }\nandroidx-material3 = { group = \"androidx.compose.material3\", name = \"material3\", version.ref = \"material3\" }\nlogging-interceptor = { module = \"com.squareup.okhttp3:logging-interceptor\", version.ref = \"okhttp\" }\nmmkv = { module = \"com.tencent:mmkv\", version.ref = \"mmkv\" }\nokhttp = { module = \"com.squareup.okhttp3:okhttp\", version.ref = \"okhttp\" }\nretrofit = { module = \"com.squareup.retrofit2:retrofit\", version.ref = \"converterGson\" }\nandroidx-hilt-navigation-compose = { group = \"androidx.hilt\", name = \"hilt-navigation-compose\", version.ref = \"hiltNavigationCompose\" }\nroom-runtime = { module = \"androidx.room:room-runtime\", version.ref = \"room\" }\nroom-ktx = { module = \"androidx.room:room-ktx\", version.ref = \"room\" }\nroom-compiler = { module = \"androidx.room:room-compiler\", version.ref = \"room\" }\npaging-runtime = { module = \"androidx.paging:paging-runtime-ktx\", version.ref = \"paging\" }\npaging-compose = { module = \"androidx.paging:paging-compose\", version.ref = \"paging\" }\nroom-paging = { module = \"androidx.room:room-paging\", version.ref = \"room\" }\n\n\n[plugins]\nandroid-application = { id = \"com.android.application\", version.ref = \"agp\" }\nkotlin-android = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\nkotlin-compose = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }\nksp = { id = \"com.google.devtools.ksp\", version.ref = \"ksp\" }\nhilt-android = { id = \"com.google.dagger.hilt.android\", version.ref = \"hilt\" }\n\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Thu Dec 18 12:06:56 PST 2025\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.13-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. For more details, visit\n# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects\n# org.gradle.parallel=true\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app's APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n# Kotlin code style for this project: \"official\" or \"obsolete\":\nkotlin.code.style=official\n# Enables namespacing of each library's R class so that its R class includes only the\n# resources declared in the library itself and none from the library's dependencies,\n# thereby reducing the size of the R class for that library\nandroid.nonTransitiveRClass=true"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=`expr $i + 1`\n    done\n    case $i in\n        0) set -- ;;\n        1) set -- \"$args0\" ;;\n        2) set -- \"$args0\" \"$args1\" ;;\n        3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=`save \"$@\"`\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem      https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n\n@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto execute\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto execute\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "pluginManagement {\n    repositories {\n        google {\n            content {\n                includeGroupByRegex(\"com\\\\.android.*\")\n                includeGroupByRegex(\"com\\\\.google.*\")\n                includeGroupByRegex(\"androidx.*\")\n            }\n        }\n        mavenCentral()\n        gradlePluginPortal()\n    }\n}\ndependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n        google()\n        mavenCentral()\n        gradlePluginPortal()\n    }\n}\n\nrootProject.name = \"Pineapple\"\ninclude(\":app\")\n "
  }
]