├── .github
├── ISSUE_TEMPLATE
│ ├── bug.yml
│ ├── config.yml
│ └── feature-request.yml
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── build.yml
├── .gitignore
├── .idea
├── .gitignore
├── .name
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── deploymentTargetDropDown.xml
├── discord.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── kotlinc.xml
├── ktfmt.xml
├── misc.xml
└── vcs.xml
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── release
│ ├── Chouten_1.0.apk
│ └── output-metadata.json
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── chouten
│ │ └── app
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_chouten_launcher-playstore.png
│ ├── java
│ │ └── com
│ │ │ └── chouten
│ │ │ └── app
│ │ │ ├── ChoutenApp.kt
│ │ │ ├── Functions.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── Network.kt
│ │ │ ├── PreferenceManager.kt
│ │ │ ├── data
│ │ │ ├── AlertData.kt
│ │ │ ├── Constants.kt
│ │ │ ├── DataLayer.kt
│ │ │ ├── LogDataLayer.kt
│ │ │ ├── ModuleDataLayer.kt
│ │ │ ├── ModuleModel.kt
│ │ │ ├── SettingsModel.kt
│ │ │ ├── SnackbarData.kt
│ │ │ └── WebviewHandler.kt
│ │ │ └── ui
│ │ │ ├── Navigation.kt
│ │ │ ├── Screen.kt
│ │ │ ├── components
│ │ │ ├── Alert.kt
│ │ │ ├── Banners.kt
│ │ │ ├── Divider.kt
│ │ │ ├── EpisodeItem.kt
│ │ │ ├── ListAnimation.kt
│ │ │ ├── ModuleSelector.kt
│ │ │ ├── SegmentedControl.kt
│ │ │ ├── SettingsComponents.kt
│ │ │ ├── Shimmer.kt
│ │ │ ├── Slider.kt
│ │ │ ├── Snackbar.kt
│ │ │ └── SwipeAction.kt
│ │ │ ├── theme
│ │ │ ├── Colors.kt
│ │ │ ├── Modifiers.kt
│ │ │ ├── Shapes.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ │ └── views
│ │ │ ├── home
│ │ │ ├── HomePage.kt
│ │ │ └── HomePageViewModel.kt
│ │ │ ├── info
│ │ │ ├── InfoPage.kt
│ │ │ └── InfoPageViewModel.kt
│ │ │ ├── search
│ │ │ ├── SearchPage.kt
│ │ │ └── SearchPageViewModel.kt
│ │ │ ├── settings
│ │ │ ├── MorePage.kt
│ │ │ ├── MorePageViewModel.kt
│ │ │ └── screens
│ │ │ │ ├── LogPage.kt
│ │ │ │ ├── NetworkPage.kt
│ │ │ │ └── appearance
│ │ │ │ ├── AppearancePage.kt
│ │ │ │ └── AppearanceViewModel.kt
│ │ │ └── watch
│ │ │ ├── PlayerActivity.kt
│ │ │ ├── WatchPage.kt
│ │ │ └── WatchPageViewModel.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_chouten_launcher.xml
│ │ ├── ic_chouten_launcher_round.xml
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_chouten_launcher.png
│ │ ├── ic_chouten_launcher_foreground.png
│ │ ├── ic_chouten_launcher_round.png
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_chouten_launcher.png
│ │ ├── ic_chouten_launcher_foreground.png
│ │ ├── ic_chouten_launcher_round.png
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_chouten_launcher.png
│ │ ├── ic_chouten_launcher_foreground.png
│ │ ├── ic_chouten_launcher_round.png
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_chouten_launcher.png
│ │ ├── ic_chouten_launcher_foreground.png
│ │ ├── ic_chouten_launcher_round.png
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_chouten_launcher.png
│ │ ├── ic_chouten_launcher_foreground.png
│ │ ├── ic_chouten_launcher_round.png
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values-night
│ │ └── splashscreen_dark.xml
│ │ ├── values-v31
│ │ └── colors.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── ic_chouten_launcher_background.xml
│ │ ├── splashscreen.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── com
│ └── chouten
│ └── app
│ └── ExampleUnitTest.kt
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── lint.xml
└── settings.gradle
/.github/ISSUE_TEMPLATE/bug.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: File a bug report
3 | labels: [bug]
4 | title: "[BUG] - "
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Thanks for taking the time to fill out this bug report!
10 |
11 | Please do not submit feature requests.
12 | - type: textarea
13 | id: reproduce
14 | attributes:
15 | label: Steps To Reproduce
16 | description: How can we reproduce the behavior.
17 | value: |
18 | 1. Go to '...'
19 | 2. Click on '....'
20 | 3. Scroll down to '....'
21 | 4. Click on '...'
22 | validations:
23 | required: true
24 | - type: textarea
25 | id: expected
26 | attributes:
27 | label: Expected Result
28 | description: A clear and concise description of what you expected to happen.
29 | validations:
30 | required: true
31 | - type: textarea
32 | id: actual
33 | attributes:
34 | label: Actual Result
35 | description: A clear and concise description of what is happening.
36 | validations:
37 | required: true
38 | - type: textarea
39 | id: screenshots
40 | attributes:
41 | label: Screenshots or Videos
42 | description: If applicable, add screenshots and/or a short video to help explain your problem.
43 | - type: textarea
44 | id: additional-context
45 | attributes:
46 | label: Additional Context
47 | description: Add any other context about the problem here.
48 | - type: input
49 | id: os-version
50 | attributes:
51 | label: Operating System Version
52 | description: What version of the operating system(s) are you seeing the problem on?
53 | - type: input
54 | id: device
55 | attributes:
56 | label: Device
57 | description: Which device are you seeing the problem on?
58 | placeholder: Samsung Galaxy S10, Google Pixel 7
59 | # - type: input
60 | # id: version
61 | # attributes:
62 | # label: Build Version
63 | # description: What version of our software are you running? (go to "Settings" → "About" in the app)
64 | # validations:
65 | # required: true
66 | - type: checkboxes
67 | id: beta
68 | attributes:
69 | label: Beta
70 | options:
71 | - label: Using a pre-release version of the application.
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.yml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description: Request a new feature or functionality
3 | labels: [feature request]
4 | title: "[FEATURE REQUEST] - "
5 | body:
6 |
7 | - type: textarea
8 | id: feature-description
9 | attributes:
10 | label: Describe your suggested feature
11 | description: How can Chouten be improved?
12 | placeholder: |
13 | Example:
14 | "It should work like this..."
15 | validations:
16 | required: true
17 |
18 | - type: textarea
19 | id: examples
20 | attributes:
21 | label: Examples
22 | placeholder: |
23 | Found an open source app that already implements this or documentation on it?
24 | Put a link to it or if it's an idea for the ui a screenshot or screen recording here
25 |
26 | - type: textarea
27 | id: other-details
28 | attributes:
29 | label: Other details
30 | placeholder: |
31 | Additional details and attachments.
32 |
33 | - type: checkboxes
34 | id: acknowledgements
35 | attributes:
36 | label: Acknowledgements
37 | description: Read this carefully, we will close and ignore your issue if you skimmed through this.
38 | options:
39 | - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
40 | required: true
41 | - label: I have written a short but informative title.
42 | required: true
43 | - label: I have updated the app to the **latest version**.
44 | required: true
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Type of change
2 | - [ ] Bug fix
3 | - [ ] New feature development
4 | - [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
5 | - [ ] Build/deploy pipeline (DevOps)
6 | - [ ] Other
7 |
8 | ## Objective
9 |
10 |
11 |
12 |
13 | ## Code changes
14 |
15 |
16 | * **file.ext:** Description of what was changed and why
17 |
18 | ## Screenshots
19 |
20 |
21 |
22 |
23 | ## Before you submit
24 | - Please check for errors (`gradlew lint`) (required)
25 | - Please format and remove unused imports (required)
26 | - Please add **unit tests** where it makes sense to do so (encouraged but not required)
27 | - If this change requires a **documentation update** - please notify us
28 | - If this change has particular **deployment requirements** - please notify us
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 | branches: [ "main", "dev" ]
6 | pull_request:
7 | branches: [ "main", "dev" ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - name: set up JDK 11
15 | uses: actions/setup-java@v3
16 | with:
17 | distribution: 'temurin'
18 | java-version: '17'
19 | cache: 'gradle'
20 |
21 | - name: Grant execute permission for gradlew
22 | run: chmod +x gradlew
23 | # Here we need to decode keystore.jks from base64 string and place it
24 | # in the folder specified in the release signing configuration
25 | - name: Decode Keystore
26 | id: decode_keystore
27 | uses: timheuer/base64-to-file@v1.2
28 | with:
29 | fileName: 'android_keystore.jks'
30 | fileDir: '/home/runner/work/Chouten-Android/Chouten-Android/app/keystore/'
31 | encodedString: ${{ secrets.KEYSTORE }}
32 |
33 | # Build and sign APK ("-x test" argument is used to skip tests)
34 | - name: Build APK
35 | run: ./gradlew :app:assembleRelease -x test
36 | env:
37 | SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
38 | SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
39 | SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
40 |
41 | # Show information about the APK's signing certificates
42 | - name: Verify Signature
43 | run: $ANDROID_SDK_ROOT/build-tools/33.0.1/apksigner verify --print-certs app/build/outputs/apk/release/Chouten.apk
44 |
45 | - name: Upload a Build Artifact
46 | uses: actions/upload-artifact@v3.0.0
47 | with:
48 | name: Chouten
49 | path: "app/build/outputs/apk/release/Chouten.apk"
50 |
51 | - name: Upload the APK to Discord
52 | shell: bash
53 | run: |
54 | curl -F "debug=@app/build/outputs/apk/release/Chouten.apk" ${{ secrets.DISCORD_WEBHOOK_URL }}
55 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | Chouten
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | xmlns:android
18 |
19 | ^$
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | xmlns:.*
29 |
30 | ^$
31 |
32 |
33 | BY_NAME
34 |
35 |
36 |
37 |
38 |
39 |
40 | .*:id
41 |
42 | http://schemas.android.com/apk/res/android
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | .*:name
52 |
53 | http://schemas.android.com/apk/res/android
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | name
63 |
64 | ^$
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | style
74 |
75 | ^$
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | .*
85 |
86 | ^$
87 |
88 |
89 | BY_NAME
90 |
91 |
92 |
93 |
94 |
95 |
96 | .*
97 |
98 | http://schemas.android.com/apk/res/android
99 |
100 |
101 | ANDROID_ATTRIBUTE_ORDER
102 |
103 |
104 |
105 |
106 |
107 |
108 | .*
109 |
110 | .*
111 |
112 |
113 | BY_NAME
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/.idea/discord.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/ktfmt.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.10'
5 | }
6 |
7 | android {
8 | namespace 'com.chouten.app'
9 | compileSdk 33
10 |
11 | defaultConfig {
12 | applicationId "com.chouten.app"
13 | minSdk 23
14 | targetSdk 33
15 | versionCode 1
16 | versionName "0.1.3-alpha"
17 | minSdkVersion 23
18 |
19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables {
21 | useSupportLibrary true
22 | }
23 | }
24 |
25 | lintOptions {
26 | lintConfig = file("$rootDir/lint.xml")
27 | }
28 |
29 | signingConfigs {
30 | release {
31 | storeFile = file("keystore/android_keystore.jks")
32 | storePassword System.getenv("SIGNING_STORE_PASSWORD")
33 | keyAlias System.getenv("SIGNING_KEY_ALIAS")
34 | keyPassword System.getenv("SIGNING_KEY_PASSWORD")
35 | }
36 | }
37 |
38 | buildTypes {
39 | release {
40 | minifyEnabled false
41 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
42 |
43 | applicationVariants.all { variant ->
44 | variant.outputs.all {
45 | outputFileName = "Chouten.apk"
46 | }
47 | }
48 | signingConfig signingConfigs.release
49 | }
50 | }
51 | compileOptions {
52 | sourceCompatibility JavaVersion.VERSION_1_8
53 | targetCompatibility JavaVersion.VERSION_1_8
54 | }
55 | kotlinOptions {
56 | jvmTarget = '1.8'
57 | }
58 | buildFeatures {
59 | compose true
60 | }
61 | composeOptions {
62 | kotlinCompilerExtensionVersion '1.4.3'
63 | }
64 | packagingOptions {
65 | resources {
66 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
67 | }
68 | }
69 | }
70 |
71 | dependencies {
72 |
73 | implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
74 |
75 | implementation "com.github.skydoves:landscapist-glide:2.1.9"
76 |
77 | def lifecycle_version = "2.6.1"
78 | def nav_version = "2.6.0"
79 | def exo_version = "1.0.2"
80 |
81 | implementation 'androidx.core:core-ktx:1.10.1'
82 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
83 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
84 | implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
85 | implementation 'androidx.activity:activity-compose:1.7.2'
86 | implementation "androidx.compose.ui:ui:$compose_version"
87 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
88 | implementation "androidx.navigation:navigation-compose:$nav_version"
89 | implementation "androidx.compose.ui:ui-util:$compose_version"
90 |
91 | // Accompanist
92 | implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version"
93 |
94 | // Exoplayer
95 | implementation "androidx.media3:media3-exoplayer:$exo_version"
96 | implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
97 | implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
98 | implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
99 | implementation "androidx.media3:media3-ui:$exo_version"
100 | implementation "androidx.media3:media3-cast:$exo_version"
101 | implementation "androidx.media3:media3-transformer:$exo_version"
102 | implementation "androidx.media3:media3-decoder:$exo_version"
103 | implementation "androidx.media3:media3-datasource:$exo_version"
104 | implementation "androidx.media3:media3-common:$exo_version"
105 | implementation "androidx.media3:media3-database:$exo_version"
106 |
107 | // Compose Testing
108 | testImplementation 'junit:junit:4.13.2'
109 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
110 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
111 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
112 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
113 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
114 |
115 | implementation 'androidx.core:core-splashscreen:1.0.1'
116 |
117 | // Material UI
118 | implementation 'androidx.compose.material3:material3:1.2.0-alpha02'
119 | implementation "androidx.compose.material:material-icons-extended:$compose_version"
120 | implementation 'androidx.compose.material:material:1.4.3'
121 |
122 | // Network
123 | implementation 'com.github.brahmkshatriya:NiceHttp:1.1.0'
124 | implementation "org.jetbrains.kotlin:kotlin-reflect:1.8.10"
125 |
126 | implementation 'com.valentinilk.shimmer:compose-shimmer:1.0.4'
127 |
128 | implementation "com.google.guava:guava:31.1-android"
129 | }
130 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/release/Chouten_1.0.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/release/Chouten_1.0.apk
--------------------------------------------------------------------------------
/app/release/output-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "artifactType": {
4 | "type": "APK",
5 | "kind": "Directory"
6 | },
7 | "applicationId": "com.chouten.app",
8 | "variantName": "release",
9 | "elements": [
10 | {
11 | "type": "SINGLE",
12 | "filters": [],
13 | "attributes": [],
14 | "versionCode": 1,
15 | "versionName": "1.0",
16 | "outputFile": "Chouten_1.0.apk"
17 | }
18 | ],
19 | "elementType": "File"
20 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/chouten/app/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext =
22 | InstrumentationRegistry.getInstrumentation().targetContext
23 | assertEquals("com.chouten.app", appContext.packageName)
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
9 |
10 |
13 |
14 |
27 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
52 |
55 |
56 |
61 |
62 |
--------------------------------------------------------------------------------
/app/src/main/ic_chouten_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/ic_chouten_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ChoutenApp.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.expandVertically
5 | import androidx.compose.animation.shrinkVertically
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.WindowInsets
8 | import androidx.compose.foundation.layout.WindowInsetsSides
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.navigationBars
11 | import androidx.compose.foundation.layout.only
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.layout.windowInsetsPadding
14 | import androidx.compose.material3.Scaffold
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.collectAsState
17 | import androidx.compose.ui.Modifier
18 | import androidx.navigation.compose.rememberNavController
19 | import com.chouten.app.data.DataLayer
20 | import com.chouten.app.data.LogDataLayer
21 | import com.chouten.app.data.ModuleDataLayer
22 | import com.chouten.app.data.NavigationItems
23 | import com.chouten.app.ui.BottomNavigationBar
24 | import com.chouten.app.ui.Navigation
25 | import com.chouten.app.ui.components.AppStateBanners
26 | import com.chouten.app.ui.components.ChoutenAlert
27 | import com.chouten.app.ui.components.Snackbar
28 | import kotlinx.coroutines.flow.asStateFlow
29 |
30 | lateinit var ModuleLayer: ModuleDataLayer
31 | lateinit var LogLayer: LogDataLayer
32 | val PrimaryDataLayer = DataLayer()
33 |
34 | fun initializeRepositories() {
35 | ModuleLayer = ModuleDataLayer()
36 | LogLayer = LogDataLayer()
37 | }
38 |
39 | @Composable
40 | fun ChoutenApp() {
41 | val scaffoldInsets = WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
42 |
43 | val navController = rememberNavController()
44 | val alerts = PrimaryDataLayer.alertQueue.asStateFlow()
45 |
46 | Scaffold(modifier = Modifier.fillMaxSize(), topBar = {
47 | AppStateBanners(
48 | downloadedOnlyMode = preferenceHandler.isOfflineMode,
49 | incognitoMode = preferenceHandler.isIncognito,
50 | indexing = false,
51 | modifier = Modifier.windowInsetsPadding(scaffoldInsets),
52 | )
53 | },
54 | contentWindowInsets = scaffoldInsets,
55 | bottomBar = {
56 | AnimatedVisibility(
57 | visible = PrimaryDataLayer.isNavigationShown,
58 | enter = expandVertically(),
59 | exit = shrinkVertically()
60 | ) {
61 | BottomNavigationBar(navController = navController, items = listOf(
62 | NavigationItems.HomePage,
63 | NavigationItems.SearchPage,
64 | NavigationItems.MorePage,
65 | ), onItemClick = {
66 | navController.navigate(route = it.route)
67 | })
68 | }
69 | }, snackbarHost = { Snackbar() }, content = { padding ->
70 | Box(
71 | modifier = Modifier
72 | .padding(padding)
73 | .fillMaxSize()
74 | ) {
75 | Box(
76 | modifier = Modifier.fillMaxSize()
77 | ) {
78 | alerts.collectAsState().value.forEach {
79 | ChoutenAlert(alert = it)
80 | }
81 | }
82 |
83 | Navigation(navController)
84 | }
85 | })
86 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app
2 |
3 | import android.content.ClipData
4 | import android.content.Intent
5 | import android.content.Intent.ACTION_VIEW
6 | import android.os.Bundle
7 | import android.util.Log
8 | import androidx.activity.ComponentActivity
9 | import androidx.activity.compose.setContent
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.runtime.LaunchedEffect
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.graphics.luminance
14 | import androidx.core.net.toUri
15 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
16 | import androidx.core.view.WindowCompat
17 | import androidx.lifecycle.lifecycleScope
18 | import com.chouten.app.ui.components.DownloadedOnlyBannerBackgroundColor
19 | import com.chouten.app.ui.components.IncognitoModeBannerBackgroundColor
20 | import com.chouten.app.ui.theme.ChoutenTheme
21 | import com.google.accompanist.systemuicontroller.rememberSystemUiController
22 | import kotlinx.coroutines.Dispatchers
23 | import kotlinx.coroutines.launch
24 |
25 | const val PREFERENCE_FILE = "CHOUTEN_PREFS"
26 |
27 | lateinit var preferenceHandler: PreferenceManager
28 |
29 | lateinit var App: MainActivity
30 |
31 | class MainActivity : ComponentActivity() {
32 | override fun onCreate(savedInstanceState: Bundle?) {
33 | installSplashScreen()
34 |
35 | super.onCreate(savedInstanceState)
36 | App = this@MainActivity
37 |
38 | actionBar?.hide()
39 | WindowCompat.setDecorFitsSystemWindows(window, false)
40 |
41 | preferenceHandler = PreferenceManager(this)
42 |
43 | checkPermissions()
44 | createAppDirectory()
45 |
46 | initializeNetwork(applicationContext)
47 | initializeRepositories()
48 |
49 | lifecycleScope.launch(Dispatchers.IO) {
50 | ModuleLayer.loadModules()
51 | }
52 |
53 | if (intent != null) handleSharedIntent(intent)
54 | setContent {
55 | val incognito = preferenceHandler.isIncognito
56 | val downloadOnly = preferenceHandler.isOfflineMode
57 | // Set statusbar color considering the top app state banner
58 | val systemUiController = rememberSystemUiController()
59 | ChoutenTheme {
60 | val statusBarBackgroundColor = when {
61 | downloadOnly -> DownloadedOnlyBannerBackgroundColor
62 | incognito -> IncognitoModeBannerBackgroundColor
63 | else -> MaterialTheme.colorScheme.surface
64 | }
65 | LaunchedEffect(systemUiController, statusBarBackgroundColor) {
66 | val luminance = statusBarBackgroundColor.luminance()
67 | val darkIcons = luminance > 0.5
68 |
69 | systemUiController.setStatusBarColor(
70 | color = Color.Transparent,
71 | darkIcons = darkIcons,
72 | transformColorForLightContent = { Color.Black }
73 | )
74 | }
75 | ChoutenApp()
76 | }
77 | }
78 | }
79 |
80 | private fun handleSharedIntent(intent: Intent?) {
81 | Log.d("INTENT", "$intent")
82 | // Enqueue the Resource
83 | lifecycleScope.launch {
84 | when (intent?.type) {
85 | "text/plain" -> ModuleLayer.enqueueRemoteInstall(
86 | this@MainActivity, intent
87 | )
88 |
89 | "application/octet-stream" ->
90 | ModuleLayer.enqueueFileInstall(
91 | intent, this@MainActivity
92 | )
93 |
94 | else -> Log.d(
95 | "IMPORT",
96 | "Import type `${intent?.type}` not yet implemented"
97 | )
98 | }
99 |
100 | // If the file is opened in a file manager, the intent will be null
101 | // so we need to check for that
102 | if (intent != null && intent.action == ACTION_VIEW) {
103 | val contentUrl = intent.dataString
104 | // Set the clipdata to the content url
105 | val clipData = ClipData.newRawUri("Content URL", contentUrl?.toUri())
106 | intent.clipData = clipData
107 | ModuleLayer.enqueueFileInstall(
108 | intent, this@MainActivity
109 | )
110 | }
111 | }
112 | }
113 |
114 | override fun onNewIntent(intent: Intent?) {
115 | super.onNewIntent(intent)
116 | handleSharedIntent(intent)
117 | }
118 |
119 | override fun onDestroy() {
120 | super.onDestroy()
121 | println("Destroying Activity")
122 | ModuleLayer.webviewHandler.destroy()
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/Network.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import android.util.Log
6 | import com.chouten.app.data.CustomDNS
7 | import dev.brahmkshatriya.nicehttp.Requests
8 | import dev.brahmkshatriya.nicehttp.ResponseParser
9 | import dev.brahmkshatriya.nicehttp.addGenericDns
10 | import kotlinx.coroutines.CancellationException
11 | import kotlinx.coroutines.async
12 | import kotlinx.coroutines.runBlocking
13 | import kotlinx.serialization.ExperimentalSerializationApi
14 | import kotlinx.serialization.InternalSerializationApi
15 | import kotlinx.serialization.decodeFromString
16 | import kotlinx.serialization.json.Json
17 | import kotlinx.serialization.serializer
18 | import okhttp3.Cache
19 | import okhttp3.OkHttpClient
20 | import java.io.File
21 | import java.io.PrintWriter
22 | import java.io.Serializable
23 | import java.io.StringWriter
24 | import java.util.concurrent.TimeUnit
25 | import kotlin.reflect.KClass
26 | import kotlin.reflect.KFunction
27 |
28 | // Borrowed from https://github.com/saikou-app/saikou/blob/main/app/src/main/java/ani/saikou/Network.kt
29 | //TODO: properly implement functions, this is a half-assed copy
30 |
31 | val defaultHeaders = mapOf(
32 | "User-Agent" to
33 | "Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"
34 | .format(Build.VERSION.RELEASE, Build.MODEL)
35 | )
36 | lateinit var cache: Cache
37 |
38 | lateinit var okHttpClient: OkHttpClient
39 | lateinit var client: Requests
40 |
41 | fun initializeNetwork(context: Context) {
42 | val dns = preferenceHandler.dns
43 | cache = Cache(
44 | File(context.cacheDir, "http_cache"),
45 | 5 * 1024L * 1024L // 5 MiB
46 | )
47 | okHttpClient = OkHttpClient.Builder()
48 | .followRedirects(true)
49 | .followSslRedirects(true)
50 | .cache(cache)
51 | .apply {
52 | when (dns) {
53 | CustomDNS.GOOGLE -> addGoogleDns()
54 | CustomDNS.CLOUDFLARE -> addCloudFlareDns()
55 | CustomDNS.ADGUARD -> addAdGuardDns()
56 | else -> {}
57 | }
58 | }
59 | .build()
60 | client = Requests(
61 | okHttpClient,
62 | defaultHeaders,
63 | defaultCacheTime = 6,
64 | defaultCacheTimeUnit = TimeUnit.HOURS,
65 | responseParser = Mapper
66 | )
67 | }
68 |
69 | object Mapper : ResponseParser {
70 |
71 | @OptIn(ExperimentalSerializationApi::class)
72 | val json = Json {
73 | isLenient = true
74 | ignoreUnknownKeys = true
75 | explicitNulls = false
76 | }
77 |
78 | @OptIn(InternalSerializationApi::class)
79 | override fun parse(text: String, kClass: KClass): T {
80 | return json.decodeFromString(kClass.serializer(), text)
81 | }
82 |
83 | override fun parseSafe(text: String, kClass: KClass): T? {
84 | return try {
85 | parse(text, kClass)
86 | } catch (e: Exception) {
87 | null
88 | }
89 | }
90 |
91 | inline fun parse(text: String): T {
92 | return json.decodeFromString(text)
93 | }
94 | }
95 |
96 | fun Collection.asyncMap(f: suspend (A) -> B): List = runBlocking {
97 | map { async { f(it) } }.map { it.await() }
98 | }
99 |
100 | fun Collection.asyncMapNotNull(f: suspend (A) -> B?): List =
101 | runBlocking {
102 | map { async { f(it) } }.mapNotNull { it.await() }
103 | }
104 |
105 | fun logError(e: Exception, post: Boolean = true/*, snackbar: Boolean = true*/) {
106 | val sw = StringWriter()
107 | val pw = PrintWriter(sw)
108 | e.printStackTrace(pw)
109 | val stackTrace: String = sw.toString()
110 | if (post) {
111 | //if (snackbar)
112 | //snackString(e.localizedMessage, null, stackTrace)
113 | //else
114 | //toast(e.localizedMessage)
115 | Log.d("Error", stackTrace)
116 | }
117 | e.printStackTrace()
118 | }
119 |
120 | fun tryWith(
121 | post: Boolean = false/*, snackbar: Boolean = true*/,
122 | call: () -> T
123 | ): T? {
124 | return try {
125 | call.invoke()
126 | } catch (e: Exception) {
127 | logError(e, post/*, snackbar*/)
128 | null
129 | }
130 | }
131 |
132 | suspend fun tryWithSuspend(
133 | post: Boolean = false/*, snackbar: Boolean = true*/,
134 | call: suspend () -> T
135 | ): T? {
136 | return try {
137 | call.invoke()
138 | } catch (e: Exception) {
139 | logError(e, post/*, snackbar*/)
140 | null
141 | } catch (e: CancellationException) {
142 | null
143 | }
144 | }
145 |
146 | /**
147 | * A url, which can also have headers
148 | * **/
149 | data class FileUrl(
150 | val url: String,
151 | val headers: Map = mapOf()
152 | ) : Serializable {
153 | companion object {
154 | operator fun get(
155 | url: String?,
156 | headers: Map = mapOf()
157 | ): FileUrl? {
158 | return FileUrl(url ?: return null, headers)
159 | }
160 | }
161 | }
162 |
163 | //Credits to leg
164 | data class Lazier(
165 | val lClass: KFunction,
166 | val name: String
167 | ) {
168 | val get = lazy { lClass.call() }
169 | }
170 |
171 | fun lazyList(vararg objects: Pair>): List> {
172 | return objects.map {
173 | Lazier(it.second, it.first)
174 | }
175 | }
176 |
177 | fun T.printIt(pre: String = ""): T {
178 | println("$pre$this")
179 | return this
180 | }
181 |
182 |
183 | fun OkHttpClient.Builder.addGoogleDns() = (
184 | addGenericDns(
185 | "https://dns.google/dns-query",
186 | listOf(
187 | "8.8.4.4",
188 | "8.8.8.8"
189 | )
190 | ))
191 |
192 | fun OkHttpClient.Builder.addCloudFlareDns() = (
193 | addGenericDns(
194 | "https://cloudflare-dns.com/dns-query",
195 | listOf(
196 | "1.1.1.1",
197 | "1.0.0.1",
198 | "2606:4700:4700::1111",
199 | "2606:4700:4700::1001"
200 | )
201 | ))
202 |
203 | fun OkHttpClient.Builder.addAdGuardDns() = (
204 | addGenericDns(
205 | "https://dns.adguard.com/dns-query",
206 | listOf(
207 | // "Non-filtering"
208 | "94.140.14.140",
209 | "94.140.14.141",
210 | )
211 | ))
212 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/PreferenceManager.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import android.os.Build
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.setValue
9 | import androidx.core.content.edit
10 | import com.chouten.app.data.AppThemeType
11 | import com.chouten.app.data.CustomDNS
12 | import com.chouten.app.data.Preferences
13 | import kotlin.reflect.KProperty
14 |
15 | class PreferenceManager(context: Context) :
16 | BasePreferenceManager(
17 | context.applicationContext.getSharedPreferences(
18 | PREFERENCE_FILE,
19 | Context.MODE_PRIVATE
20 | )
21 | ) {
22 | var isOledTheme: Boolean by booleanPreference(
23 | Preferences.Settings.oledTheme.preference.first,
24 | false
25 | )
26 | var isDynamicColor: Boolean by booleanPreference(
27 | Preferences.Settings.dynamicColor.preference.first,
28 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
29 | )
30 |
31 | var isOfflineMode: Boolean by booleanPreference(
32 | Preferences.Settings.downloadedOnly.preference.first,
33 | false
34 | )
35 |
36 | var isIncognito: Boolean by booleanPreference(
37 | Preferences.Settings.incognito.preference.first,
38 | false
39 | )
40 |
41 | var isDevMode: Boolean by booleanPreference(
42 | Preferences.Settings.devMode.preference.first,
43 | false
44 | )
45 |
46 | var selectedModule by stringPreference(
47 | Preferences.SelectedModule
48 | )
49 |
50 | var themeType by enumPreference(
51 | Preferences.Settings.themeType.preference.first,
52 | AppThemeType.SYSTEM
53 | )
54 |
55 | var dns by enumPreference(
56 | Preferences.Settings.dns.preference.first,
57 | CustomDNS.NONE
58 | )
59 | }
60 |
61 | abstract class BasePreferenceManager(private val handler: SharedPreferences) {
62 |
63 | fun getBoolean(key: String, defaultValue: Boolean): Boolean =
64 | handler.getBoolean(key, defaultValue)
65 |
66 | fun putBoolean(key: String, value: Boolean) =
67 | handler.edit { putBoolean(key, value) }
68 |
69 | fun getString(key: String, defaultValue: String?): String? =
70 | handler.getString(key, defaultValue)
71 |
72 | fun putString(key: String, value: String?): Unit =
73 | handler.edit { putString(key, value) }
74 |
75 | fun getInt(key: String, defaultValue: Int): Int =
76 | handler.getInt(key, defaultValue)
77 |
78 | fun putInt(key: String, value: Int): Unit =
79 | handler.edit { putInt(key, value) }
80 |
81 | fun getFloat(key: String, defaultValue: Float): Float =
82 | handler.getFloat(key, defaultValue)
83 |
84 | fun putFloat(key: String, value: Float): Unit =
85 | handler.edit { putFloat(key, value) }
86 |
87 | fun getLong(key: String, defaultValue: Long): Long =
88 | handler.getLong(key, defaultValue)
89 |
90 | fun putLong(key: String, value: Long): Unit =
91 | handler.edit { putLong(key, value) }
92 |
93 | inline fun > getEnum(key: String, defaultValue: T): T =
94 | enumValueOf(getString(key, defaultValue.name)!!)
95 |
96 | inline fun > putEnum(key: String, value: T) =
97 | putString(key, value.name)
98 |
99 | protected class Preference(
100 | private val key: String,
101 | defaultValue: T,
102 | getter: (key: String, defaultValue: T) -> T,
103 | private val setter: (key: String, value: T) -> Unit
104 | ) {
105 | var value by mutableStateOf(getter(key, defaultValue))
106 | private set
107 |
108 | operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
109 | operator fun setValue(
110 | thisRef: Any?,
111 | property: KProperty<*>,
112 | updatedValue: T
113 | ) {
114 | value = updatedValue
115 | setter(key, updatedValue)
116 | }
117 | }
118 |
119 | protected fun booleanPreference(
120 | key: String,
121 | defaultValue: Boolean
122 | ) = Preference(
123 | key = key,
124 | defaultValue = defaultValue,
125 | getter = { _, _ -> getBoolean(key, defaultValue) },
126 | setter = { _, value -> putBoolean(key, value) }
127 | )
128 |
129 | protected fun stringPreference(
130 | key: String,
131 | defaultValue: String = ""
132 | ) = Preference(
133 | key = key,
134 | defaultValue = defaultValue,
135 | getter = ::getString,
136 | setter = ::putString
137 | )
138 |
139 | protected fun intPreference(
140 | key: String,
141 | defaultValue: Int
142 | ) = Preference(
143 | key = key,
144 | defaultValue = defaultValue,
145 | getter = ::getInt,
146 | setter = ::putInt
147 | )
148 |
149 | protected fun floatPreference(
150 | key: String,
151 | defaultValue: Float
152 | ) = Preference(
153 | key = key,
154 | defaultValue = defaultValue,
155 | getter = ::getFloat,
156 | setter = ::putFloat
157 | )
158 |
159 | protected fun longPreference(
160 | key: String,
161 | defaultValue: Long
162 | ) = Preference(
163 | key = key,
164 | defaultValue = defaultValue,
165 | getter = ::getLong,
166 | setter = ::putLong
167 | )
168 |
169 | protected inline fun > enumPreference(
170 | key: String,
171 | defaultValue: T
172 | ) = Preference(
173 | key = key,
174 | defaultValue = defaultValue,
175 | getter = ::getEnum,
176 | setter = ::putEnum
177 | )
178 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/data/AlertData.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.data
2 |
3 | import androidx.compose.ui.graphics.vector.ImageVector
4 |
5 | data class AlertData(
6 | val title: String = "Notice",
7 | val message: String,
8 | val confirmButtonText: String? = null,
9 | val confirmButtonAction: (() -> Unit)? = null,
10 | val cancelButtonText: String? = null,
11 | val cancelButtonAction: (() -> Unit)? = null,
12 | val icon: ImageVector? = null,
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/data/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.data
2 |
3 | import android.os.Build
4 | import android.os.Environment
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.filled.Home
7 | import androidx.compose.material.icons.filled.MoreHoriz
8 | import androidx.compose.material.icons.filled.Search
9 | import androidx.compose.material.icons.outlined.Home
10 | import androidx.compose.material.icons.outlined.MoreHoriz
11 | import androidx.compose.material.icons.outlined.Search
12 | import com.chouten.app.R
13 | import com.chouten.app.ui.BottomNavItem
14 | import java.io.File
15 |
16 | object AppPaths {
17 | val baseDir =
18 | Environment.getExternalStoragePublicDirectory("${Environment.DIRECTORY_DOCUMENTS}/Chouten/")
19 | val _toCreate = listOf("Modules", "Themes")
20 | val addedDirs = mutableMapOf()
21 | }
22 |
23 | object RequestCodes {
24 | const val allowAllFiles = 1
25 | }
26 |
27 | object Preferences {
28 | const val SelectedModule = "SelectedModule"
29 |
30 | object Settings {
31 | val oledTheme = ChoutenSetting(
32 | R.string.oled_theme_toggle__title,
33 | R.string.oled_theme_toggle__desc,
34 | preference = Pair("oledMode", Boolean)
35 | )
36 | val downloadedOnly = ChoutenSetting(
37 | R.string.downloaded_only_toggle__title,
38 | R.string.downloaded_only_toggle__desc,
39 | preference = Pair("offlineMode", Boolean)
40 | )
41 |
42 | val incognito = ChoutenSetting(
43 | R.string.incognito_toggle__title,
44 | R.string.incognito_toggle__desc,
45 | preference = Pair("incognito", Boolean)
46 | )
47 |
48 | val devMode = ChoutenSetting(
49 | R.string.dev_mode_toggle__title,
50 | R.string.dev_mode_toggle__desc,
51 | preference = Pair("devMode", Boolean)
52 | )
53 |
54 | val dynamicColor = ChoutenSetting(
55 | R.string.dynamic_colour_toggle__title,
56 | R.string.dynamic_colour_toggle__desc,
57 | preference = Pair("dynamicColor", Boolean),
58 | constraints = { Build.VERSION.SDK_INT >= 31 } // Disable if not on Android 12+
59 | )
60 | val themeType = ChoutenSetting(
61 | R.string.appearance__title,
62 | R.string.appearance__desc,
63 | preference = Pair("themeType", Enum),
64 | )
65 | val dns = ChoutenSetting(
66 | R.string.dns__title,
67 | R.string.dns__desc,
68 | preference = Pair("dns", String),
69 | )
70 | }
71 | }
72 |
73 | object NavigationItems {
74 | val HomePage = BottomNavItem(
75 | name = R.string.navbar_home,
76 | route = "home/",
77 | activeIcon = Icons.Filled.Home,
78 | inactiveIcon = Icons.Outlined.Home,
79 | )
80 | val SearchPage = BottomNavItem(
81 | name = R.string.navbar_search,
82 | route = "search/",
83 | activeIcon = Icons.Filled.Search,
84 | inactiveIcon = Icons.Outlined.Search,
85 | )
86 | val MorePage = BottomNavItem(
87 | name = R.string.navbar_more,
88 | route = "more/",
89 | activeIcon = Icons.Filled.MoreHoriz,
90 | inactiveIcon = Icons.Outlined.MoreHoriz
91 | )
92 | val AppearancePage = BottomNavItem(
93 | name = R.string.appearance__title,
94 | route = "more/appearance",
95 | )
96 | val NetworkPage = BottomNavItem(
97 | name = R.string.network__title,
98 | route = "more/network",
99 | )
100 | val LogPage = BottomNavItem(
101 | name = R.string.settings_submenu_log,
102 | route = "more/log",
103 | )
104 | }
105 |
106 | enum class AppThemeType(val printable: String) {
107 | LIGHT("Light"),
108 | DARK("Dark"),
109 | SYSTEM("System");
110 |
111 | override fun toString(): String {
112 | return this.printable
113 | }
114 | }
115 |
116 | enum class CustomDNS(val printable: String) {
117 | NONE("None"),
118 | CLOUDFLARE("Cloudflare"),
119 | GOOGLE("Google"),
120 | ADGUARD("AdGuard");
121 |
122 | override fun toString(): String {
123 | return this.printable
124 | }
125 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/data/DataLayer.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.data
2 |
3 | import android.util.Log
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import androidx.lifecycle.MutableLiveData
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 |
10 | class DataLayer {
11 | var snackbarQueue: MutableLiveData> =
12 | MutableLiveData(listOf())
13 | private set
14 | private var _snackbarDeleteBuffer = 0
15 |
16 | // Alert Queue uses Flows because we want to be able
17 | // to easily modify it and keep the state in composables
18 | var alertQueue: MutableStateFlow> =
19 | MutableStateFlow(listOf())
20 | private set
21 |
22 | var isNavigationShown by mutableStateOf(true)
23 |
24 | fun enqueueSnackbar(content: SnackbarVisualsWithError) {
25 | if (_snackbarDeleteBuffer > 0) {
26 | // Force it to pop however many need to be
27 | popSnackbarQueue(true)
28 | }
29 |
30 | snackbarQueue.postValue(snackbarQueue.value?.plus(content))
31 | }
32 |
33 | fun popSnackbarQueue(force: Boolean = false) {
34 | if (!force && _snackbarDeleteBuffer < snackbarDeleteBufferMax) {
35 | _snackbarDeleteBuffer += 1; return
36 | }
37 |
38 | val amountToRemove =
39 | if (force) _snackbarDeleteBuffer else snackbarDeleteBufferMax
40 |
41 | if (snackbarQueue.value == null || snackbarQueue.value?.size == 0) return
42 |
43 | // TODO: Fix snackbar.value
44 | // Using postValue causes some weird issues where the snackbar
45 | // doesn't ever actually clear the values off meaning that the messages
46 | // from before are displayed along with the new one.
47 | // Using `.value = <....>` can be an issue because when the app is
48 | // in the background there may not be a state to alter and the
49 | // operation will error out.
50 | try {
51 | snackbarQueue.value =
52 | snackbarQueue.value?.takeLast(snackbarQueue.value!!.size - amountToRemove)
53 | } catch (e: Exception) {
54 | Log.d("CHOUTEN/SNACKBAR", e.localizedMessage ?: "Snackbar Error")
55 | }
56 |
57 | _snackbarDeleteBuffer = 0
58 | }
59 |
60 | fun enqueueAlert(content: AlertData) {
61 | alertQueue.value = alertQueue.value.plus(content)
62 | }
63 |
64 | fun popAlertQueue() {
65 | try {
66 | alertQueue.value = alertQueue.value.drop(1)
67 | } catch (e: Exception) {
68 | Log.d("CHOUTEN/ALERT", e.localizedMessage ?: "Alert Error")
69 | }
70 | }
71 |
72 | companion object {
73 | const val snackbarDeleteBufferMax = 5
74 | }
75 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/data/LogDataLayer.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.data
2 |
3 | import android.util.Log
4 | import androidx.compose.runtime.mutableStateListOf
5 | import com.chouten.app.ModuleLayer
6 | import java.text.SimpleDateFormat
7 | import java.util.Date
8 | import java.util.Locale
9 |
10 | class LogDataLayer {
11 | private var _logEntries = mutableStateListOf()
12 | val logEntries: List
13 | get() = _logEntries
14 |
15 | fun addLogEntry(entry: LogEntry) {
16 | Log.d("CHOUTEN/LOG", entry.message)
17 | _logEntries.add(entry)
18 | }
19 |
20 | fun clearLogs() {
21 | _logEntries.clear()
22 | }
23 | }
24 |
25 | data class LogEntry(
26 | val timestamp: String = SimpleDateFormat(
27 | "HH:mm:ss",
28 | Locale.getDefault()
29 | ).format(Date()),
30 | val title: String,
31 | val module: ModuleModel = ModuleLayer.selectedModule
32 | ?: throw Exception("No module selected"),
33 | val message: String,
34 | val isError: Boolean,
35 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/data/ModuleModel.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.data
2 |
3 | import com.chouten.app.Mapper
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 | import org.json.*
7 |
8 | @Serializable
9 | data class ModuleModel(
10 | var id: String, // The ID provided by the module
11 | // something that needs to be defined within the JSON
12 | val type: String,
13 | val subtypes: List,
14 | var name: String,
15 | val version: String,
16 | val formatVersion: Int,
17 | val updateUrl: String,
18 | @SerialName("general") val meta: ModuleMetaData,
19 | var code: Map?
20 | ) {
21 | @Serializable
22 | data class ModuleMetaData(
23 | val author: String,
24 | val description: String,
25 | var icon: String?,
26 | val lang: List,
27 | @SerialName("baseURL") val baseUrl: String,
28 | @SerialName("bgColor") val backgroundColor: String,
29 | @SerialName("fgColor") val foregroundColor: String,
30 | ) {
31 | override fun toString(): String {
32 | return "{\"author\": \"$author\", \"description\": \"$description\", \"icon\": \"$icon\", \"lang\": ${
33 | lang.map {
34 | "\"$it\""
35 | }
36 | }, \"baseURL\": \"$baseUrl\", \"bgColor\": \"$backgroundColor\", \"fgColor\": \"$foregroundColor\"}"
37 | }
38 | }
39 |
40 | override fun toString(): String {
41 | return "{\"id\": \"$id\", \"type\": \"${type}\", \"subtypes\": ${
42 | subtypes.map {
43 | "\"${it}\""
44 | }
45 | }, \"name\": \"$name\", \"version\": \"$version\", \"formatVersion\": $formatVersion, \"updateUrl\": \"$updateUrl\", \"general\": $meta}"
46 | }
47 |
48 | @Serializable
49 | data class ModuleCode(
50 | val home: List = listOf(),
51 | val search: List = listOf(),
52 | val info: List = listOf(),
53 | val mediaConsume: List = listOf(),
54 | ) {
55 | @Serializable
56 | data class ModuleCodeblock(
57 | val imports: List? = listOf(),
58 | // not set within the JSON
59 | var code: String,
60 | )
61 |
62 | override fun toString(): String {
63 | return "{\"home\": $home, \"search\": $search, \"info\": $info, \"mediaConsume\": $mediaConsume}"
64 | }
65 | }
66 |
67 | override fun hashCode(): Int {
68 | return (
69 | this.type.hashCode()
70 | + this.subtypes.hashCode()
71 | + this.name.hashCode()
72 | + this.version.hashCode()
73 | + this.updateUrl.hashCode()
74 | + this.meta.hashCode()
75 | )
76 | }
77 |
78 | override fun equals(other: Any?): Boolean {
79 | if (this === other) return true
80 | if (javaClass != other?.javaClass) return false
81 |
82 | other as ModuleModel
83 |
84 | if (id != other.id) return false
85 | if (type != other.type) return false
86 | if (subtypes != other.subtypes) return false
87 | if (name != other.name) return false
88 | if (version != other.version) return false
89 | if (updateUrl != other.updateUrl) return false
90 | return meta == other.meta
91 | }
92 | }
93 |
94 | @Serializable
95 | data class ModuleResponse(
96 | val result: T, val action: String? = ""
97 | )
98 |
99 | // TODO: make the action field static (should be action = "error")
100 | // it doesn't stringify correctly when that's the case
101 | @Serializable
102 | data class ErrorAction(
103 | val action: String,
104 | val result: String
105 | )
106 |
107 | @Serializable
108 | data class HTTPAction(
109 | val reqId: String,
110 | val responseText: String
111 | )
112 |
113 | @Serializable
114 | data class ModuleAction(
115 | val action: String? = ""
116 | )
117 |
118 | @Serializable
119 | data class HomeResult(
120 | val type: String, // "Carousel", "list", "grid_2x", "..."
121 | val title: String, // "Spotlight", "Recently released", "..."
122 | val data: List
123 | ) {
124 | @Serializable
125 | data class HomeItem(
126 | val url: String,
127 | val image: String,
128 | val titles: Map,
129 | val indicator: String?,
130 | val current: Int?,
131 | val total: Int?,
132 | val subtitle: String?,
133 | val subtitleValue: List,
134 | val buttonText: String?,
135 | val showIcon: Boolean? = false,
136 | val iconText: String?,
137 | )
138 | }
139 |
140 | @Serializable
141 | data class SearchResult(
142 | val url: String,
143 | val img: String,
144 | val title: String,
145 | val indicatorText: String?,
146 | val currentCount: Int?,
147 | val totalCount: Int?,
148 | )
149 |
150 | @Serializable
151 | data class WebviewPayload(
152 | val query: String,
153 | val action: String
154 | )
155 |
156 | @Serializable
157 | data class BasePayload(
158 | val reqId: String?,
159 | val action: String,
160 | val payload: String
161 | )
162 |
163 | @Serializable
164 | data class HomepagePayload(
165 | val action: String,
166 | )
167 |
168 | @Serializable
169 | data class InfoResult(
170 | val id: String?,
171 | val titles: Titles,
172 | val epListURLs: List,
173 | val altTitles: List?,
174 | val description: String,
175 | val poster: String,
176 | val banner: String?,
177 | val status: String?,
178 | val totalMediaCount: Int?,
179 | val mediaType: String,
180 | val seasons: List?,
181 | val mediaList: List?,
182 | ) {
183 | @Serializable
184 | data class MediaListItem(
185 | val title: String,
186 | val list: List
187 | )
188 |
189 | @Serializable
190 | data class Titles(
191 | val primary: String,
192 | val secondary: String?
193 | )
194 |
195 | @Serializable
196 | data class MediaItem(
197 | val url: String,
198 | val number: Float?,
199 | val title: String?,
200 | val description: String?,
201 | val image: String?,
202 | ) {
203 | override fun toString(): String{
204 | // TODO: refactor
205 | val map = MediaItem(
206 | url = url,
207 | number = number,
208 | title = title,
209 | description = description,
210 | image = image
211 | )
212 |
213 | return Mapper.json.encodeToString(MediaItem.serializer(), map);
214 | }
215 | }
216 |
217 | @Serializable
218 | data class Season(
219 | val name: String,
220 | val url: String,
221 | )
222 | }
223 |
224 | @Serializable
225 | data class WatchResult(
226 | val sources: List,
227 | val subtitles: List,
228 | val skips: List,
229 | val headers: Map,
230 | ) {
231 |
232 | @Serializable
233 | data class ServerData(
234 | val title: String,
235 | val list: List
236 | )
237 |
238 | @Serializable
239 | data class Server(
240 | val name: String,
241 | val url: String,
242 | )
243 |
244 | @Serializable
245 | data class Source(
246 | val file: String,
247 | val type: String,
248 | )
249 |
250 | @Serializable
251 | data class Subtitles(
252 | val url: String,
253 | val language: String,
254 | )
255 |
256 | @Serializable
257 | data class SkipTimes(
258 | val start: Double,
259 | val end: Double,
260 | val type: String
261 | )
262 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/data/SettingsModel.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.data
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.compose.ui.graphics.vector.ImageVector
5 |
6 | data class ChoutenSetting(
7 | @StringRes val text: Int,
8 | @StringRes val secondaryText: Int? = null,
9 | val icon: ImageVector? = null,
10 | val preference: Pair,
11 | val constraints: (() -> Boolean)? = null,
12 | val onToggle: ((Boolean) -> Unit)? = null
13 | )
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/data/SnackbarData.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.data
2 |
3 | import androidx.compose.material3.SnackbarDuration
4 | import androidx.compose.material3.SnackbarVisuals
5 |
6 | data class SnackbarAction(
7 | val actionText: String? = null,
8 | val action: () -> Unit
9 | )
10 |
11 | class SnackbarVisualsWithError(
12 | override val message: String,
13 | val isError: Boolean,
14 | val shouldShowButton: Boolean = false,
15 | val buttonText: String? = null,
16 | val customButton: SnackbarAction? = null
17 | ) : SnackbarVisuals {
18 | override val actionLabel: String
19 | get() = customButton?.actionText ?: if (isError) "Error" else "OK"
20 | override val withDismissAction: Boolean
21 | get() = false
22 | override val duration: SnackbarDuration
23 | get() = if (!shouldShowButton) SnackbarDuration.Indefinite else SnackbarDuration.Short
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/Screen.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui
2 |
3 | import com.chouten.app.data.NavigationItems
4 |
5 | sealed class Screen(val route: String) {
6 | object HomePage : Screen(NavigationItems.HomePage.route)
7 | object SearchPage : Screen(NavigationItems.SearchPage.route)
8 | object MorePage : Screen(NavigationItems.MorePage.route)
9 | object AppearancePage : Screen(NavigationItems.AppearancePage.route)
10 | object NetworkPage : Screen(NavigationItems.NetworkPage.route)
11 | object LogPage : Screen(NavigationItems.LogPage.route)
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/components/Alert.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.components
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.material3.AlertDialog
5 | import androidx.compose.material3.Button
6 | import androidx.compose.material3.Icon
7 | import androidx.compose.material3.Text
8 | import androidx.compose.material3.TextButton
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.saveable.rememberSaveable
12 | import androidx.compose.ui.platform.LocalLifecycleOwner
13 | import androidx.compose.ui.res.stringResource
14 | import androidx.lifecycle.lifecycleScope
15 | import com.chouten.app.PrimaryDataLayer
16 | import com.chouten.app.R
17 | import com.chouten.app.data.AlertData
18 | import kotlinx.coroutines.launch
19 |
20 | @Composable
21 | fun ChoutenAlert(alert: AlertData) {
22 |
23 | val isShown = rememberSaveable {
24 | mutableStateOf(true)
25 | }
26 |
27 | val defaultDismiss = {
28 | PrimaryDataLayer.popAlertQueue()
29 | isShown.value = false
30 | }
31 |
32 | val owner = LocalLifecycleOwner.current
33 |
34 | AnimatedVisibility(visible = isShown.value) {
35 | AlertDialog(onDismissRequest = {
36 | alert.cancelButtonAction?.invoke()
37 | defaultDismiss()
38 | }, icon = {
39 | if (alert.icon != null) {
40 | Icon(imageVector = alert.icon, contentDescription = null)
41 | }
42 | },
43 | title = {
44 | Text(text = alert.title)
45 | }, text = {
46 | Text(text = alert.message)
47 | }, confirmButton = {
48 | Button(
49 | onClick = {
50 | alert.confirmButtonAction?.invoke()
51 | defaultDismiss()
52 | },
53 | ) {
54 | Text(
55 | text = alert.confirmButtonText
56 | ?: stringResource(id = R.string.ok)
57 | )
58 | }
59 | }, dismissButton = {
60 | if (alert.cancelButtonText != null && alert.cancelButtonAction != null) {
61 | TextButton(
62 | onClick = {
63 | owner.lifecycleScope.launch {
64 | alert.cancelButtonAction.invoke()
65 | defaultDismiss()
66 | }
67 | },
68 | ) {
69 | Text(text = alert.cancelButtonText)
70 | }
71 | }
72 | })
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/components/Banners.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.components
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.compose.animation.AnimatedVisibility
5 | import androidx.compose.animation.expandVertically
6 | import androidx.compose.animation.shrinkVertically
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.Spacer
11 | import androidx.compose.foundation.layout.WindowInsets
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.layout.requiredSize
15 | import androidx.compose.foundation.layout.statusBars
16 | import androidx.compose.foundation.layout.width
17 | import androidx.compose.foundation.layout.windowInsetsPadding
18 | import androidx.compose.material3.CircularProgressIndicator
19 | import androidx.compose.material3.MaterialTheme
20 | import androidx.compose.material3.Text
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.runtime.getValue
23 | import androidx.compose.runtime.mutableStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.runtime.setValue
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.layout.SubcomposeLayout
28 | import androidx.compose.ui.platform.LocalDensity
29 | import androidx.compose.ui.res.stringResource
30 | import androidx.compose.ui.text.style.TextAlign
31 | import androidx.compose.ui.unit.Dp
32 | import androidx.compose.ui.unit.dp
33 |
34 | import androidx.compose.ui.util.fastForEach
35 | import androidx.compose.ui.util.fastMap
36 | import androidx.compose.ui.util.fastMaxBy
37 |
38 | val DownloadedOnlyBannerBackgroundColor
39 | @Composable get() = MaterialTheme.colorScheme.tertiary
40 | val IncognitoModeBannerBackgroundColor
41 | @Composable get() = MaterialTheme.colorScheme.primary
42 | val IndexingBannerBackgroundColor
43 | @Composable get() = MaterialTheme.colorScheme.secondary
44 |
45 | @Composable
46 | fun WarningBanner(
47 | @StringRes textRes: Int,
48 | modifier: Modifier = Modifier,
49 | topPadding: Dp = 0.dp
50 | ) {
51 | Text(
52 | text = stringResource(textRes),
53 | modifier = modifier
54 | .fillMaxWidth()
55 | .background(MaterialTheme.colorScheme.error)
56 | .padding(top = topPadding + 8.dp, bottom = 8.dp),
57 | color = MaterialTheme.colorScheme.onError,
58 | style = MaterialTheme.typography.bodyMedium,
59 | textAlign = TextAlign.Center,
60 | )
61 | }
62 |
63 |
64 | @Composable
65 | fun AppStateBanners(
66 | downloadedOnlyMode: Boolean,
67 | incognitoMode: Boolean,
68 | indexing: Boolean,
69 | modifier: Modifier = Modifier,
70 | ) {
71 | val density = LocalDensity.current
72 | val mainInsets = WindowInsets.statusBars
73 | val mainInsetsTop = mainInsets.getTop(density)
74 | SubcomposeLayout(modifier = modifier) { constraints ->
75 | val indexingPlaceable = subcompose(0) {
76 | AnimatedVisibility(
77 | visible = indexing,
78 | enter = expandVertically(),
79 | exit = shrinkVertically(),
80 | ) {
81 | IndexingDownloadBanner(
82 | modifier = Modifier.windowInsetsPadding(mainInsets),
83 | )
84 | }
85 | }.fastMap { it.measure(constraints) }
86 | val indexingHeight = indexingPlaceable.fastMaxBy { it.height }?.height ?: 0
87 |
88 | val downloadedOnlyPlaceable = subcompose(1) {
89 | AnimatedVisibility(
90 | visible = downloadedOnlyMode,
91 | enter = expandVertically(),
92 | exit = shrinkVertically(),
93 | ) {
94 | val top = (mainInsetsTop - indexingHeight).coerceAtLeast(0)
95 | DownloadedOnlyModeBanner(
96 | modifier = Modifier.windowInsetsPadding(WindowInsets(top = top)),
97 | )
98 | }
99 | }.fastMap { it.measure(constraints) }
100 | val downloadedOnlyHeight = downloadedOnlyPlaceable.fastMaxBy { it.height }?.height ?: 0
101 |
102 | val incognitoPlaceable = subcompose(2) {
103 | AnimatedVisibility(
104 | visible = incognitoMode,
105 | enter = expandVertically(),
106 | exit = shrinkVertically(),
107 | ) {
108 | val top = (mainInsetsTop - indexingHeight - downloadedOnlyHeight).coerceAtLeast(0)
109 | IncognitoModeBanner(
110 | modifier = Modifier.windowInsetsPadding(WindowInsets(top = top)),
111 | )
112 | }
113 | }.fastMap { it.measure(constraints) }
114 | val incognitoHeight = incognitoPlaceable.fastMaxBy { it.height }?.height ?: 0
115 |
116 | layout(constraints.maxWidth, indexingHeight + downloadedOnlyHeight + incognitoHeight) {
117 | indexingPlaceable.fastForEach {
118 | it.place(0, 0)
119 | }
120 | downloadedOnlyPlaceable.fastForEach {
121 | it.place(0, indexingHeight)
122 | }
123 | incognitoPlaceable.fastForEach {
124 | it.place(0, indexingHeight + downloadedOnlyHeight)
125 | }
126 | }
127 | }
128 | }
129 |
130 |
131 | @Composable
132 | private fun DownloadedOnlyModeBanner(modifier: Modifier = Modifier) {
133 | Text(
134 | text = "Downloaded only mode",
135 | modifier = Modifier
136 | .background(DownloadedOnlyBannerBackgroundColor)
137 | .fillMaxWidth()
138 | .padding(4.dp)
139 | .then(modifier),
140 | color = MaterialTheme.colorScheme.onTertiary,
141 | textAlign = TextAlign.Center,
142 | style = MaterialTheme.typography.labelMedium,
143 | )
144 | }
145 |
146 | @Composable
147 | private fun IncognitoModeBanner(modifier: Modifier = Modifier) {
148 | Text(
149 | text = "Incognito mode",
150 | modifier = Modifier
151 | .background(IncognitoModeBannerBackgroundColor)
152 | .fillMaxWidth()
153 | .padding(4.dp)
154 | .then(modifier),
155 | color = MaterialTheme.colorScheme.onPrimary,
156 | textAlign = TextAlign.Center,
157 | style = MaterialTheme.typography.labelMedium,
158 | )
159 | }
160 |
161 | @Composable
162 | private fun IndexingDownloadBanner(modifier: Modifier = Modifier) {
163 | val density = LocalDensity.current
164 | Row(
165 | modifier = Modifier
166 | .background(color = IndexingBannerBackgroundColor)
167 | .fillMaxWidth()
168 | .padding(8.dp)
169 | .then(modifier),
170 | horizontalArrangement = Arrangement.Center,
171 | ) {
172 | var textHeight by remember { mutableStateOf(0.dp) }
173 | CircularProgressIndicator(
174 | modifier = Modifier.requiredSize(textHeight),
175 | color = MaterialTheme.colorScheme.onSecondary,
176 | strokeWidth = textHeight / 8,
177 | )
178 | Spacer(modifier = Modifier.width(8.dp))
179 | Text(
180 | text = "Indexing...",
181 | color = MaterialTheme.colorScheme.onSecondary,
182 | textAlign = TextAlign.Center,
183 | style = MaterialTheme.typography.labelMedium,
184 | onTextLayout = {
185 | with(density) {
186 | textHeight = it.size.height.toDp()
187 | }
188 | },
189 | )
190 | }
191 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/components/Divider.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.fillMaxHeight
6 | import androidx.compose.foundation.layout.heightIn
7 | import androidx.compose.foundation.layout.width
8 | import androidx.compose.material3.DividerDefaults
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.platform.LocalDensity
13 | import androidx.compose.ui.unit.Dp
14 | import androidx.compose.ui.unit.dp
15 |
16 | /**
17 | * A vertical variation of the Material Design divider.
18 | *
19 | * A divider is a thin line that groups content in lists and layouts.
20 | *
21 | * 
22 | *
23 | * @param modifier the [Modifier] to be applied to this divider line.
24 | * @param thickness thickness of this divider line. Using [Dp.Hairline] will produce a single pixel
25 | * divider regardless of screen density.
26 | * @param color color of this divider line.
27 | * @param height the maximum height of this divider.
28 | */
29 | @Composable
30 | fun VerticalDivider(
31 | modifier: Modifier = Modifier,
32 | thickness: Dp = DividerDefaults.Thickness,
33 | color: Color = DividerDefaults.color,
34 | height: Dp = Dp.Infinity
35 | ) {
36 | val targetThickness = if (thickness == Dp.Hairline) {
37 | (1f / LocalDensity.current.density).dp
38 | } else {
39 | thickness
40 | }
41 | Box(
42 | modifier
43 | .heightIn(max = height)
44 | .fillMaxHeight()
45 | .width(targetThickness)
46 | .background(color = color)
47 | )
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/components/EpisodeItem.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.fillMaxHeight
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.foundation.layout.heightIn
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.layout.width
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.Text
16 | import androidx.compose.material3.surfaceColorAtElevation
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.draw.clip
21 | import androidx.compose.ui.layout.ContentScale
22 | import androidx.compose.ui.text.font.FontWeight
23 | import androidx.compose.ui.text.style.TextOverflow
24 | import androidx.compose.ui.unit.dp
25 | import androidx.compose.ui.unit.sp
26 | import com.chouten.app.data.InfoResult
27 | import com.skydoves.landscapist.ImageOptions
28 | import com.skydoves.landscapist.glide.GlideImage
29 | import com.valentinilk.shimmer.shimmer
30 |
31 | @Composable
32 | fun EpisodeItem(
33 | item: InfoResult.MediaItem,
34 | imageAlternative: String,
35 | modifier: Modifier = Modifier
36 | ) {
37 | Column(
38 | Modifier
39 | .fillMaxWidth()
40 | .clip(MaterialTheme.shapes.medium)
41 | .background(
42 | MaterialTheme.colorScheme.surfaceColorAtElevation(
43 | 3.dp
44 | )
45 | )
46 | .then(modifier)
47 | ) {
48 | Row(
49 | horizontalArrangement = Arrangement.spacedBy(
50 | 12.dp
51 | )
52 | ) {
53 | GlideImage(
54 | modifier = Modifier
55 | .width(160.dp)
56 | .height(90.dp)
57 | .clip(MaterialTheme.shapes.medium),
58 | imageModel = {
59 | item.image ?: imageAlternative
60 | },
61 | imageOptions = ImageOptions(
62 | contentScale = ContentScale.Crop,
63 | alignment = Alignment.Center,
64 | contentDescription = "${item.title} Thumbnail",
65 | ),
66 | loading = {
67 | Box(
68 | Modifier
69 | .shimmer()
70 | .matchParentSize()
71 | .background(MaterialTheme.colorScheme.onSurface)
72 | )
73 | },
74 | )
75 | Column(Modifier.heightIn(max = 90.dp)) {
76 | Column(
77 | modifier = Modifier.fillMaxHeight(
78 | 0.7F
79 | ),
80 | verticalArrangement = Arrangement.SpaceAround
81 | ) {
82 | Text(
83 | item.title ?: "Episode 1",
84 | color = MaterialTheme.colorScheme.onSurface,
85 | fontSize = 15.sp,
86 | lineHeight = 16.sp,
87 | fontWeight = FontWeight.SemiBold,
88 | maxLines = 2,
89 | overflow = TextOverflow.Ellipsis
90 | )
91 | }
92 | Row(
93 | horizontalArrangement = Arrangement.SpaceBetween,
94 | modifier = Modifier
95 | .fillMaxWidth()
96 | .padding(
97 | bottom = 6.dp, end = 6.dp
98 | )
99 | ) {
100 | Text(
101 | "Episode ${
102 | item.number.toString()
103 | .substringBefore(".0")
104 | }",
105 | color = MaterialTheme.colorScheme.onSurface.copy(
106 | 0.7F
107 | ),
108 | fontSize = 12.sp,
109 | fontWeight = FontWeight.SemiBold
110 | )
111 |
112 | Text(
113 | "24 mins",
114 | color = MaterialTheme.colorScheme.onSurface.copy(
115 | 0.7F
116 | ),
117 | fontSize = 12.sp,
118 | fontWeight = FontWeight.SemiBold
119 | )
120 | }
121 | }
122 | }
123 | if (item.description?.isNotBlank() == true) {
124 | Text(
125 | "${item.description}",
126 | color = MaterialTheme.colorScheme.onSurface.copy(
127 | 0.7F
128 | ),
129 | fontSize = 12.sp,
130 | lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
131 | maxLines = 4,
132 | overflow = TextOverflow.Ellipsis,
133 | modifier = Modifier.padding(all = 12.dp)
134 | )
135 | }
136 | }
137 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/components/ListAnimation.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.components
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.animation.AnimatedVisibility
5 | import androidx.compose.animation.EnterTransition
6 | import androidx.compose.animation.ExitTransition
7 | import androidx.compose.animation.core.MutableTransitionState
8 | import androidx.compose.animation.expandVertically
9 | import androidx.compose.animation.shrinkVertically
10 | import androidx.compose.foundation.lazy.LazyItemScope
11 | import androidx.compose.foundation.lazy.LazyListScope
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.LaunchedEffect
14 | import androidx.compose.runtime.State
15 | import androidx.compose.runtime.mutableStateOf
16 | import androidx.compose.runtime.remember
17 | import androidx.recyclerview.widget.DiffUtil
18 | import androidx.recyclerview.widget.ListUpdateCallback
19 | import kotlinx.coroutines.Dispatchers
20 | import kotlinx.coroutines.withContext
21 |
22 | @SuppressLint("ComposableNaming", "UnusedTransitionTargetStateParameter")
23 | /**
24 | * @param state Use [updateAnimatedItemsState].
25 | */
26 | inline fun LazyListScope.animatedItemsIndexed(
27 | state: List>,
28 | enterTransition: EnterTransition = expandVertically(),
29 | exitTransition: ExitTransition = shrinkVertically(),
30 | noinline key: ((item: T) -> Any)? = null,
31 | crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
32 | ) {
33 | items(
34 | state.size,
35 | if (key != null) { keyIndex: Int -> key(state[keyIndex].item) } else null
36 | ) { index ->
37 | val item = state[index]
38 | val visibility = item.visibility
39 | androidx.compose.runtime.key(key?.invoke(item.item)) {
40 | AnimatedVisibility(
41 | visibleState = visibility,
42 | enter = enterTransition,
43 | exit = exitTransition
44 | ) {
45 | itemContent(index, item.item)
46 | }
47 | }
48 | }
49 | }
50 |
51 | @Composable
52 | fun updateAnimatedItemsState(
53 | newList: List
54 | ): State>> {
55 | val state = remember { mutableStateOf(emptyList>()) }
56 | LaunchedEffect(newList) {
57 | if (state.value == newList) {
58 | return@LaunchedEffect
59 | }
60 | val oldList = state.value.toList()
61 | val diffCb = object : DiffUtil.Callback() {
62 | override fun getOldListSize(): Int = oldList.size
63 | override fun getNewListSize(): Int = newList.size
64 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
65 | oldList[oldItemPosition].item == newList[newItemPosition]
66 |
67 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
68 | oldList[oldItemPosition].item == newList[newItemPosition]
69 | }
70 | val diffResult = calculateDiff(false, diffCb)
71 | val compositeList = oldList.toMutableList()
72 | diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
73 | override fun onInserted(position: Int, count: Int) {
74 | for (i in 0 until count) {
75 | val newItem = AnimatedItem(
76 | visibility = MutableTransitionState(false),
77 | newList[position + i]
78 | )
79 | newItem.visibility.targetState = true
80 | compositeList.add(position + i, newItem)
81 | }
82 | }
83 |
84 | override fun onRemoved(position: Int, count: Int) {
85 | for (i in 0 until count) {
86 | compositeList[position + i].visibility.targetState = false
87 | }
88 | }
89 |
90 | override fun onMoved(fromPosition: Int, toPosition: Int) {
91 | // not detecting moves.
92 | }
93 |
94 | override fun onChanged(position: Int, count: Int, payload: Any?) {
95 | // irrelevant with compose.
96 | }
97 | })
98 | if (state.value != compositeList) state.value = compositeList
99 |
100 | val initialAnimation = androidx.compose.animation.core.Animatable(1.0f)
101 | initialAnimation.animateTo(0f)
102 | state.value = state.value.filter { it.visibility.targetState }
103 | }
104 | return state
105 | }
106 |
107 | data class AnimatedItem(
108 | val visibility: MutableTransitionState,
109 | val item: T,
110 | ) {
111 | override fun hashCode(): Int {
112 | return item?.hashCode() ?: 0
113 | }
114 |
115 | override fun equals(other: Any?): Boolean {
116 | if (this === other) return true
117 | if (javaClass != other?.javaClass) return false
118 | other as AnimatedItem<*>
119 | return item == other.item
120 | }
121 | }
122 |
123 | suspend fun calculateDiff(
124 | detectMoves: Boolean = true,
125 | diffCb: DiffUtil.Callback
126 | ): DiffUtil.DiffResult {
127 | return withContext(Dispatchers.Unconfined) {
128 | DiffUtil.calculateDiff(diffCb, detectMoves)
129 | }
130 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/components/SegmentedControl.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.components
2 |
3 | import androidx.compose.animation.animateContentSize
4 | import androidx.compose.foundation.BorderStroke
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.offset
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.width
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.filled.Check
14 | import androidx.compose.material3.ButtonDefaults
15 | import androidx.compose.material3.Icon
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.OutlinedButton
18 | import androidx.compose.material3.Text
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.getValue
21 | import androidx.compose.runtime.mutableStateOf
22 | import androidx.compose.runtime.remember
23 | import androidx.compose.runtime.setValue
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.graphics.Color
27 | import androidx.compose.ui.layout.onGloballyPositioned
28 | import androidx.compose.ui.platform.LocalDensity
29 | import androidx.compose.ui.text.font.FontWeight
30 | import androidx.compose.ui.unit.Dp
31 | import androidx.compose.ui.unit.dp
32 | import androidx.compose.ui.zIndex
33 |
34 | /**
35 | * - `modifier` : modifier to be applied to the control (Optional)
36 | * - `items` : list of items to be rendered
37 | * - `defaultSelectedItemIndex` : to highlight item by default (Optional)
38 | * - `useFixedWidth` : set true if you want to set fix width to item (Optional)
39 | * - `itemWidth` : Provide item width if useFixedWidth is set to true (Optional)
40 | * - `cornerRadius` : To make control as rounded (Optional)
41 | * - `color` : Set color to control (Optional)
42 | * - `onItemSelection` : Get selected item index
43 | */
44 | @Composable
45 | fun SegmentedControl(
46 | modifier: Modifier = Modifier,
47 | items: List,
48 | defaultSelectedItemIndex: Int = 0,
49 | useFixedWidth: Boolean = false,
50 | itemWidth: Dp = 120.dp,
51 | cornerRadius: Int = 10,
52 | color: Color = MaterialTheme.colorScheme.onPrimaryContainer,
53 | onItemSelection: (selectedItemIndex: Int) -> Unit
54 | ) {
55 | val selectedIndex = remember { mutableStateOf(defaultSelectedItemIndex) }
56 | val localDensity = LocalDensity.current
57 | // Create element height in dp state
58 | var rowWidthDp by remember {
59 | mutableStateOf(0.dp)
60 | }
61 |
62 | Row(
63 | modifier = modifier
64 | .onGloballyPositioned { coordinates ->
65 | // Set column height using the LayoutCoordinates
66 | rowWidthDp = with(localDensity) { coordinates.size.width.toDp() }
67 | }
68 | .padding(horizontal = 5.dp)
69 | .fillMaxWidth(),
70 | ) {
71 | items.forEachIndexed { index, item ->
72 | OutlinedButton(
73 | modifier = when (index) {
74 | 0 -> {
75 | if (useFixedWidth) {
76 | Modifier
77 | .width(itemWidth)
78 | .offset(0.dp, 0.dp)
79 | .zIndex(if (selectedIndex.value == index) 1f else 0f)
80 | } else {
81 | Modifier
82 | .width(rowWidthDp / items.size)
83 | .offset(0.dp, 0.dp)
84 | .zIndex(if (selectedIndex.value == index) 1f else 0f)
85 | }
86 | }
87 |
88 | else -> {
89 | if (useFixedWidth) Modifier
90 | .width(itemWidth)
91 | .offset((-1 * index).dp, 0.dp)
92 | .zIndex(if (selectedIndex.value == index) 1f else 0f)
93 | else Modifier
94 | .width(rowWidthDp / items.size)
95 | .offset((-1 * index).dp, 0.dp)
96 | .zIndex(if (selectedIndex.value == index) 1f else 0f)
97 | }
98 | },
99 | onClick = {
100 | selectedIndex.value = index
101 | onItemSelection(selectedIndex.value)
102 | },
103 | shape = when (index) {
104 | /**
105 | * left outer button
106 | */
107 | 0 -> RoundedCornerShape(
108 | topStartPercent = cornerRadius,
109 | topEndPercent = 0,
110 | bottomStartPercent = cornerRadius,
111 | bottomEndPercent = 0
112 | )
113 | /**
114 | * right outer button
115 | */
116 | items.size - 1 -> RoundedCornerShape(
117 | topStartPercent = 0,
118 | topEndPercent = cornerRadius,
119 | bottomStartPercent = 0,
120 | bottomEndPercent = cornerRadius
121 | )
122 | /**
123 | * middle button
124 | */
125 | else -> RoundedCornerShape(
126 | topStartPercent = 0,
127 | topEndPercent = 0,
128 | bottomStartPercent = 0,
129 | bottomEndPercent = 0
130 | )
131 | },
132 | border = BorderStroke(
133 | 1.dp, if (selectedIndex.value == index) color else color.copy(alpha = 0.75f)
134 | ),
135 | colors = if (selectedIndex.value == index) {
136 | /**
137 | * selected colors
138 | */
139 | ButtonDefaults.outlinedButtonColors(
140 | containerColor = color
141 | )
142 | } else {
143 | /**
144 | * not selected colors
145 | */
146 | ButtonDefaults.outlinedButtonColors(containerColor = Color.Transparent)
147 | },
148 | ) {
149 | Row(
150 | modifier = Modifier
151 | .fillMaxWidth()
152 | .animateContentSize(),
153 | verticalAlignment = Alignment.CenterVertically,
154 | horizontalArrangement = Arrangement.SpaceEvenly
155 | ) {
156 | if (selectedIndex.value == index) {
157 | Icon(
158 | Icons.Default.Check,
159 | contentDescription = "Use $item as selected item",
160 | tint = MaterialTheme.colorScheme.inverseOnSurface
161 | )
162 | }
163 | Text(
164 | text = item,
165 | fontWeight = FontWeight.Normal,
166 | color = if (selectedIndex.value == index) MaterialTheme.colorScheme.inverseOnSurface else color.copy(
167 | alpha = 0.9f
168 | ),
169 | )
170 | }
171 | }
172 | }
173 | }
174 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/components/SettingsComponents.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.components
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.foundation.LocalIndication
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.interaction.MutableInteractionSource
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.Spacer
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.layout.width
15 | import androidx.compose.material.icons.Icons
16 | import androidx.compose.material.icons.filled.ArrowBack
17 | import androidx.compose.material3.AlertDialog
18 | import androidx.compose.material3.ExperimentalMaterial3Api
19 | import androidx.compose.material3.Icon
20 | import androidx.compose.material3.IconButton
21 | import androidx.compose.material3.ListItem
22 | import androidx.compose.material3.MaterialTheme
23 | import androidx.compose.material3.RadioButton
24 | import androidx.compose.material3.Scaffold
25 | import androidx.compose.material3.Switch
26 | import androidx.compose.material3.Text
27 | import androidx.compose.material3.TextButton
28 | import androidx.compose.material3.TopAppBar
29 | import androidx.compose.runtime.Composable
30 | import androidx.compose.runtime.getValue
31 | import androidx.compose.runtime.mutableStateOf
32 | import androidx.compose.runtime.remember
33 | import androidx.compose.runtime.saveable.rememberSaveable
34 | import androidx.compose.runtime.setValue
35 | import androidx.compose.ui.Alignment
36 | import androidx.compose.ui.Modifier
37 | import androidx.compose.ui.res.stringResource
38 | import androidx.compose.ui.unit.dp
39 | import androidx.navigation.NavController
40 | import com.chouten.app.R
41 | import com.chouten.app.data.ChoutenSetting
42 |
43 | @Composable
44 | fun SettingsToggle(
45 | preference: ChoutenSetting,
46 | modifier: Modifier = Modifier,
47 | onCheckedChange: ((Boolean) -> Unit),
48 | defaultValue: Boolean = false
49 | ) {
50 | var toggleState by rememberSaveable { mutableStateOf(defaultValue) }
51 | val interactionSource = remember { MutableInteractionSource() }
52 |
53 | SettingsItem(
54 | modifier = modifier.clickable(
55 | interactionSource = interactionSource,
56 | indication = LocalIndication.current
57 | ) {
58 | preference.onToggle?.invoke(!toggleState)
59 | onCheckedChange.invoke(!toggleState)
60 | toggleState = !toggleState
61 | },
62 | icon = { preference.icon?.let { Icon(it, stringResource(preference.text)) } },
63 | text = { Text(stringResource(preference.text)) },
64 | secondaryText = { preference.secondaryText?.let { Text(stringResource(it)) } }
65 | ) {
66 | Switch(
67 | enabled = preference.constraints?.let { it() } != false,
68 | checked = toggleState,
69 | onCheckedChange = {
70 | preference.onToggle?.invoke(toggleState)
71 | onCheckedChange.invoke(it)
72 | toggleState = it
73 | },
74 | interactionSource = interactionSource
75 | )
76 | }
77 | }
78 |
79 | @Composable
80 | inline fun > SettingsChoice(
81 | preference: ChoutenSetting,
82 | modifier: Modifier = Modifier,
83 | crossinline onPreferenceChange: (T) -> Unit,
84 | crossinline onPreviewSelectionChange: (T) -> Unit = {},
85 | defaultValue: T
86 | ) {
87 | var isOpen by rememberSaveable {
88 | mutableStateOf(false)
89 | }
90 |
91 | SettingsItem(modifier.clickable {
92 | isOpen = true
93 | },
94 | { preference.icon?.let { Icon(it, stringResource(preference.text)) } },
95 | { Text(stringResource(preference.text)) },
96 | { preference.secondaryText?.let { Text(stringResource(it)) } }) {
97 | SettingsChoicePopup(visible = isOpen,
98 | title = { Text(text = stringResource(preference.text)) },
99 | defaultValue = defaultValue,
100 | onClose = { isOpen = false },
101 | onSelection = { onPreferenceChange(it); isOpen = false },
102 | onPreviewSelection = { onPreviewSelectionChange(it) })
103 | }
104 | }
105 |
106 | @Composable
107 | inline fun > SettingsChoicePopup(
108 | visible: Boolean,
109 | noinline title: @Composable () -> Unit,
110 | defaultValue: T,
111 | noinline onClose: () -> Unit,
112 | noinline onSelection: (T) -> Unit,
113 | noinline onPreviewSelection: (T) -> Unit = {},
114 | ) {
115 | var selected by rememberSaveable { mutableStateOf(defaultValue) }
116 | AnimatedVisibility(visible = visible) {
117 | AlertDialog(onDismissRequest = {
118 | onClose()
119 | if (selected != defaultValue) {
120 | selected = defaultValue
121 | onSelection(defaultValue)
122 | }
123 | }, title = title, text = {
124 | Column {
125 | enumValues().forEach { e ->
126 | Row(
127 | Modifier
128 | .fillMaxWidth()
129 | .clickable {
130 | selected = e
131 | onPreviewSelection(e)
132 | }, verticalAlignment = Alignment.CenterVertically
133 | ) {
134 | RadioButton(selected = e == selected, onClick = {
135 | selected = e
136 | onPreviewSelection(e)
137 | })
138 | Spacer(Modifier.width(8.dp))
139 | Text(
140 | e.toString(),
141 | style = MaterialTheme.typography.bodyLarge
142 | )
143 | }
144 | }
145 | }
146 | }, confirmButton = {
147 | TextButton(onClick = { onClose(); onSelection(selected) }) {
148 | Text(stringResource(R.string.confirm))
149 | }
150 | })
151 | }
152 | }
153 |
154 | @Composable
155 | fun SettingsItem(
156 | modifier: Modifier = Modifier,
157 | icon: @Composable (() -> Unit)? = null,
158 | text: @Composable () -> Unit,
159 | secondaryText: @Composable (() -> Unit) = { },
160 | trailing: @Composable (() -> Unit) = { },
161 | ) {
162 | ListItem(
163 | modifier = modifier,
164 | leadingContent = icon,
165 | headlineContent = text,
166 | supportingContent = secondaryText,
167 | trailingContent = trailing,
168 | )
169 | }
170 |
171 | @OptIn(ExperimentalMaterial3Api::class)
172 | @Composable
173 | fun Subpage(
174 | title: String,
175 | navController: NavController,
176 | content: @Composable () -> Unit
177 | ) {
178 | Scaffold(topBar = {
179 | TopAppBar(title = {
180 | Row(
181 | verticalAlignment = Alignment.CenterVertically,
182 | ) {
183 | IconButton(onClick = {
184 | navController.popBackStack()
185 | }) {
186 | Icon(
187 | imageVector = Icons.Filled.ArrowBack,
188 | contentDescription = stringResource(R.string.back)
189 | )
190 | }
191 | Text(title)
192 | }
193 | })
194 | }) { scaffoldPadding ->
195 | Box(
196 | Modifier
197 | .padding(scaffoldPadding)
198 | .fillMaxSize()
199 | ) {
200 | content()
201 | }
202 | }
203 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/components/Snackbar.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.components
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.Close
6 | import androidx.compose.material3.ButtonDefaults
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.SnackbarHost
10 | import androidx.compose.material3.SnackbarHostState
11 | import androidx.compose.material3.Text
12 | import androidx.compose.material3.TextButton
13 | import androidx.compose.material3.surfaceColorAtElevation
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.graphics.Color
17 | import androidx.compose.ui.platform.LocalLifecycleOwner
18 | import androidx.compose.ui.unit.dp
19 | import androidx.lifecycle.lifecycleScope
20 | import com.chouten.app.PrimaryDataLayer
21 | import com.chouten.app.data.SnackbarVisualsWithError
22 | import com.chouten.app.ui.theme.shapes
23 | import kotlinx.coroutines.launch
24 | import androidx.compose.material3.Snackbar as MaterialSnackbar
25 |
26 | @Composable
27 | fun Snackbar() {
28 | val snackbarState = SnackbarHostState()
29 |
30 | // Observe the Flow of the Snackbar Queue
31 | val owner = LocalLifecycleOwner.current
32 | PrimaryDataLayer.snackbarQueue.observe(owner) {
33 | it.iterator().forEach { snackbarItem ->
34 | owner.lifecycleScope.launch {
35 | snackbarState.showSnackbar(
36 | snackbarItem
37 | )
38 | }
39 | PrimaryDataLayer.popSnackbarQueue()
40 | }
41 | }
42 |
43 | SnackbarHost(hostState = snackbarState) { data ->
44 | val extendedVisuals = data.visuals as? SnackbarVisualsWithError
45 | val isError =
46 | extendedVisuals?.isError
47 | ?: false
48 | val buttonColor = if (isError) {
49 | ButtonDefaults.buttonColors(
50 | containerColor = MaterialTheme.colorScheme.error,
51 | contentColor = MaterialTheme.colorScheme.onError
52 | )
53 | } else {
54 | ButtonDefaults.buttonColors(
55 | containerColor = Color.Transparent,
56 | contentColor = MaterialTheme.colorScheme.primary
57 | )
58 | }
59 |
60 | MaterialSnackbar(
61 | modifier = Modifier.padding(12.dp),
62 | containerColor = if (!isError) {
63 | MaterialTheme.colorScheme.surfaceColorAtElevation(
64 | 10.dp
65 | )
66 | } else {
67 | MaterialTheme.colorScheme.error
68 | },
69 | contentColor = if (!isError) {
70 | MaterialTheme.colorScheme.onSurface
71 | } else {
72 | MaterialTheme.colorScheme.onError
73 | },
74 | action = {
75 | TextButton(
76 | onClick = {
77 | extendedVisuals?.customButton?.action?.invoke()
78 | ?: if (isError) data.dismiss() else data.performAction()
79 | },
80 | shape = shapes.extraSmall,
81 | colors = buttonColor
82 | ) {
83 | extendedVisuals?.customButton?.actionText?.let {
84 | Text(
85 | it
86 | )
87 | } ?: extendedVisuals?.buttonText?.let {
88 | Text(
89 | it
90 | )
91 | } ?: Icon(Icons.Default.Close, "Dismiss")
92 | }
93 | }
94 | ) {
95 | Text(
96 | text = data.visuals.message,
97 | maxLines = 8
98 | )
99 | }
100 | }
101 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/theme/Colors.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val md_light_primary = Color(0xFF31628D)
6 | val md_light_onPrimary = Color(0xFFFFFFFF)
7 | val md_light_primaryContainer = Color(0xFFCFE5FF)
8 | val md_light_onPrimaryContainer = Color(0xFF001D34)
9 | val md_light_inversePrimary = Color(0xFF99CBFF)
10 | val md_light_secondary = Color(0xFF515E7D)
11 | val md_light_onSecondary = Color(0xFFFFFFFF)
12 | val md_light_secondaryContainer = Color(0xFFD5E4F7)
13 | val md_light_onSecondaryContainer = Color(0xFF0E1D2A)
14 | val md_light_tertiary = Color(0xFF695779)
15 | val md_light_onTertiary = Color(0xFFFFFFFF)
16 | val md_light_tertiaryContainer = Color(0xFFF0DBFF)
17 | val md_light_onTertiaryContainer = Color(0xFF231533)
18 | val md_light_background = Color(0xFFFCFCFF)
19 | val md_light_onBackground = Color(0xFF1A1C1E)
20 | val md_light_surface = Color(0xFFFCFCFF)
21 | val md_light_onSurface = Color(0xFF1A1C1E)
22 | val md_light_surfaceVariant = Color(0xFFDEE3EB)
23 | val md_light_onSurfaceVariant = Color(0xFF42474E)
24 | val md_light_surfaceTint = Color(0xFF31628D)
25 | val md_light_inverseSurface = Color(0xFF2F3033)
26 | val md_light_inverseOnSurface = Color(0xFFF1F0F4)
27 | val md_light_error = Color(0xFFB3261E)
28 | val md_light_onError = Color(0xFFFFFFFF)
29 | val md_light_errorContainer = Color(0xFFF9DEDC)
30 | val md_light_onErrorContainer = Color(0xFF410E0B)
31 | val md_light_outline = Color(0xFF71767E)
32 | val md_light_outlineVariant = Color(0xFFCAC4D0)
33 | val md_light_scrim = Color(0xFF000000)
34 |
35 | val md_dark_primary = Color(0xFF9DCBFB)
36 | val md_dark_onPrimary = Color(0xFF003354)
37 | val md_dark_primaryContainer = Color(0xFF124A73)
38 | val md_dark_onPrimaryContainer = Color(0xFFCFE5FF)
39 | val md_dark_inversePrimary = Color(0xFF31628D)
40 | val md_dark_secondary = Color(0xFFB9C8DA)
41 | val md_dark_onSecondary = Color(0xFF243240)
42 | val md_dark_secondaryContainer = Color(0xFF3A4857)
43 | val md_dark_onSecondaryContainer = Color(0xFFD5E4F7)
44 | val md_dark_tertiary = Color(0xFFD4BEE6)
45 | val md_dark_onTertiary = Color(0xFF392A49)
46 | val md_dark_tertiaryContainer = Color(0xFF504060)
47 | val md_dark_onTertiaryContainer = Color(0xFFF0DBFF)
48 | val md_dark_background = Color(0xFF1A1C1E)
49 | val md_dark_onBackground = Color(0xFFE2E2E5)
50 | val md_dark_surface = Color(0xFF1A1C1E)
51 | val md_dark_onSurface = Color(0xFFE2E2E5)
52 | val md_dark_surfaceVariant = Color(0xFF42474E)
53 | val md_dark_onSurfaceVariant = Color(0xFFC2C7CF)
54 | val md_dark_surfaceTint = Color(0xFF9DCBFB)
55 | val md_dark_inverseSurface = Color(0xFFE2E2E5)
56 | val md_dark_inverseOnSurface = Color(0xFF2F3033)
57 | val md_dark_error = Color(0xFFF2B8B5)
58 | val md_dark_onError = Color(0xFF601410)
59 | val md_dark_errorContainer = Color(0xFF8C1D18)
60 | val md_dark_onErrorContainer = Color(0xFFF9DEDC)
61 | val md_dark_outline = Color(0xFF8C9199)
62 | val md_dark_outlineVariant = Color(0xFF49454F)
63 | val md_dark_scrim = Color(0xFF000000)
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/theme/Modifiers.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.theme
2 |
3 | import androidx.compose.ui.Modifier
4 | import androidx.compose.ui.composed
5 | import androidx.compose.ui.draw.drawWithCache
6 | import androidx.compose.ui.geometry.CornerRadius
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.graphics.PathEffect
9 | import androidx.compose.ui.graphics.drawscope.Stroke
10 | import androidx.compose.ui.platform.LocalDensity
11 | import androidx.compose.ui.unit.Dp
12 |
13 | // https://stackoverflow.com/questions/66427587/how-to-have-dashed-border-in-jetpack-compose
14 | fun Modifier.dashedBorder(
15 | strokeWidth: Dp,
16 | color: Color,
17 | cornerRadius: Dp,
18 | interval: Float = 5f
19 | ) =
20 | composed {
21 | val density = LocalDensity.current
22 | val strokeWidthPx = density.run { strokeWidth.toPx() }
23 | val cornerRadiusPx = density.run { cornerRadius.toPx() }
24 |
25 | this.then(
26 | Modifier.drawWithCache {
27 | onDrawBehind {
28 | val stroke =
29 | Stroke(
30 | width = strokeWidthPx,
31 | pathEffect =
32 | PathEffect.dashPathEffect(
33 | floatArrayOf(100 / interval, 100 / interval), 0f
34 | )
35 | )
36 |
37 | drawRoundRect(
38 | style = stroke,
39 | color = color,
40 | cornerRadius = CornerRadius(cornerRadiusPx)
41 | )
42 | }
43 | })
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/theme/Shapes.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material3.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val shapes = Shapes(
8 | extraSmall = RoundedCornerShape(4.dp),
9 | small = RoundedCornerShape(8.dp),
10 | medium = RoundedCornerShape(16.dp),
11 | large = RoundedCornerShape(24.dp),
12 | extraLarge = RoundedCornerShape(32.dp)
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.theme
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.res.Configuration
6 | import android.os.Build
7 | import androidx.compose.animation.animateColorAsState
8 | import androidx.compose.animation.core.CubicBezierEasing
9 | import androidx.compose.animation.core.tween
10 | import androidx.compose.material3.ColorScheme
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.darkColorScheme
13 | import androidx.compose.material3.dynamicDarkColorScheme
14 | import androidx.compose.material3.dynamicLightColorScheme
15 | import androidx.compose.material3.lightColorScheme
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.runtime.SideEffect
18 | import androidx.compose.runtime.remember
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.graphics.luminance
21 | import androidx.compose.ui.graphics.toArgb
22 | import androidx.compose.ui.platform.LocalContext
23 | import androidx.compose.ui.platform.LocalView
24 | import androidx.core.view.ViewCompat
25 | import com.chouten.app.data.AppThemeType
26 | import com.chouten.app.preferenceHandler
27 |
28 | private val darkColorScheme =
29 | darkColorScheme(
30 | primary = md_dark_primary,
31 | onPrimary = md_dark_onPrimary,
32 | primaryContainer = md_dark_primaryContainer,
33 | onPrimaryContainer = md_dark_onPrimaryContainer,
34 | inversePrimary = md_dark_inversePrimary,
35 | secondary = md_dark_secondary,
36 | onSecondary = md_dark_onSecondary,
37 | secondaryContainer = md_dark_secondaryContainer,
38 | onSecondaryContainer = md_dark_onSecondaryContainer,
39 | tertiary = md_dark_tertiary,
40 | onTertiary = md_dark_onTertiary,
41 | tertiaryContainer = md_dark_tertiaryContainer,
42 | onTertiaryContainer = md_dark_onTertiaryContainer,
43 | background = md_dark_background,
44 | onBackground = md_dark_onBackground,
45 | surface = md_dark_surface,
46 | onSurface = md_dark_onSurface,
47 | surfaceVariant = md_dark_surfaceVariant,
48 | onSurfaceVariant = md_dark_onSurfaceVariant,
49 | surfaceTint = md_dark_surfaceTint,
50 | inverseSurface = md_dark_inverseSurface,
51 | inverseOnSurface = md_dark_inverseOnSurface,
52 | error = md_dark_error,
53 | onError = md_dark_onError,
54 | errorContainer = md_dark_errorContainer,
55 | onErrorContainer = md_dark_onErrorContainer,
56 | outline = md_dark_outline,
57 | outlineVariant = md_dark_outlineVariant,
58 | scrim = md_dark_scrim,
59 | )
60 |
61 | private val lightColorScheme =
62 | lightColorScheme(
63 | primary = md_light_primary,
64 | onPrimary = md_light_onPrimary,
65 | primaryContainer = md_light_primaryContainer,
66 | onPrimaryContainer = md_light_onPrimaryContainer,
67 | inversePrimary = md_light_inversePrimary,
68 | secondary = md_light_secondary,
69 | onSecondary = md_light_onSecondary,
70 | secondaryContainer = md_light_secondaryContainer,
71 | onSecondaryContainer = md_light_onSecondaryContainer,
72 | tertiary = md_light_tertiary,
73 | onTertiary = md_light_onTertiary,
74 | tertiaryContainer = md_light_tertiaryContainer,
75 | onTertiaryContainer = md_light_onTertiaryContainer,
76 | background = md_light_background,
77 | onBackground = md_light_onBackground,
78 | surface = md_light_surface,
79 | onSurface = md_light_onSurface,
80 | surfaceVariant = md_light_surfaceVariant,
81 | onSurfaceVariant = md_light_onSurfaceVariant,
82 | surfaceTint = md_light_surfaceTint,
83 | inverseSurface = md_light_inverseSurface,
84 | inverseOnSurface = md_light_inverseOnSurface,
85 | error = md_light_error,
86 | onError = md_light_onError,
87 | errorContainer = md_light_errorContainer,
88 | onErrorContainer = md_light_onErrorContainer,
89 | outline = md_light_outline,
90 | outlineVariant = md_light_outlineVariant,
91 | scrim = md_light_scrim,
92 | )
93 |
94 | fun isDarkTheme(context: Context): Boolean {
95 | val uiMode = context.applicationContext.resources.configuration.uiMode
96 | val uiDark = Configuration.UI_MODE_NIGHT_YES
97 | return when (preferenceHandler.themeType) {
98 | AppThemeType.SYSTEM -> uiMode.and(Configuration.UI_MODE_NIGHT_MASK) == uiDark
99 | AppThemeType.DARK -> true
100 | else -> false
101 | }
102 | }
103 |
104 | @Composable
105 | fun ChoutenTheme(
106 | darkTheme: Boolean = isDarkTheme(LocalContext.current),
107 | oledTheme: Boolean = preferenceHandler.isOledTheme,
108 | // Dynamic color is available on Android 12+
109 | dynamicColor: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S,
110 | content: @Composable () -> Unit
111 | ) {
112 | val colorScheme =
113 | when {
114 | preferenceHandler.isDynamicColor && dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
115 | val context = LocalContext.current
116 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(
117 | context
118 | )
119 | }
120 |
121 | darkTheme -> darkColorScheme
122 | else -> lightColorScheme
123 | }.let {
124 | if (darkTheme && oledTheme) it.toOled() else it
125 | }
126 | val view = LocalView.current
127 | if (!view.isInEditMode) {
128 | SideEffect {
129 | val activity = view.context as? Activity
130 | activity?.window?.apply {
131 | navigationBarColor = Color.Transparent.toArgb()
132 | statusBarColor = Color.Transparent.toArgb()
133 | }
134 | // Set statusbar icons color considering the top app state banner
135 | val isIncognito = preferenceHandler.isIncognito
136 | val isOfflineMode = preferenceHandler.isOfflineMode
137 | val statusBarBackgroundColor = when {
138 | isOfflineMode -> colorScheme.tertiary
139 | isIncognito -> colorScheme.primary
140 | else -> colorScheme.surface
141 | }
142 |
143 | val isLightStatusBar = statusBarBackgroundColor.luminance() > 0.5
144 | ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars =
145 | isLightStatusBar
146 | }
147 | }
148 |
149 | MaterialTheme(
150 | colorScheme = animate(colorScheme),
151 | typography = Typography,
152 | content = content
153 | )
154 | }
155 |
156 | @Composable
157 | private fun animate(colors: ColorScheme): ColorScheme {
158 | val animSpec = remember {
159 | tween(durationMillis = 300, easing = CubicBezierEasing(0.2f, 0f, 0f, 1f))
160 | }
161 |
162 | @Composable
163 | fun animateColor(color: Color): Color =
164 | animateColorAsState(
165 | targetValue = color,
166 | animationSpec = animSpec,
167 | label = "Theme Color Transition"
168 | ).value
169 |
170 | return ColorScheme(
171 | primary = animateColor(colors.primary),
172 | onPrimary = animateColor(colors.onPrimary),
173 | primaryContainer = animateColor(colors.primaryContainer),
174 | onPrimaryContainer = animateColor(colors.onPrimaryContainer),
175 | inversePrimary = animateColor(colors.inversePrimary),
176 | secondary = animateColor(colors.secondary),
177 | onSecondary = animateColor(colors.onSecondary),
178 | secondaryContainer = animateColor(colors.secondaryContainer),
179 | onSecondaryContainer = animateColor(colors.onSecondaryContainer),
180 | tertiary = animateColor(colors.tertiary),
181 | onTertiary = animateColor(colors.onTertiary),
182 | tertiaryContainer = animateColor(colors.tertiaryContainer),
183 | onTertiaryContainer = animateColor(colors.onTertiaryContainer),
184 | background = animateColor(colors.background),
185 | onBackground = animateColor(colors.onBackground),
186 | surface = animateColor(colors.surface),
187 | onSurface = animateColor(colors.onSurface),
188 | surfaceVariant = animateColor(colors.surfaceVariant),
189 | onSurfaceVariant = animateColor(colors.onSurfaceVariant),
190 | surfaceTint = animateColor(colors.surfaceTint),
191 | inverseSurface = animateColor(colors.inverseSurface),
192 | inverseOnSurface = animateColor(colors.inverseOnSurface),
193 | error = animateColor(colors.error),
194 | onError = animateColor(colors.onError),
195 | errorContainer = animateColor(colors.errorContainer),
196 | onErrorContainer = animateColor(colors.onErrorContainer),
197 | outline = animateColor(colors.outline),
198 | outlineVariant = animateColor(colors.outlineVariant),
199 | scrim = animateColor(colors.scrim)
200 | )
201 | }
202 |
203 | private fun ColorScheme.toOled() = this.copy(
204 | surface = Color.Black,
205 | background = Color.Black
206 | )
207 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/views/home/HomePageViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.views.home
2 |
3 | import android.content.Context
4 | import android.os.Handler
5 | import android.os.Looper
6 | import android.util.Log
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.mutableStateListOf
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.setValue
11 | import androidx.lifecycle.ViewModel
12 | import androidx.lifecycle.viewModelScope
13 | import com.chouten.app.Mapper
14 | import com.chouten.app.ModuleLayer
15 | import com.chouten.app.PrimaryDataLayer
16 | import com.chouten.app.data.HomeResult
17 | import com.chouten.app.data.ModuleModel
18 | import com.chouten.app.data.ModuleResponse
19 | import com.chouten.app.data.SnackbarVisualsWithError
20 | import com.chouten.app.data.WebviewHandler
21 | import com.chouten.app.data.ErrorAction
22 | import com.chouten.app.data.ModuleAction
23 | import kotlinx.coroutines.launch
24 | import com.chouten.app.data.HomepagePayload
25 |
26 | class HomePageViewModel(context: Context, private val webview: WebviewHandler = WebviewHandler()) :
27 | ViewModel() {
28 |
29 | var isLoading by mutableStateOf(false)
30 | private set
31 |
32 | private val _homeResults = mutableStateListOf()
33 | val homeResults: List
34 | get() = _homeResults
35 |
36 | private val _loadedModule = mutableStateOf(null)
37 | val loadedModule: ModuleModel?
38 | get() = _loadedModule.value
39 |
40 | var errors = 0
41 |
42 | init {
43 | webview.initialize(context)
44 | webview.dontCloseOnError()
45 | webview.setCallback(this::callback)
46 | }
47 |
48 | // This is called by the webview
49 | // when it's done calling the logic function
50 | fun callback(message: String) {
51 |
52 | val action = Mapper.parse(message).action
53 |
54 | try {
55 | if(action == "error"){
56 | val error = Mapper.parse(message)
57 | throw Exception(error.result)
58 | }
59 |
60 | val results = Mapper.parse>>(message)
61 |
62 | _homeResults.clear()
63 | _homeResults.addAll(results.result)
64 | } catch (e: Exception) {
65 | errors += 1
66 | PrimaryDataLayer.enqueueSnackbar(
67 | SnackbarVisualsWithError(
68 | e.localizedMessage ?: "Error parsing home page results.",
69 | isError = true
70 | )
71 | )
72 | e.printStackTrace()
73 | }
74 |
75 | isLoading = false
76 | }
77 |
78 | private val handler = Handler(Looper.getMainLooper())
79 |
80 | // TODO: unify with init
81 | fun initialize(shouldReset: Boolean = false) {
82 | if(shouldReset){
83 | errors = 0
84 | }
85 |
86 | _homeResults.clear()
87 | viewModelScope.launch { loadHomePage() }
88 | }
89 |
90 | fun getCode(): String{
91 | val currentModule = ModuleLayer.selectedModule ?: throw Exception("No module selected")
92 | val subtype = currentModule.subtypes.getOrNull(0) ?: throw Exception("Subtype not found")
93 | return currentModule.code?.get(subtype)?.home?.getOrNull(0)?.code ?: throw Exception("Code not found")
94 | }
95 |
96 | private suspend fun loadHomePage() {
97 | _loadedModule.value = ModuleLayer.selectedModule
98 |
99 | isLoading = true
100 | val code = this.getCode()
101 |
102 | val webviewPayload = HomepagePayload(
103 | action = "homepage"
104 | )
105 |
106 | webview.load(
107 | code,
108 | Mapper.json.encodeToString(HomepagePayload.serializer(), webviewPayload)
109 | )
110 |
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/views/info/InfoPageViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.views.info
2 |
3 | import android.content.Context
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableIntStateOf
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.setValue
8 | import androidx.lifecycle.ViewModel
9 | import androidx.lifecycle.viewModelScope
10 | import com.chouten.app.Mapper
11 | import com.chouten.app.ModuleLayer
12 | import com.chouten.app.PrimaryDataLayer
13 | import com.chouten.app.data.ErrorAction
14 | import com.chouten.app.data.InfoResult
15 | import com.chouten.app.data.ModuleAction
16 | import com.chouten.app.data.ModuleResponse
17 | import com.chouten.app.data.SnackbarVisualsWithError
18 | import com.chouten.app.data.WebviewHandler
19 | import com.chouten.app.data.WebviewPayload
20 | import kotlinx.coroutines.launch
21 | import kotlinx.coroutines.sync.Mutex
22 | import java.net.URLDecoder
23 |
24 | class InfoPageViewModel(context: Context, private val url: String, private var title: String) :
25 | ViewModel() {
26 |
27 | private val webview = WebviewHandler()
28 |
29 | private val syncLock = Mutex(false)
30 |
31 | private var hasLoadedInfo by mutableStateOf(false)
32 | val hasLoadedInfoText: Boolean
33 | get() = hasLoadedInfo
34 |
35 | private var hasLoadedMediaEpisodes by mutableStateOf(false)
36 | val hasLoadedEpisodes: Boolean
37 | get() = hasLoadedMediaEpisodes
38 |
39 | private var altTitles by mutableStateOf(listOf())
40 | val altTitlesText: List
41 | get() = altTitles
42 |
43 | private var description by mutableStateOf("")
44 | val descriptionText: String
45 | get() = description
46 |
47 | private var thumbnail by mutableStateOf("")
48 | val thumbnailUrl: String
49 | get() = thumbnail
50 |
51 | private var banner by mutableStateOf("")
52 | val bannerUrl: String
53 | get() = banner
54 |
55 | private var status by mutableStateOf("")
56 | val statusText: String
57 | get() = status
58 |
59 | private var mediaCount by mutableIntStateOf(0)
60 | val mediaCountText: Int
61 | get() = mediaCount
62 |
63 | private var mediaType by mutableStateOf("")
64 | val mediaTypeText: String
65 | get() = mediaType
66 |
67 | private var infoResult by mutableStateOf(listOf())
68 | val infoResults: List
69 | get() = infoResult
70 |
71 | private var _seasons by mutableStateOf(listOf())
72 | val seasons: List
73 | get() = _seasons
74 |
75 | private var selectedSeason by mutableIntStateOf(0)
76 | val selectedSeasonText: Int
77 | get() = selectedSeason
78 |
79 | fun getCode(): String{
80 | val currentModule = ModuleLayer.selectedModule ?: throw Exception("No module selected")
81 | val subtype = currentModule.subtypes.getOrNull(0) ?: throw Exception("Subtype not found")
82 | return currentModule.code?.get(subtype)?.info?.getOrNull(0)?.code ?: throw Exception("Code not found")
83 | }
84 |
85 | fun callback(message: String) {
86 | val title = ""
87 |
88 | if (message.isBlank()) {
89 | PrimaryDataLayer.enqueueSnackbar(
90 | SnackbarVisualsWithError("No results found for $title", false)
91 | )
92 | }
93 |
94 | val action = Mapper.parse(message).action
95 |
96 | try{
97 | if(action == "error"){
98 | val error = Mapper.parse(message)
99 | throw Exception(error.result)
100 | }
101 |
102 | when(action){
103 | "metadata" -> {
104 | try {
105 | val results = Mapper.parse>(message)
106 |
107 | val result = results.result
108 | altTitles = result.altTitles ?: listOf()
109 | description = result.description
110 | thumbnail = result.poster
111 | banner = result.banner ?: ""
112 | status = result.status ?: ""
113 | mediaCount = result.totalMediaCount ?: 0
114 | mediaType = result.mediaType
115 | hasLoadedInfo = true
116 |
117 |
118 | val epListURL = result.seasons?.getOrNull(
119 | selectedSeason
120 | )?.url ?: result.epListURLs.firstOrNull() ?: ""
121 |
122 | val webviewPayload = WebviewPayload(
123 | query = epListURL,
124 | action = "eplist"
125 | )
126 |
127 | val code = getCode()
128 |
129 | viewModelScope.launch {
130 | webview.load(
131 | code,
132 | Mapper.json.encodeToString(WebviewPayload.serializer(), webviewPayload)
133 | )
134 | }
135 |
136 | } catch (e: Exception) {
137 | e.printStackTrace()
138 | PrimaryDataLayer.enqueueSnackbar(
139 | SnackbarVisualsWithError("Error parsing results for $title", false)
140 | )
141 | }
142 | }
143 | "eplist" -> {
144 | try {
145 | val results = Mapper.parse>>(message)
146 | infoResult = results.result
147 | _seasons = infoResult.map {
148 | InfoResult.Season(
149 | it.title,
150 | it.list.firstOrNull()?.url ?: ""
151 | )
152 | }.distinctBy { it.url }
153 | hasLoadedMediaEpisodes = true
154 | } catch (e: Exception) {
155 | PrimaryDataLayer.enqueueSnackbar(
156 | SnackbarVisualsWithError(
157 | "Error parsing second results for $title",
158 | false
159 | )
160 | )
161 | println("Parsing error: $e")
162 | throw Exception("Error parsing results for $title")
163 | }
164 | }
165 | else -> {
166 | throw Exception("Action not found. The action must be either set to 'eplist' or 'metadata'")
167 | }
168 | }
169 | }catch(e: Exception){
170 | PrimaryDataLayer.enqueueSnackbar(
171 | SnackbarVisualsWithError(
172 | e.localizedMessage ?: "Error parsing home page results.",
173 | isError = true
174 | )
175 | )
176 | e.printStackTrace()
177 | }
178 | }
179 |
180 | init {
181 | // Both title and url are url-encoded.
182 | title = URLDecoder.decode(title, "UTF-8")
183 | val decodedUrl = URLDecoder.decode(url, "UTF-8")
184 |
185 | // We want to get the info code from the webview handler
186 | // and then load the page with that code.
187 | webview.initialize(context)
188 | webview.setCallback(this::callback)
189 |
190 | val code = getCode()
191 | val webviewPayload = WebviewPayload(
192 | query = decodedUrl,
193 | action = "metadata"
194 | )
195 |
196 | viewModelScope.launch {
197 | webview.load(code, Mapper.json.encodeToString(WebviewPayload.serializer(), webviewPayload))
198 | }
199 |
200 | }
201 |
202 | suspend fun changeSeason(context: Context, season: InfoResult.Season) {
203 | hasLoadedInfo = false
204 | hasLoadedMediaEpisodes = false
205 | selectedSeason = seasons.indexOfFirst { it == season }
206 | webview.destroy()
207 | webview.initialize(context)
208 | webview.setCallback(::callback)
209 |
210 | val code = getCode()
211 | val metadataPayload = WebviewPayload(
212 | query = url,
213 | action = "metadata"
214 | )
215 |
216 | viewModelScope.launch {
217 | webview.load(code, Mapper.json.encodeToString(WebviewPayload.serializer(), metadataPayload))
218 | }
219 | }
220 |
221 | fun getTitle(): String {
222 | return title
223 | }
224 |
225 | fun getUrl(): String {
226 | return url
227 | }
228 |
229 | override fun onCleared() {
230 | webview.destroy()
231 | super.onCleared()
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/views/search/SearchPageViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.views.search
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateListOf
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.setValue
9 | import androidx.lifecycle.ViewModel
10 | import androidx.lifecycle.viewModelScope
11 | import com.chouten.app.LogLayer
12 | import com.chouten.app.Mapper
13 | import com.chouten.app.ModuleLayer
14 | import com.chouten.app.PrimaryDataLayer
15 | import com.chouten.app.data.LogEntry
16 | import com.chouten.app.data.ModuleResponse
17 | import com.chouten.app.data.SearchResult
18 | import com.chouten.app.data.SnackbarVisualsWithError
19 | import com.chouten.app.data.WebviewHandler
20 | import com.chouten.app.data.ModuleAction
21 | import com.chouten.app.data.ErrorAction
22 | import com.chouten.app.data.WebviewPayload
23 | import kotlinx.coroutines.FlowPreview
24 | import kotlinx.coroutines.Job
25 | import kotlinx.coroutines.flow.MutableStateFlow
26 | import kotlinx.coroutines.flow.collectLatest
27 | import kotlinx.coroutines.flow.debounce
28 | import kotlinx.coroutines.flow.distinctUntilChanged
29 | import kotlinx.coroutines.launch
30 |
31 | class SearchPageViewModel(
32 | context: Context,
33 | private val webview: WebviewHandler = WebviewHandler()
34 | ) : ViewModel() {
35 | var isSearching by mutableStateOf(false)
36 | private set
37 |
38 | // We want to keep track of the search job so that
39 | // we can cancel it if the search query changes.
40 | private var searchJob: Job? = null
41 |
42 | // We want to use a flow for this so that we
43 | // can have a debounce on the search.
44 | // Every time the search query changes, we want to
45 | // wait 500ms before actually searching.
46 | private var _searchQuery = MutableStateFlow("")
47 |
48 | @OptIn(FlowPreview::class)
49 | var searchQuery: String = _searchQuery.value
50 | set(value) {
51 | field = value
52 | _searchQuery.value = value
53 | searchJob?.cancel()
54 | searchJob =
55 | viewModelScope.launch {
56 | _searchQuery.debounce(500).distinctUntilChanged().collectLatest {
57 | isSearching = true
58 | if (it.isBlank()) {
59 | _searchResults.clear()
60 | isSearching = false
61 | return@collectLatest
62 | }
63 | println("Searching for $it")
64 | _searchQuery.value = it
65 | search(it)
66 | }
67 | }
68 | }
69 |
70 | var previousSearchQuery by mutableStateOf("")
71 | private set // We don't want to be able to set this from outside the class.
72 |
73 | init {
74 | webview.initialize(context)
75 | webview.dontCloseOnError()
76 | webview.setCallback(this::callback)
77 | }
78 |
79 | fun getCode(): String{
80 | val currentModule = ModuleLayer.selectedModule ?: throw Exception("No module selected")
81 | val subtype = currentModule.subtypes.getOrNull(0) ?: throw Exception("Subtype not found")
82 | return currentModule.code?.get(subtype)?.search?.getOrNull(0)?.code ?: throw Exception("Code not found")
83 | }
84 |
85 | // This is called by the webview
86 | // when it's done calling the logic function
87 | fun callback(message: String) {
88 |
89 | if (message.isBlank()) {
90 | PrimaryDataLayer.enqueueSnackbar(
91 | SnackbarVisualsWithError("No results found ", false)
92 | )
93 | isSearching = false
94 | }
95 |
96 | _searchResults.clear()
97 |
98 | val action = Mapper.parse(message).action
99 |
100 | try {
101 | if(action == "error"){
102 | val error = Mapper.parse(message)
103 | throw Exception(error.result)
104 | }
105 |
106 | val results = Mapper.parse>>(message)
107 | _searchResults.addAll(results.result)
108 | isSearching = false
109 | } catch (e: Exception) {
110 | PrimaryDataLayer.enqueueSnackbar(
111 | SnackbarVisualsWithError("Error parsing search results: " + e.message, true)
112 | )
113 | e.printStackTrace()
114 | e.localizedMessage
115 | ?.let {
116 | LogEntry(
117 | title = "Error parsing search results",
118 | message = it,
119 | isError = true,
120 | )
121 | }
122 | ?.let { LogLayer.addLogEntry(it) }
123 | isSearching = false
124 | }
125 | }
126 |
127 | private val _searchResults = mutableStateListOf()
128 | val searchResults: List
129 | get() = _searchResults
130 |
131 | private suspend fun search(query: String) {
132 | // Update the previous search query.
133 | // By setting it to the searchQuery value,
134 | // we can ensure that it is always up to date.
135 | previousSearchQuery = _searchQuery.value
136 |
137 | // We want to get the currently selected module and then
138 | // search for the query within that module.
139 | isSearching = true
140 |
141 | val code = getCode()
142 |
143 | if(!code.isEmpty()){
144 | val webviewPayload = WebviewPayload(
145 | query = query,
146 | action = "search"
147 | )
148 |
149 | webview.load(code, Mapper.json.encodeToString(WebviewPayload.serializer(), webviewPayload))
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/views/settings/MorePage.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.views.settings
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.filled.ChevronRight
11 | import androidx.compose.material3.*
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.unit.dp
16 | import androidx.navigation.NavController
17 | import com.chouten.app.R
18 | import com.chouten.app.data.Preferences
19 | import com.chouten.app.preferenceHandler
20 | import com.chouten.app.ui.Screen
21 | import com.chouten.app.ui.components.SettingsItem
22 | import com.chouten.app.ui.components.SettingsToggle
23 | import kotlinx.serialization.json.Json
24 |
25 | val json = Json { prettyPrint = true }
26 |
27 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
28 | @OptIn(ExperimentalMaterial3Api::class)
29 | @Composable
30 | fun MorePage(
31 | navHost: NavController
32 | ) {
33 | Scaffold(topBar = {
34 | CenterAlignedTopAppBar(
35 | title = {
36 | Text(
37 | text = stringResource(R.string.navbar_more_header),
38 | style = MaterialTheme.typography.headlineMedium
39 | )
40 | }, modifier = Modifier.fillMaxWidth()
41 | )
42 | }) {
43 | Column(
44 | modifier = Modifier
45 | .fillMaxSize()
46 | .padding(it),
47 | ) {
48 | SettingsToggle(
49 | preference = Preferences.Settings.downloadedOnly,
50 | defaultValue = preferenceHandler.getBoolean(
51 | Preferences.Settings.downloadedOnly.preference.first,
52 | false
53 | ),
54 | onCheckedChange = { toggle ->
55 | preferenceHandler.isOfflineMode = toggle
56 | }
57 | )
58 | SettingsToggle(
59 | preference = Preferences.Settings.incognito,
60 | defaultValue = preferenceHandler.getBoolean(
61 | Preferences.Settings.incognito.preference.first,
62 | false
63 | ),
64 | onCheckedChange = { toggle ->
65 | preferenceHandler.isIncognito = toggle
66 | }
67 | )
68 | Divider(Modifier.padding(16.dp))
69 | SettingsItem(
70 | modifier = Modifier.clickable {
71 | navHost.navigate(Screen.AppearancePage.route)
72 | },
73 | icon = {},
74 | text = { Text(stringResource(R.string.appearance__title)) },
75 | secondaryText = { Text(stringResource(R.string.appearance__desc)) },
76 | trailing = {
77 | Icon(
78 | Icons.Default.ChevronRight,
79 | stringResource(R.string.log__title)
80 | )
81 | },
82 | )
83 | SettingsItem(
84 | modifier = Modifier.clickable {
85 | navHost.navigate(Screen.NetworkPage.route)
86 | },
87 | icon = {},
88 | text = { Text(stringResource(R.string.network__title)) },
89 | secondaryText = { Text(stringResource(R.string.network__desc)) },
90 | trailing = {
91 | Icon(
92 | Icons.Default.ChevronRight,
93 | stringResource(R.string.network__title)
94 | )
95 | },
96 | )
97 | SettingsItem(
98 | modifier = Modifier.clickable {
99 | navHost.navigate(Screen.LogPage.route)
100 | },
101 | icon = {},
102 | text = { Text(stringResource(R.string.log__title)) },
103 | secondaryText = { Text(stringResource(R.string.log__desc)) },
104 | trailing = {
105 | Icon(
106 | Icons.Default.ChevronRight,
107 | stringResource(R.string.log__title)
108 | )
109 | },
110 | )
111 | SettingsToggle(
112 | preference = Preferences.Settings.devMode,
113 | defaultValue = preferenceHandler.getBoolean(
114 | Preferences.Settings.devMode.preference.first,
115 | false
116 | ),
117 | onCheckedChange = { toggle ->
118 | preferenceHandler.isDevMode = toggle
119 | }
120 | )
121 | }
122 | }
123 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/views/settings/MorePageViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.views.settings
2 |
3 | import androidx.lifecycle.ViewModel
4 |
5 | class SettingsPageViewModel : ViewModel()
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/views/settings/screens/LogPage.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.views.settings.screens
2 |
3 | import androidx.compose.animation.animateContentSize
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.BoxWithConstraints
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.layout.size
14 | import androidx.compose.foundation.lazy.LazyColumn
15 | import androidx.compose.foundation.lazy.items
16 | import androidx.compose.foundation.shape.CircleShape
17 | import androidx.compose.material.icons.Icons
18 | import androidx.compose.material.icons.filled.Clear
19 | import androidx.compose.material3.Card
20 | import androidx.compose.material3.CardDefaults
21 | import androidx.compose.material3.CircularProgressIndicator
22 | import androidx.compose.material3.FilledTonalButton
23 | import androidx.compose.material3.Icon
24 | import androidx.compose.material3.MaterialTheme
25 | import androidx.compose.material3.Text
26 | import androidx.compose.runtime.Composable
27 | import androidx.compose.runtime.getValue
28 | import androidx.compose.runtime.mutableStateOf
29 | import androidx.compose.runtime.remember
30 | import androidx.compose.runtime.setValue
31 | import androidx.compose.ui.Alignment
32 | import androidx.compose.ui.Modifier
33 | import androidx.compose.ui.draw.clip
34 | import androidx.compose.ui.layout.ContentScale
35 | import androidx.compose.ui.res.stringResource
36 | import androidx.compose.ui.text.font.FontWeight
37 | import androidx.compose.ui.unit.IntSize
38 | import androidx.compose.ui.unit.dp
39 | import com.chouten.app.R
40 | import com.chouten.app.data.LogDataLayer
41 | import com.chouten.app.data.LogEntry
42 | import com.skydoves.landscapist.ImageOptions
43 | import com.skydoves.landscapist.glide.GlideImage
44 |
45 | @Composable
46 | fun LogPage(
47 | provider: LogDataLayer
48 | ) {
49 |
50 | val logs = remember {
51 | provider.logEntries.asReversed()
52 | }
53 |
54 | Column(
55 | modifier = Modifier.animateContentSize(),
56 | horizontalAlignment = Alignment.CenterHorizontally,
57 | ) {
58 | // TODO: Not cut off the results horribly - floating bar?
59 | Row(
60 | modifier = Modifier
61 | .fillMaxWidth()
62 | .padding(16.dp, 16.dp),
63 | horizontalArrangement = Arrangement.spacedBy(8.dp),
64 | ) {
65 | FilledTonalButton(
66 | onClick = { provider.clearLogs() },
67 | enabled = logs.isNotEmpty()
68 | ) {
69 | Row(
70 | horizontalArrangement = Arrangement.spacedBy(8.dp),
71 | verticalAlignment = Alignment.CenterVertically,
72 | ) {
73 | Icon(
74 | imageVector = Icons.Filled.Clear,
75 | contentDescription = stringResource(R.string.log__action_clear),
76 | )
77 | Text(
78 | text = stringResource(R.string.log__action_clear),
79 | fontWeight = FontWeight.Bold
80 | )
81 | }
82 | }
83 | }
84 |
85 | if (logs.isEmpty()) {
86 | Column(
87 | modifier = Modifier
88 | .fillMaxSize()
89 | .padding(16.dp, 0.dp),
90 | horizontalAlignment = Alignment.CenterHorizontally,
91 | verticalArrangement = Arrangement.Center
92 | ) {
93 | Text(
94 | text = stringResource(R.string.log__placeholder)
95 | )
96 | }
97 | } else LazyColumn(
98 | modifier = Modifier
99 | .fillMaxSize()
100 | .padding(bottom = 8.dp),
101 | horizontalAlignment = Alignment.CenterHorizontally,
102 | verticalArrangement = Arrangement.spacedBy(8.dp)
103 | ) {
104 | items(
105 | logs
106 | ) {
107 | BoxWithConstraints(
108 | modifier = Modifier.padding(16.dp, 0.dp)
109 | ) {
110 | LogEntryCard(it)
111 | }
112 | }
113 | }
114 | }
115 | }
116 |
117 | @Composable
118 | fun LogEntryCard(
119 | entry: LogEntry
120 | ) {
121 | var isMessageExpanded by remember {
122 | mutableStateOf(false)
123 | }
124 |
125 | Card(
126 | colors = if (entry.isError) {
127 | CardDefaults.cardColors(
128 | containerColor = MaterialTheme.colorScheme.error
129 | )
130 | } else CardDefaults.cardColors(), modifier = Modifier.fillMaxWidth()
131 | ) {
132 | Column(
133 | modifier = Modifier.padding(8.dp),
134 | verticalArrangement = Arrangement.SpaceBetween
135 | ) {
136 | Row(
137 | horizontalArrangement = Arrangement.SpaceBetween,
138 | verticalAlignment = Alignment.CenterVertically,
139 | modifier = Modifier
140 | .fillMaxWidth()
141 | .padding(bottom = 8.dp)
142 | ) {
143 | Row(
144 | horizontalArrangement = Arrangement.spacedBy(8.dp),
145 | verticalAlignment = Alignment.CenterVertically,
146 | ) {
147 | GlideImage(
148 | imageModel = { entry.module.meta.icon },
149 | imageOptions = ImageOptions(
150 | contentScale = ContentScale.Fit,
151 | alignment = Alignment.Center,
152 | contentDescription = "Favicon for the ${entry.module.name} module",
153 | requestSize = IntSize(128, 128)
154 | ),
155 | loading = {
156 | Box(Modifier.matchParentSize()) {
157 | CircularProgressIndicator(
158 | Modifier.align(
159 | Alignment.Center
160 | )
161 | )
162 | }
163 | },
164 | modifier = Modifier
165 | .size(36.dp)
166 | .clip(CircleShape),
167 | )
168 |
169 | Column {
170 | Text(
171 | text = entry.title,
172 | style = MaterialTheme.typography.headlineSmall
173 | )
174 |
175 | Text(
176 | text = entry.module.name,
177 | style = MaterialTheme.typography.labelMedium
178 | )
179 | }
180 | }
181 |
182 | Text(
183 | text = entry.timestamp,
184 | style = MaterialTheme.typography.labelMedium
185 | )
186 | }
187 |
188 | Text(modifier = Modifier
189 | .padding(8.dp)
190 | .clickable {
191 | isMessageExpanded = !isMessageExpanded
192 | }
193 | .animateContentSize(),
194 | text = entry.message,
195 | style = MaterialTheme.typography.bodyMedium,
196 | maxLines = if (isMessageExpanded) Int.MAX_VALUE else 4)
197 | }
198 | }
199 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/views/settings/screens/NetworkPage.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.views.settings.screens
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import com.chouten.app.App
8 | import com.chouten.app.data.CustomDNS
9 | import com.chouten.app.data.Preferences
10 | import com.chouten.app.initializeNetwork
11 | import com.chouten.app.preferenceHandler
12 | import com.chouten.app.ui.components.SettingsChoice
13 |
14 | @Composable
15 | fun NetworkPage() {
16 | Column(
17 | modifier = Modifier.fillMaxSize()
18 | ) {
19 | SettingsChoice(
20 | preference = Preferences.Settings.dns,
21 | onPreferenceChange = { updated ->
22 | preferenceHandler.dns = updated
23 | // We need to remake the client
24 | initializeNetwork(App.applicationContext)
25 | },
26 | defaultValue = preferenceHandler.getEnum(
27 | Preferences.Settings.dns.preference.first,
28 | CustomDNS.NONE
29 | ),
30 | )
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/views/settings/screens/appearance/AppearancePage.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.views.settings.screens.appearance
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.text.KeyboardActions
7 | import androidx.compose.foundation.text.KeyboardOptions
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.CopyAll
10 | import androidx.compose.material3.AlertDialog
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.material3.OutlinedTextField
14 | import androidx.compose.material3.Text
15 | import androidx.compose.material3.TextButton
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.runtime.mutableStateOf
18 | import androidx.compose.runtime.saveable.rememberSaveable
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.platform.LocalContext
21 | import androidx.compose.ui.platform.LocalFocusManager
22 | import androidx.compose.ui.res.stringResource
23 | import androidx.compose.ui.text.input.ImeAction
24 | import com.chouten.app.PrimaryDataLayer
25 | import com.chouten.app.R
26 | import com.chouten.app.data.AppThemeType
27 | import com.chouten.app.data.Preferences
28 | import com.chouten.app.data.SnackbarVisualsWithError
29 | import com.chouten.app.preferenceHandler
30 | import com.chouten.app.ui.components.SettingsChoice
31 | import com.chouten.app.ui.components.SettingsItem
32 | import com.chouten.app.ui.components.SettingsToggle
33 |
34 |
35 | @Composable
36 | fun AppearancePage() {
37 | val context = LocalContext.current
38 | val vm = AppearanceViewModel(context, MaterialTheme.colorScheme)
39 |
40 | val isNamePromptVisible = rememberSaveable { mutableStateOf(false) }
41 | val themeName = rememberSaveable { mutableStateOf("") }
42 | val labelText = rememberSaveable { mutableStateOf(R.string.export_theme__label) }
43 |
44 | val successMessage = stringResource(R.string.export_theme__success)
45 |
46 | val exportTheme = {
47 | labelText.value = R.string.export_theme__label
48 | if (vm.exportTheme(themeName.value)) {
49 | isNamePromptVisible.value = false
50 | PrimaryDataLayer.enqueueSnackbar(
51 | SnackbarVisualsWithError(
52 | message = successMessage,
53 | isError = false,
54 | // TODO: Add custom button
55 | // which opens the folder where the theme was exported
56 | )
57 | )
58 | } else {
59 | labelText.value = R.string.export_theme__error
60 | }
61 | }
62 |
63 | val focusManager = LocalFocusManager.current
64 |
65 | Column {
66 |
67 | AnimatedVisibility(visible = isNamePromptVisible.value) {
68 | // We want an Alert with a text field for the user to enter the name of the theme
69 | AlertDialog(onDismissRequest = {
70 | isNamePromptVisible.value = false
71 | }, title = {
72 | Text(text = stringResource(R.string.export_theme__title))
73 | }, text = {
74 | OutlinedTextField(
75 | value = themeName.value,
76 | onValueChange = { themeName.value = it },
77 | label = {
78 | Text(
79 | stringResource(R.string.export_theme__label),
80 | color = if (labelText.value == R.string.export_theme__error) {
81 | MaterialTheme.colorScheme.error
82 | } else {
83 | MaterialTheme.colorScheme.onSurface
84 | }
85 | )
86 | },
87 | shape = MaterialTheme.shapes.medium,
88 | singleLine = true,
89 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
90 | keyboardActions = KeyboardActions(onDone = {
91 | exportTheme()
92 | focusManager.clearFocus()
93 | })
94 | )
95 | }, confirmButton = {
96 | TextButton(onClick = {
97 | exportTheme()
98 | }) {
99 | Text(text = stringResource(R.string.export_theme__title))
100 | }
101 | }, dismissButton = {
102 | TextButton(onClick = {
103 | isNamePromptVisible.value = false
104 | }) {
105 | Text(text = stringResource(R.string.cancel))
106 | }
107 | })
108 | }
109 |
110 | SettingsToggle(preference = Preferences.Settings.dynamicColor,
111 | defaultValue = preferenceHandler.getBoolean(
112 | Preferences.Settings.dynamicColor.preference.first, true
113 | ),
114 | onCheckedChange = { toggle ->
115 | preferenceHandler.isDynamicColor = toggle
116 | })
117 | SettingsToggle(preference = Preferences.Settings.oledTheme,
118 | defaultValue = preferenceHandler.getBoolean(
119 | Preferences.Settings.oledTheme.preference.first, false
120 | ),
121 | onCheckedChange = { toggle ->
122 | preferenceHandler.isOledTheme = toggle
123 | })
124 | SettingsItem(modifier = Modifier.clickable {
125 | isNamePromptVisible.value = true
126 | },
127 | { },
128 | { Text(text = stringResource(R.string.export_theme__title)) },
129 | { Text(text = stringResource(R.string.export_theme__desc)) }) {
130 | Icon(
131 | Icons.Default.CopyAll, stringResource(R.string.export_theme__desc)
132 | )
133 | }
134 | SettingsChoice(preference = Preferences.Settings.themeType,
135 | onPreferenceChange = { updated ->
136 | preferenceHandler.themeType = updated
137 | },
138 | defaultValue = preferenceHandler.getEnum(
139 | Preferences.Settings.themeType.preference.first, AppThemeType.SYSTEM
140 | ),
141 | onPreviewSelectionChange = { theme ->
142 | preferenceHandler.themeType = theme
143 | })
144 | }
145 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/views/settings/screens/appearance/AppearanceViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.views.settings.screens.appearance
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import androidx.compose.material3.ColorScheme
6 | import androidx.compose.material3.dynamicDarkColorScheme
7 | import androidx.compose.material3.dynamicLightColorScheme
8 | import androidx.compose.material3.surfaceColorAtElevation
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.graphics.toArgb
11 | import androidx.compose.ui.unit.dp
12 | import androidx.lifecycle.ViewModel
13 | import com.chouten.app.PrimaryDataLayer
14 | import com.chouten.app.data.AppPaths
15 | import com.chouten.app.data.SnackbarVisualsWithError
16 | import com.chouten.app.ui.views.settings.json
17 | import kotlinx.serialization.encodeToString
18 | import kotlin.reflect.full.declaredMemberProperties
19 |
20 | class AppearanceViewModel(context: Context, colorScheme: ColorScheme) : ViewModel() {
21 |
22 | private val themes: Pair?
23 | private val surfaceContainer: Color
24 |
25 | init {
26 | this.surfaceContainer = colorScheme.surfaceColorAtElevation(3.dp)
27 | if (Build.VERSION.SDK_INT >= 31) {
28 | this.themes = Pair(
29 | dynamicLightColorScheme(context), dynamicDarkColorScheme(context)
30 | )
31 | } else {
32 | this.themes = null
33 | }
34 | }
35 |
36 | fun exportTheme(themeName: String): Boolean {
37 | if (themes == null) {
38 | PrimaryDataLayer.enqueueSnackbar(
39 | SnackbarVisualsWithError(
40 | "Android 12 or higher is required to export themes", false
41 | )
42 | )
43 | return false
44 | }
45 | val getColours: ((ColorScheme) -> Map) = { theme ->
46 | theme::class.declaredMemberProperties.associate { member ->
47 | val hexColor =
48 | Integer.toHexString((member.getter.call(theme) as Color).toArgb()).drop(2)
49 | if (member.name.startsWith("on")) {
50 | member.name to "#$hexColor"
51 | } else {
52 | member.name.replaceFirstChar { it.uppercase() } to "#$hexColor"
53 | }
54 | }
55 | }
56 |
57 | val lightPairs = getColours(themes.first).toMutableMap().apply {
58 | this["SurfaceContainer"] = Integer.toHexString(surfaceContainer.toArgb()).drop(2)
59 | }
60 | val darkPairs = getColours(themes.second).toMutableMap().apply {
61 | this["SurfaceContainer"] = Integer.toHexString(surfaceContainer.toArgb()).drop(2)
62 | }
63 |
64 | val exportedJson = json.encodeToString(
65 | mapOf(
66 | "light" to lightPairs, "dark" to darkPairs
67 | )
68 | )
69 |
70 | val themeDir = AppPaths.addedDirs.getOrElse("Themes") {
71 | throw Exception("Themes directory not found")
72 | }
73 |
74 | val export = {
75 | val themeFile = themeDir.resolve("${themeName.trim()}.theme")
76 | themeFile.createNewFile()
77 | themeFile.writeText(exportedJson)
78 | true
79 | }
80 |
81 | return try {
82 | export()
83 | } catch (e: Exception) {
84 | // Create the app folders
85 | themeDir.mkdir()
86 | try {
87 | export()
88 | } catch (e: Exception) {
89 | e.printStackTrace()
90 | false
91 | }
92 | }
93 | }
94 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/views/watch/WatchPage.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.views.watch
2 |
3 | import android.app.Activity
4 | import android.content.pm.ActivityInfo
5 | import android.net.Uri
6 | import android.view.ViewGroup
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.DisposableEffect
9 | import androidx.compose.runtime.LaunchedEffect
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.runtime.saveable.rememberSaveable
12 | import androidx.compose.ui.platform.LocalContext
13 | import androidx.compose.ui.viewinterop.AndroidView
14 | import androidx.media3.common.C
15 | import androidx.media3.common.MediaItem
16 | import androidx.media3.exoplayer.ExoPlayer
17 | import androidx.media3.ui.AspectRatioFrameLayout
18 | import androidx.media3.ui.PlayerView
19 | import com.chouten.app.findActivity
20 |
21 | @Composable
22 | @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
23 | fun WatchPage(
24 | provider: WatchPageViewModel
25 | ) {
26 | // Force the user's screen to
27 | // be oriented to landscape.
28 | //LockScreenOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
29 |
30 | val context = LocalContext.current
31 | val orientationOnLoad = rememberSaveable {
32 | (context as Activity).requestedOrientation
33 | }
34 |
35 | if (provider.sources.isNotEmpty()) {
36 | val _player = remember {
37 | ExoPlayer.Builder(
38 | context
39 | ).build()
40 | .apply {
41 | setMediaItem(
42 | MediaItem.fromUri(
43 | Uri.parse(
44 | provider.sources.first().file
45 | )
46 | )
47 | )
48 | prepare()
49 | }
50 | }
51 | _player.playWhenReady = true
52 | _player.videoScalingMode =
53 | C.VIDEO_SCALING_MODE_SCALE_TO_FIT
54 | _player.repeatMode = ExoPlayer.REPEAT_MODE_OFF
55 |
56 |
57 | LaunchedEffect((context as Activity).requestedOrientation) {
58 | context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
59 | }
60 |
61 | DisposableEffect(
62 | AndroidView(
63 | factory = {
64 | PlayerView(context).apply {
65 | resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL
66 | player = _player
67 | layoutParams = ViewGroup.LayoutParams(
68 | ViewGroup.LayoutParams.MATCH_PARENT,
69 | ViewGroup.LayoutParams.MATCH_PARENT
70 | )
71 | }
72 | }
73 | )
74 | ) {
75 | // Restore the user's screen to
76 | // its original orientation.
77 | context.requestedOrientation = orientationOnLoad
78 |
79 | onDispose {
80 | _player.release()
81 | }
82 | }
83 | }
84 | }
85 |
86 | @Composable
87 | fun LockScreenOrientation(orientation: Int) {
88 | val context = LocalContext.current
89 | DisposableEffect(orientation) {
90 | val activity = context.findActivity() ?: return@DisposableEffect onDispose {}
91 | val originalOrientation = activity.requestedOrientation
92 | activity.requestedOrientation = orientation
93 | onDispose {
94 | // restore original orientation when view disappears
95 | activity.requestedOrientation = originalOrientation
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/app/src/main/java/com/chouten/app/ui/views/watch/WatchPageViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app.ui.views.watch
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.setValue
8 | import androidx.lifecycle.ViewModel
9 | import androidx.lifecycle.viewModelScope
10 | import com.chouten.app.Mapper
11 | import com.chouten.app.ModuleLayer
12 | import com.chouten.app.PrimaryDataLayer
13 | import com.chouten.app.data.ModuleResponse
14 | import com.chouten.app.data.SnackbarVisualsWithError
15 | import com.chouten.app.data.WatchResult
16 | import com.chouten.app.data.WebviewHandler
17 | import java.net.URLDecoder
18 | import kotlinx.coroutines.launch
19 | import kotlinx.coroutines.sync.Mutex
20 |
21 | class WatchPageViewModel(
22 | context: Context,
23 | private var title: String? = "",
24 | private var name: String? = "",
25 | _url: String
26 | ) : ViewModel() {
27 | var url: String = ""
28 | private set
29 |
30 | val webview = WebviewHandler()
31 |
32 | private val syncLock = Mutex(false)
33 |
34 | private var _mediaUrl by mutableStateOf("")
35 | val mediaUrl: String
36 | get() = _mediaUrl
37 |
38 | private var _servers by mutableStateOf(listOf())
39 | val servers: List
40 | get() = _servers
41 |
42 | private var _sources by mutableStateOf(listOf())
43 | val sources: List
44 | get() = _sources
45 |
46 | private var _subtitles by mutableStateOf(listOf())
47 | val subtitles: List
48 | get() = _subtitles
49 |
50 | private var _skips by mutableStateOf(listOf())
51 | val skips: List
52 | get() = _skips
53 |
54 |
55 | init {
56 |
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_chouten_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_chouten_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_chouten_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-hdpi/ic_chouten_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_chouten_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-hdpi/ic_chouten_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_chouten_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-hdpi/ic_chouten_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_chouten_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-mdpi/ic_chouten_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_chouten_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-mdpi/ic_chouten_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_chouten_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-mdpi/ic_chouten_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_chouten_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-xhdpi/ic_chouten_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_chouten_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-xhdpi/ic_chouten_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_chouten_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-xhdpi/ic_chouten_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_chouten_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-xxhdpi/ic_chouten_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_chouten_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-xxhdpi/ic_chouten_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_chouten_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-xxhdpi/ic_chouten_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_chouten_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-xxxhdpi/ic_chouten_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_chouten_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-xxxhdpi/ic_chouten_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_chouten_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-xxxhdpi/ic_chouten_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-night/splashscreen_dark.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v31/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | @android:color/system_accent1_0
6 | @android:color/system_accent1_10
7 | @android:color/system_accent1_50
8 | @android:color/system_accent1_100
9 | @android:color/system_accent1_200
10 | @android:color/system_accent1_300
11 | @android:color/system_accent1_400
12 | @android:color/system_accent1_500
13 | @android:color/system_accent1_600
14 | @android:color/system_accent1_700
15 | @android:color/system_accent1_800
16 | @android:color/system_accent1_900
17 | @android:color/system_accent1_1000
18 |
19 |
20 | @android:color/system_accent2_0
21 | @android:color/system_accent2_10
22 | @android:color/system_accent2_50
23 | @android:color/system_accent2_100
24 | @android:color/system_accent2_200
25 | @android:color/system_accent2_300
26 | @android:color/system_accent2_400
27 | @android:color/system_accent2_500
28 | @android:color/system_accent2_600
29 | @android:color/system_accent2_700
30 | @android:color/system_accent2_800
31 | @android:color/system_accent2_900
32 | @android:color/system_accent2_1000
33 |
34 |
35 | @android:color/system_accent2_0
36 | @android:color/system_accent2_10
37 | @android:color/system_accent2_50
38 | @android:color/system_accent2_100
39 | @android:color/system_accent2_200
40 | @android:color/system_accent2_300
41 | @android:color/system_accent2_400
42 | @android:color/system_accent2_500
43 | @android:color/system_accent2_600
44 | @android:color/system_accent2_700
45 | @android:color/system_accent2_800
46 | @android:color/system_accent2_900
47 | @android:color/system_accent2_1000
48 |
49 |
50 | @android:color/system_accent2_0
51 | @android:color/system_accent2_10
52 | @android:color/system_accent2_50
53 | @android:color/system_accent2_100
54 | @android:color/system_accent2_200
55 | @android:color/system_accent2_300
56 | @android:color/system_accent2_400
57 | @android:color/system_accent2_500
58 | @android:color/system_accent2_600
59 | @android:color/system_accent2_700
60 | @android:color/system_accent2_800
61 | @android:color/system_accent2_900
62 | @android:color/system_accent2_1000
63 |
64 |
65 | @android:color/system_neutral1_0
66 | @android:color/system_neutral1_10
67 | @android:color/system_neutral1_50
68 | @android:color/system_neutral1_100
69 |
70 | @android:color/system_neutral1_200
71 |
72 | @android:color/system_neutral1_300
73 |
74 | @android:color/system_neutral1_400
75 |
76 | @android:color/system_neutral1_500
77 |
78 | @android:color/system_neutral1_600
79 |
80 | @android:color/system_neutral1_700
81 |
82 | @android:color/system_neutral1_800
83 |
84 | @android:color/system_neutral1_900
85 |
86 | @android:color/system_neutral1_1000
87 |
88 |
89 | @android:color/system_neutral2_0
90 | @android:color/system_neutral2_10
91 | @android:color/system_neutral2_50
92 | @android:color/system_neutral2_100
93 |
94 | @android:color/system_neutral2_200
95 |
96 | @android:color/system_neutral2_300
97 |
98 | @android:color/system_neutral2_400
99 |
100 | @android:color/system_neutral2_500
101 |
102 | @android:color/system_neutral2_600
103 |
104 | @android:color/system_neutral2_700
105 |
106 | @android:color/system_neutral2_800
107 |
108 | @android:color/system_neutral2_900
109 |
110 | @android:color/system_neutral2_1000
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_chouten_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #000000
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/splashscreen.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Chouten
3 | Module Selection
4 | Select one of the modules below to provide this app with data:
5 | No Module
6 | Import Module
7 | Import Theme
8 | Import Module from URL
9 | Import Module
10 | Import Module or a Repo from a URL. Please make sure you trust the URL.
11 | Import Theme or a Repo from a URL. Please make sure you trust the URL.
12 |
13 | Search for Content
14 | Search with
15 | No Module Selected
16 |
17 | Your Profile
18 |
19 | Ok
20 | Confirm
21 | Confirm Selection
22 | Cancel
23 | Import
24 |
25 |
26 | Home
27 | Search
28 | 頂点
29 | More
30 |
31 | App Logs
32 |
33 | Back
34 |
35 |
36 | Dynamic Theming
37 | Use Material You Dynamic Theming
38 |
39 | Toggle Downloaded Only
40 | Go offline and stuff you know
41 |
42 | Toggle Incognito
43 | For the naughty naughty
44 |
45 | Toggle dev mode
46 | Do not toggle, unless you know what you are doing
47 |
48 | Export Theme
49 | Export Theme as File
50 | Theme Exported Successfully
51 | Theme Name
52 | Could not export Theme
53 |
54 | Appearance
55 | Light/Dark/System
56 |
57 | Network
58 | Network Settings
59 | DNS
60 | Change DNS
61 |
62 | View Logs
63 | View module logs
64 | Clear Logs
65 | No logs… Yay?
66 | OLED Theme
67 | Enable a darker theme to save battery on certain devices
68 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/chouten/app/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.chouten.app
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | compose_version = '1.4.3'
4 | kotlin_version = '1.8.20'
5 | accompanist_version = '0.30.1'
6 | }
7 |
8 | repositories {
9 | google()
10 | mavenCentral()
11 | maven { url 'https://jitpack.io' }
12 | }
13 |
14 | }// Top-level build file where you can add configuration options common to all sub-projects/modules.
15 |
16 | plugins {
17 | id 'com.android.application' version '8.1.0' apply false
18 | id 'com.android.library' version '8.1.0' apply false
19 | id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
20 | id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.10'
21 | id "org.jetbrains.kotlin.jvm" version "1.8.10"
22 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ## For more details on how to configure your build environment visit
2 | # http://www.gradle.org/docs/current/userguide/build_environment.html
3 | #
4 | # Specifies the JVM arguments used for the daemon process.
5 | # The setting is particularly useful for tweaking memory settings.
6 | # Default value: -Xmx1024m -XX:MaxPermSize=256m
7 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
8 | #
9 | # When configured, Gradle will run in incubating parallel mode.
10 | # This option should only be used with decoupled projects. More details, visit
11 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
12 | # org.gradle.parallel=true
13 | # AndroidX package structure to make it clearer which packages are bundled with the
14 | # Android operating system, and which are packaged with your app's APK
15 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
16 | android.useAndroidX=true
17 | org.gradle.jvmargs=-Xmx4096m
18 | kotlin.code.style=official
19 | # Enables namespacing of each library's R class so that its R class includes only the
20 | # resources declared in the library itself and none from the library's dependencies,
21 | # thereby reducing the size of the R class for that library
22 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Chouten-App/Chouten-Android/310d6d428f596e92cdfc2d8936b2c428b3af74f0/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Jun 03 14:44:03 EDT 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven { url 'https://jitpack.io' }
14 | }
15 | }
16 | rootProject.name = "Chouten"
17 | include ':app'
18 |
--------------------------------------------------------------------------------