├── .github └── workflows │ └── main.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinScripting.xml ├── kotlinc.xml ├── ktlint.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.de-DE.md ├── README.ja-JP.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro ├── schemas │ └── app.suhasdissa.memerize.backend.database.MemeDatabase │ │ ├── 4.json │ │ ├── 5.json │ │ └── 6.json └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── app │ │ └── suhasdissa │ │ └── memerize │ │ ├── AppContainer.kt │ │ ├── Destination.kt │ │ ├── MainActivity.kt │ │ ├── MemerizeApplication.kt │ │ ├── NavHost.kt │ │ ├── backend │ │ ├── apis │ │ │ ├── FileDownloadApi.kt │ │ │ ├── LemmyApi.kt │ │ │ ├── RedditApi.kt │ │ │ └── RedditVideoApi.kt │ │ ├── database │ │ │ ├── MemeDatabase.kt │ │ │ ├── dao │ │ │ │ ├── CommunityDAO.kt │ │ │ │ ├── LemmyMemeDAO.kt │ │ │ │ ├── RedditMemeDao.kt │ │ │ │ └── SubredditDAO.kt │ │ │ └── entity │ │ │ │ ├── AboutCommunity.kt │ │ │ │ ├── LemmyCommunity.kt │ │ │ │ ├── LemmyMeme.kt │ │ │ │ ├── Meme.kt │ │ │ │ ├── RedditCommunity.kt │ │ │ │ └── RedditMeme.kt │ │ ├── model │ │ │ ├── LemmyAbout.kt │ │ │ ├── LemmyResponse.kt │ │ │ ├── RedditAboutResponse.kt │ │ │ ├── RedditResponse.kt │ │ │ └── Sort.kt │ │ ├── repositories │ │ │ ├── CommunityRepository.kt │ │ │ ├── LemmyCommunityRepository.kt │ │ │ ├── LemmyMemeRepository.kt │ │ │ ├── MemeRepository.kt │ │ │ ├── RedditCommunityRepository.kt │ │ │ └── RedditMemeRepository.kt │ │ └── viewmodels │ │ │ ├── CheckUpdateViewModel.kt │ │ │ ├── LemmyCommunityViewModel.kt │ │ │ ├── LemmyViewModel.kt │ │ │ ├── PhotoViewModel.kt │ │ │ ├── PlayerViewModel.kt │ │ │ ├── RedditCommunityViewModel.kt │ │ │ ├── RedditViewModel.kt │ │ │ └── state │ │ │ ├── AboutCommunityState.kt │ │ │ └── MemeUiState.kt │ │ ├── ui │ │ ├── MemerizeApp.kt │ │ ├── components │ │ │ ├── CacheSizeDialog.kt │ │ │ ├── ErrorScreen.kt │ │ │ ├── HighlightCard.kt │ │ │ ├── ImageCard.kt │ │ │ ├── LoadingScreen.kt │ │ │ ├── MemeCard.kt │ │ │ ├── MemeGrid.kt │ │ │ ├── NavDrawerContent.kt │ │ │ ├── RetryScreen.kt │ │ │ ├── SettingItem.kt │ │ │ ├── SortBottomSheet.kt │ │ │ ├── SubredditCard.kt │ │ │ └── VideoCard.kt │ │ ├── screens │ │ │ ├── home │ │ │ │ ├── CommunityScreen.kt │ │ │ │ ├── HomeScreen.kt │ │ │ │ └── SubredditScreen.kt │ │ │ ├── primary │ │ │ │ ├── LemmyMemeScreen.kt │ │ │ │ └── RedditMemeScreen.kt │ │ │ ├── secondary │ │ │ │ ├── MemeFeedView.kt │ │ │ │ ├── PhotoView.kt │ │ │ │ └── VideoView.kt │ │ │ └── settings │ │ │ │ ├── AboutScreen.kt │ │ │ │ └── SettingsScreen.kt │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ └── utils │ │ ├── CheckUpdate.kt │ │ ├── OpenBrowser.kt │ │ ├── PlayerState.kt │ │ ├── Preferences.kt │ │ ├── RedditVideoDownloader.kt │ │ └── ShareUrl.kt │ ├── res │ ├── drawable │ │ ├── ic_broken_image.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_launcher_monochrome.xml │ │ ├── loading_img.xml │ │ └── reddit_placeholder.xml │ ├── mipmap-anydpi-v26 │ │ └── ic_launcher.xml │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ ├── values-de-rDE │ │ └── strings.xml │ ├── values-ja │ │ └── strings.xml │ ├── values │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── xml │ │ └── provider_paths.xml │ └── values-night │ └── themes.xml ├── build.gradle.kts ├── crowdin.yml ├── fastlane └── metadata │ └── android │ └── en-US │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ └── 5.png │ └── short_description.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── key.jks ├── logo.svg └── settings.gradle /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Create Apk 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | paths-ignore: 7 | - "README*.md" 8 | - "app/src/main/res/**" 9 | - ".github/**" 10 | push: 11 | paths-ignore: 12 | - "README*.md" 13 | - "app/src/main/res/**" 14 | - ".github/**" 15 | 16 | jobs: 17 | apk: 18 | name: Generate APK 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v1 23 | - name: Validate Gradle Wrapper 24 | uses: gradle/wrapper-validation-action@v1 25 | - name: Setup JDK 26 | uses: actions/setup-java@v3 27 | with: 28 | distribution: 'temurin' 29 | java-version: 17 30 | cache: "gradle" 31 | - name: Build APK 32 | run: bash ./gradlew assembleReleaseGithub --stacktrace 33 | - name: Upload APK 34 | uses: actions/upload-artifact@v1 35 | with: 36 | name: release 37 | path: app/build/outputs/apk/releaseGithub/ 38 | -------------------------------------------------------------------------------- /.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 | Memerize -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 42 | -------------------------------------------------------------------------------- /.idea/kotlinScripting.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2147483647 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/ktlint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | false 6 | 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.de-DE.md: -------------------------------------------------------------------------------- 1 | [日本語](README.ja-JP.md) 2 | 3 |
4 | App icon 5 |

Memerize

6 | Memerize is a handy meme viewer app for Reddit and Lemmy 7 |
8 | 9 |
10 | 11 |
12 | License 13 | Downloads 14 | Last commit 15 | Repo size 16 | Stars 17 |
18 |
19 | 20 | --- 21 | 22 |
23 | Screenshots 24 |

25 | 26 | 27 | 28 |

29 |

30 | 31 | 32 |

33 |
34 | 35 | ## Features 36 | - Material you dynamic theme with dark mode support 37 | - Add/Remove subreddits and lemmy communities as you like 38 | - Support catching memes for offline browsing. 39 | - Sort Top memes by time period (Today, This Week, This Month) 40 | - Supports sharing memes from the app 41 | - Allows custom download location 42 | 43 | ## Installation 44 | 45 | [Get it on GitHub](https://github.com/SuhasDissa/MemerizeApp/releases/latest) 48 | 49 | ## Useful Links 50 | 51 | 52 | -------------------------------------------------------------------------------- /README.ja-JP.md: -------------------------------------------------------------------------------- 1 | [English](README.md) 2 | 3 |
4 | App icon 5 |

Memerize

6 | MemerizeはRedditとLemmy用の便利なミーム閲覧アプリです 7 |
8 |
9 | 10 |
11 | License 12 | Downloads 13 | Last commit 14 | Repo size 15 | Stars 16 |
17 |
18 | 19 | --- 20 | 21 |
22 | スクリーンショット 23 |

24 | 25 | 26 | 27 |

28 |

29 | 30 | 31 |

32 |
33 | 34 | ## 特徴 35 | - ダークモードをサポートしたMaterial you dynamic theme 36 | - Subredditsやlemmyコミュニティを自由に追加/削除できます 37 | - オフライン閲覧のためのミームキャッチをサポート。 38 | - 人気のミームを期間ごとに並べ替えます(今日、今週、今月) 39 | - アプリからのミームの共有をサポート 40 | - ダウンロード場所のカスタマイズが可能 41 | 42 | ## インストール 43 | 44 | [Get it on GitHub](https://github.com/SuhasDissa/MemerizeApp/releases/latest) 47 | 48 | ## 役立つリンク 49 | 50 | 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [日本語](README.ja-JP.md) 2 | 3 |
4 | App icon 5 |

Memerize

6 | Memerize is a handy meme viewer app for Reddit and Lemmy 7 |
8 |
9 | 10 |
11 | License 12 | Downloads 13 | Last commit 14 | Repo size 15 | Stars 16 |
17 |
18 | 19 | --- 20 | 21 |
22 | Screenshots 23 |

24 | 25 | 26 | 27 |

28 |

29 | 30 | 31 |

32 |
33 | 34 | ## Features 35 | - Material you dynamic theme with dark mode support 36 | - Add/Remove subreddits and lemmy communities as you like 37 | - Support catching memes for offline browsing. 38 | - Sort Top memes by time period (Today, This Week, This Month) 39 | - Supports sharing memes from the app 40 | - Allows custom download location 41 | 42 | ## Installation 43 | 44 | [Get it on GitHub](https://github.com/SuhasDissa/MemerizeApp/releases/latest) 47 | 48 | ## Useful Links 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release/ 3 | release 4 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | kotlin("android") 4 | id("com.google.devtools.ksp") 5 | id("org.jetbrains.kotlin.plugin.serialization") version "1.8.21" 6 | } 7 | 8 | android { 9 | namespace = "app.suhasdissa.memerize" 10 | compileSdk = 34 11 | 12 | defaultConfig { 13 | applicationId = "app.suhasdissa.memerize" 14 | minSdk = 24 15 | targetSdk = 34 16 | versionCode = 24 17 | versionName = "2.4" 18 | 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | vectorDrawables { 21 | useSupportLibrary = true 22 | } 23 | ksp { 24 | arg("room.schemaLocation", "$projectDir/schemas") 25 | } 26 | } 27 | signingConfigs { 28 | create("release") { 29 | storeFile = file("../key.jks") 30 | storePassword = "lolcat" 31 | keyAlias = "key0" 32 | keyPassword = "lolcat" 33 | } 34 | } 35 | buildTypes { 36 | getByName("release") { 37 | isMinifyEnabled = true 38 | isShrinkResources = true 39 | proguardFiles( 40 | getDefaultProguardFile("proguard-android-optimize.txt"), 41 | "proguard-rules.pro" 42 | ) 43 | } 44 | getByName("debug") { 45 | applicationIdSuffix = ".debug" 46 | isDebuggable = true 47 | } 48 | create("releaseGithub") { 49 | isMinifyEnabled = true 50 | isShrinkResources = true 51 | signingConfig = signingConfigs.getByName("release") 52 | proguardFiles( 53 | getDefaultProguardFile("proguard-android-optimize.txt"), 54 | "proguard-rules.pro" 55 | ) 56 | } 57 | } 58 | compileOptions { 59 | sourceCompatibility = JavaVersion.VERSION_17 60 | targetCompatibility = JavaVersion.VERSION_17 61 | } 62 | kotlinOptions { 63 | jvmTarget = "17" 64 | } 65 | buildFeatures { 66 | compose = true 67 | buildConfig = true 68 | } 69 | composeOptions { 70 | kotlinCompilerExtensionVersion = "1.4.7" 71 | } 72 | packaging { 73 | resources { 74 | excludes.add("/META-INF/{AL2.0,LGPL2.1}") 75 | } 76 | } 77 | } 78 | 79 | dependencies { 80 | implementation("androidx.core:core-ktx:1.12.0") 81 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") 82 | implementation("androidx.activity:activity-compose:1.8.0") 83 | implementation(platform("androidx.compose:compose-bom:2023.10.00")) 84 | implementation("androidx.compose.ui:ui") 85 | implementation("androidx.compose.ui:ui-graphics") 86 | implementation("androidx.compose.ui:ui-tooling-preview") 87 | implementation("androidx.compose.material3:material3") 88 | 89 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2") 90 | implementation("androidx.navigation:navigation-compose:2.7.4") 91 | 92 | implementation("androidx.compose.material:material-icons-extended:1.5.3") 93 | 94 | testImplementation("junit:junit:4.13.2") 95 | androidTestImplementation("androidx.test.ext:junit:1.1.5") 96 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") 97 | androidTestImplementation(platform("androidx.compose:compose-bom:2022.10.00")) 98 | androidTestImplementation("androidx.compose.ui:ui-test-junit4") 99 | debugImplementation("androidx.compose.ui:ui-tooling") 100 | debugImplementation("androidx.compose.ui:ui-test-manifest") 101 | 102 | implementation("androidx.documentfile:documentfile:1.0.1") 103 | 104 | implementation("io.coil-kt:coil-compose:2.4.0") 105 | 106 | val media3_version = "1.1.1" 107 | 108 | // For media playback using ExoPlayer 109 | implementation("androidx.media3:media3-exoplayer:$media3_version") 110 | // For HLS playback support with ExoPlayer 111 | implementation("androidx.media3:media3-exoplayer-dash:$media3_version") 112 | // For building media playback UIs 113 | implementation("androidx.media3:media3-ui:$media3_version") 114 | 115 | implementation("androidx.media3:media3-session:$media3_version") 116 | 117 | val roomVersion = "2.5.2" 118 | 119 | implementation("androidx.room:room-runtime:$roomVersion") 120 | implementation("androidx.room:room-ktx:$roomVersion") 121 | annotationProcessor("androidx.room:room-compiler:$roomVersion") 122 | ksp("androidx.room:room-compiler:$roomVersion") 123 | 124 | implementation("org.burnoutcrew.composereorderable:reorderable:0.9.6") 125 | 126 | implementation("com.squareup.retrofit2:retrofit:2.9.0") 127 | implementation("com.squareup.retrofit2:converter-scalars:2.9.0") 128 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") 129 | implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0") 130 | } 131 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation 2 | 3 | -keep class retrofit2.** { *; } 4 | -keepattributes *Annotation* 5 | -keep class com.squareup.okhttp.** { *; } 6 | -keep interface com.squareup.okhttp.** { *; } 7 | -keep class okhttp3.** { *; } 8 | -keep interface okhttp3.** { *; } 9 | 10 | -keepattributes RuntimeVisibleAnnotations,AnnotationDefault 11 | 12 | -dontwarn okhttp3.internal.platform.** 13 | -dontwarn org.conscrypt.** 14 | -dontwarn org.bouncycastle.** 15 | -dontwarn org.openjsse.** -------------------------------------------------------------------------------- /app/schemas/app.suhasdissa.memerize.backend.database.MemeDatabase/5.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 5, 5 | "identityHash": "e2fc7824a14602ba5a702fa14f828362", 6 | "entities": [ 7 | { 8 | "tableName": "reddit_table", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL DEFAULT '', `is_video` INTEGER NOT NULL, `preview` TEXT NOT NULL DEFAULT '', `subreddit` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "id", 13 | "columnName": "id", 14 | "affinity": "TEXT", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "url", 19 | "columnName": "url", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "title", 25 | "columnName": "title", 26 | "affinity": "TEXT", 27 | "notNull": true, 28 | "defaultValue": "''" 29 | }, 30 | { 31 | "fieldPath": "isVideo", 32 | "columnName": "is_video", 33 | "affinity": "INTEGER", 34 | "notNull": true 35 | }, 36 | { 37 | "fieldPath": "preview", 38 | "columnName": "preview", 39 | "affinity": "TEXT", 40 | "notNull": true, 41 | "defaultValue": "''" 42 | }, 43 | { 44 | "fieldPath": "subreddit", 45 | "columnName": "subreddit", 46 | "affinity": "TEXT", 47 | "notNull": true, 48 | "defaultValue": "''" 49 | } 50 | ], 51 | "primaryKey": { 52 | "autoGenerate": false, 53 | "columnNames": [ 54 | "id" 55 | ] 56 | }, 57 | "indices": [], 58 | "foreignKeys": [] 59 | }, 60 | { 61 | "tableName": "subreddit", 62 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `icon_url` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`id`))", 63 | "fields": [ 64 | { 65 | "fieldPath": "id", 66 | "columnName": "id", 67 | "affinity": "TEXT", 68 | "notNull": true 69 | }, 70 | { 71 | "fieldPath": "iconUrl", 72 | "columnName": "icon_url", 73 | "affinity": "TEXT", 74 | "notNull": false 75 | }, 76 | { 77 | "fieldPath": "name", 78 | "columnName": "name", 79 | "affinity": "TEXT", 80 | "notNull": true 81 | } 82 | ], 83 | "primaryKey": { 84 | "autoGenerate": false, 85 | "columnNames": [ 86 | "id" 87 | ] 88 | }, 89 | "indices": [], 90 | "foreignKeys": [] 91 | }, 92 | { 93 | "tableName": "lemmy_table", 94 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL DEFAULT '', `is_video` INTEGER NOT NULL, `preview` TEXT NOT NULL DEFAULT '', `community` TEXT NOT NULL DEFAULT '', `instance` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))", 95 | "fields": [ 96 | { 97 | "fieldPath": "id", 98 | "columnName": "id", 99 | "affinity": "TEXT", 100 | "notNull": true 101 | }, 102 | { 103 | "fieldPath": "url", 104 | "columnName": "url", 105 | "affinity": "TEXT", 106 | "notNull": true 107 | }, 108 | { 109 | "fieldPath": "title", 110 | "columnName": "title", 111 | "affinity": "TEXT", 112 | "notNull": true, 113 | "defaultValue": "''" 114 | }, 115 | { 116 | "fieldPath": "isVideo", 117 | "columnName": "is_video", 118 | "affinity": "INTEGER", 119 | "notNull": true 120 | }, 121 | { 122 | "fieldPath": "preview", 123 | "columnName": "preview", 124 | "affinity": "TEXT", 125 | "notNull": true, 126 | "defaultValue": "''" 127 | }, 128 | { 129 | "fieldPath": "community", 130 | "columnName": "community", 131 | "affinity": "TEXT", 132 | "notNull": true, 133 | "defaultValue": "''" 134 | }, 135 | { 136 | "fieldPath": "instance", 137 | "columnName": "instance", 138 | "affinity": "TEXT", 139 | "notNull": true, 140 | "defaultValue": "''" 141 | } 142 | ], 143 | "primaryKey": { 144 | "autoGenerate": false, 145 | "columnNames": [ 146 | "id" 147 | ] 148 | }, 149 | "indices": [], 150 | "foreignKeys": [] 151 | }, 152 | { 153 | "tableName": "community", 154 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`community` TEXT NOT NULL, `instance` TEXT NOT NULL, `icon_url` TEXT, `name` TEXT NOT NULL, PRIMARY KEY(`community`, `instance`))", 155 | "fields": [ 156 | { 157 | "fieldPath": "id", 158 | "columnName": "community", 159 | "affinity": "TEXT", 160 | "notNull": true 161 | }, 162 | { 163 | "fieldPath": "instance", 164 | "columnName": "instance", 165 | "affinity": "TEXT", 166 | "notNull": true 167 | }, 168 | { 169 | "fieldPath": "iconUrl", 170 | "columnName": "icon_url", 171 | "affinity": "TEXT", 172 | "notNull": false 173 | }, 174 | { 175 | "fieldPath": "name", 176 | "columnName": "name", 177 | "affinity": "TEXT", 178 | "notNull": true 179 | } 180 | ], 181 | "primaryKey": { 182 | "autoGenerate": false, 183 | "columnNames": [ 184 | "community", 185 | "instance" 186 | ] 187 | }, 188 | "indices": [], 189 | "foreignKeys": [] 190 | } 191 | ], 192 | "views": [], 193 | "setupQueries": [ 194 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 195 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e2fc7824a14602ba5a702fa14f828362')" 196 | ] 197 | } 198 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 13 | 14 | 19 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuhasDissa/MemerizeApp/17ffdf78ce0b8e85aef31d31f3d8c4bd5f72b7c0/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/AppContainer.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/25/22, 7:00 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize 9 | 10 | import app.suhasdissa.memerize.backend.apis.LemmyApi 11 | import app.suhasdissa.memerize.backend.apis.RedditApi 12 | import app.suhasdissa.memerize.backend.database.MemeDatabase 13 | import app.suhasdissa.memerize.backend.repositories.LemmyCommunityRepository 14 | import app.suhasdissa.memerize.backend.repositories.LemmyCommunityRepositoryImpl 15 | import app.suhasdissa.memerize.backend.repositories.LemmyMemeRepository 16 | import app.suhasdissa.memerize.backend.repositories.LemmyMemeRepositoryImpl 17 | import app.suhasdissa.memerize.backend.repositories.RedditCommunityRepository 18 | import app.suhasdissa.memerize.backend.repositories.RedditCommunityRepositoryImpl 19 | import app.suhasdissa.memerize.backend.repositories.RedditMemeRepository 20 | import app.suhasdissa.memerize.backend.repositories.RedditMemeRepositoryImpl 21 | import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory 22 | import kotlinx.serialization.json.Json 23 | import okhttp3.MediaType.Companion.toMediaType 24 | import retrofit2.Retrofit 25 | 26 | interface AppContainer { 27 | val redditApi: RedditApi 28 | val lemmyApi: LemmyApi 29 | val redditMemeRepository: RedditMemeRepository 30 | val lemmyMemeRepository: LemmyMemeRepository 31 | val lemmyCommunityRepository: LemmyCommunityRepository 32 | val redditCommunityRepository: RedditCommunityRepository 33 | } 34 | 35 | class DefaultAppContainer(database: MemeDatabase) : AppContainer { 36 | override val redditMemeRepository: RedditMemeRepository by lazy { 37 | RedditMemeRepositoryImpl(database.redditMemeDao(), redditApi) 38 | } 39 | override val lemmyMemeRepository: LemmyMemeRepository by lazy { 40 | LemmyMemeRepositoryImpl(database.lemmyMemeDao(), lemmyApi) 41 | } 42 | override val lemmyCommunityRepository: LemmyCommunityRepository by lazy { 43 | LemmyCommunityRepositoryImpl(database.communityDao(), lemmyApi) 44 | } 45 | override val redditCommunityRepository: RedditCommunityRepository by lazy { 46 | RedditCommunityRepositoryImpl(database.subredditDao(), redditApi) 47 | } 48 | 49 | private val json = Json { ignoreUnknownKeys = true } 50 | 51 | private val redditRetrofit = Retrofit.Builder() 52 | .baseUrl("https://www.reddit.com/") 53 | .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) 54 | .build() 55 | 56 | private val lemmyRetrofit = Retrofit.Builder() 57 | .baseUrl("https://lemmy.ml/") 58 | .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) 59 | .build() 60 | 61 | override val redditApi: RedditApi by lazy { 62 | redditRetrofit.create(RedditApi::class.java) 63 | } 64 | 65 | override val lemmyApi: LemmyApi by lazy { 66 | lemmyRetrofit.create(LemmyApi::class.java) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/Destination.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize 9 | 10 | import androidx.navigation.NavType 11 | import androidx.navigation.navArgument 12 | 13 | sealed class Destination(val route: String) { 14 | object Home : Destination("home") 15 | object RedditMemeView : Destination("reddit_memeview") 16 | object LemmyMemeView : Destination("lemmy_memeview") 17 | object Settings : Destination("settings") 18 | object Subreddits : Destination("subreddits") 19 | object Communities : Destination("communities") 20 | object About : Destination("about") 21 | object RedditFeed : Destination("reddit_feed") { 22 | val routeWithArgs = "$route/{id}" 23 | val arguments = listOf(navArgument("id") { type = NavType.IntType }) 24 | } 25 | 26 | object LemmyFeed : Destination("lemmy_feed") { 27 | val routeWithArgs = "$route/{id}" 28 | val arguments = listOf(navArgument("id") { type = NavType.IntType }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize 9 | 10 | import android.os.Bundle 11 | import androidx.activity.ComponentActivity 12 | import androidx.activity.compose.setContent 13 | import app.suhasdissa.memerize.ui.MemerizeApp 14 | import app.suhasdissa.memerize.ui.theme.MemerizeTheme 15 | 16 | class MainActivity : ComponentActivity() { 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContent { 20 | MemerizeTheme { 21 | MemerizeApp() 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/MemerizeApplication.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/25/22, 6:27 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize 9 | 10 | import android.app.Application 11 | import app.suhasdissa.memerize.backend.database.MemeDatabase 12 | import app.suhasdissa.memerize.utils.UpdateUtil 13 | import app.suhasdissa.memerize.utils.defaultImageCacheSize 14 | import app.suhasdissa.memerize.utils.imageCacheKey 15 | import app.suhasdissa.memerize.utils.preferences 16 | import coil.ImageLoader 17 | import coil.ImageLoaderFactory 18 | import coil.disk.DiskCache 19 | 20 | class MemerizeApplication : Application(), ImageLoaderFactory { 21 | private val database by lazy { MemeDatabase.getDatabase(this) } 22 | lateinit var container: AppContainer 23 | 24 | override fun onCreate() { 25 | super.onCreate() 26 | container = DefaultAppContainer(database) 27 | UpdateUtil.getCurrentVersion(this.applicationContext) 28 | } 29 | 30 | override fun newImageLoader(): ImageLoader { 31 | return ImageLoader.Builder(this) 32 | .crossfade(true) 33 | .respectCacheHeaders(false) 34 | .diskCache( 35 | DiskCache.Builder() 36 | .directory(cacheDir.resolve("image_cache")) 37 | .maxSizeBytes( 38 | preferences.getInt(imageCacheKey, defaultImageCacheSize) * 1024 * 1024L 39 | ) 40 | .build() 41 | ).build() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/NavHost.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize 9 | 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.navigation.NavHostController 13 | import androidx.navigation.compose.NavHost 14 | import androidx.navigation.compose.composable 15 | import app.suhasdissa.memerize.ui.screens.home.CommunityScreen 16 | import app.suhasdissa.memerize.ui.screens.home.HomeScreen 17 | import app.suhasdissa.memerize.ui.screens.home.SubredditScreen 18 | import app.suhasdissa.memerize.ui.screens.primary.LemmyMemeScreen 19 | import app.suhasdissa.memerize.ui.screens.primary.RedditMemeScreen 20 | import app.suhasdissa.memerize.ui.screens.secondary.LemmyMemeFeed 21 | import app.suhasdissa.memerize.ui.screens.secondary.RedditMemeFeed 22 | import app.suhasdissa.memerize.ui.screens.settings.AboutScreen 23 | import app.suhasdissa.memerize.ui.screens.settings.SettingsScreen 24 | 25 | @Composable 26 | fun AppNavHost( 27 | navController: NavHostController, 28 | onDrawerOpen: () -> Unit, 29 | modifier: Modifier = Modifier 30 | ) { 31 | NavHost( 32 | navController = navController, 33 | startDestination = Destination.Home.route, 34 | modifier = modifier 35 | ) { 36 | composable(route = Destination.Home.route) { 37 | HomeScreen( 38 | onNavigate = { destination -> 39 | navController.navigateTo(destination.route) 40 | }, 41 | onDrawerOpen 42 | ) 43 | } 44 | composable(route = Destination.Settings.route) { 45 | SettingsScreen( 46 | onDrawerOpen, 47 | onAboutClick = { 48 | navController.navigateTo(Destination.About.route) 49 | } 50 | ) 51 | } 52 | composable(route = Destination.Subreddits.route) { 53 | SubredditScreen(onDrawerOpen) 54 | } 55 | composable(route = Destination.Communities.route) { 56 | CommunityScreen(onDrawerOpen) 57 | } 58 | composable(route = Destination.About.route) { 59 | AboutScreen() 60 | } 61 | composable( 62 | route = Destination.RedditMemeView.route 63 | ) { 64 | RedditMemeScreen( 65 | onClickCard = { id -> 66 | navController.navigateTo("${Destination.RedditFeed.route}/$id") 67 | } 68 | ) 69 | } 70 | composable( 71 | route = Destination.LemmyMemeView.route 72 | ) { 73 | LemmyMemeScreen( 74 | onClickCard = { id -> 75 | navController.navigateTo("${Destination.LemmyFeed.route}/$id") 76 | } 77 | ) 78 | } 79 | composable( 80 | route = Destination.RedditFeed.routeWithArgs, 81 | arguments = Destination.RedditFeed.arguments 82 | ) { 83 | val id = it.arguments?.getInt("id") 84 | if (id != null) { 85 | RedditMemeFeed(initialPage = id) 86 | } 87 | } 88 | 89 | composable( 90 | route = Destination.LemmyFeed.routeWithArgs, 91 | arguments = Destination.LemmyFeed.arguments 92 | ) { 93 | val id = it.arguments?.getInt("id") 94 | if (id != null) { 95 | LemmyMemeFeed(initialPage = id) 96 | } 97 | } 98 | } 99 | } 100 | 101 | fun NavHostController.navigateTo(route: String) = this.navigate(route) { 102 | launchSingleTop = true 103 | restoreState = true 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/apis/FileDownloadApi.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/7/23, 9:34 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.apis 9 | 10 | import okhttp3.ResponseBody 11 | import retrofit2.Call 12 | import retrofit2.http.GET 13 | import retrofit2.http.Url 14 | 15 | interface FileDownloadApi { 16 | @GET 17 | fun downloadFile(@Url fileUrl: String): Call 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/apis/LemmyApi.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/3/23, 4:40 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.apis 9 | 10 | import app.suhasdissa.memerize.backend.model.LemmyAbout 11 | import app.suhasdissa.memerize.backend.model.LemmyResponse 12 | import retrofit2.http.GET 13 | import retrofit2.http.Headers 14 | import retrofit2.http.Path 15 | import retrofit2.http.Query 16 | 17 | private const val header = 18 | "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36" // ktlint-disable max-line-length 19 | 20 | interface LemmyApi { 21 | /** 22 | * @param community The name of the community. 23 | * @param sort "Active", "Hot", "MostComments", "New", "NewComments", 24 | * "Old", "TopAll", "TopDay", "TopMonth", "TopWeek", 25 | * "TopYear". 26 | */ 27 | @Headers(header) 28 | @GET("https://{instance}/api/v3/post/list") 29 | suspend fun getLemmyData( 30 | @Path("instance") instance: String, 31 | @Query("community_name") community: String, 32 | @Query("sort") sort: String 33 | ): LemmyResponse 34 | 35 | @Headers(header) 36 | @GET("https://{instance}/api/v3/community") 37 | suspend fun getCommunity( 38 | @Path("instance") instance: String, 39 | @Query("name") name: String 40 | ): LemmyAbout 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/apis/RedditApi.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.apis 9 | 10 | import app.suhasdissa.memerize.backend.model.Reddit 11 | import app.suhasdissa.memerize.backend.model.RedditAboutResponse 12 | import retrofit2.http.GET 13 | import retrofit2.http.Headers 14 | import retrofit2.http.Path 15 | import retrofit2.http.Query 16 | 17 | private const val header = 18 | "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36" // ktlint-disable max-line-length 19 | 20 | interface RedditApi { 21 | @Headers(header) 22 | @GET("r/{subreddit}/{sort}.json") 23 | suspend fun getRedditData( 24 | @Path("subreddit") subreddit: String, 25 | @Path("sort") sort: String, 26 | @Query("t") time: String? = null 27 | ): Reddit 28 | 29 | @Headers(header) 30 | @GET("r/{subreddit}/about.json") 31 | suspend fun getAboutSubreddit( 32 | @Path("subreddit") subreddit: String 33 | ): RedditAboutResponse 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/apis/RedditVideoApi.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/7/23, 6:41 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.apis 9 | 10 | import retrofit2.http.GET 11 | import retrofit2.http.Headers 12 | import retrofit2.http.Url 13 | 14 | private const val header = 15 | "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36" // ktlint-disable max-line-length 16 | 17 | interface RedditVideoApi { 18 | @Headers(header) 19 | @GET 20 | suspend fun getRedditData( 21 | @Url url: String 22 | ): String 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/database/MemeDatabase.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.memerize.backend.database 2 | 3 | import android.content.Context 4 | import androidx.room.AutoMigration 5 | import androidx.room.Database 6 | import androidx.room.Room 7 | import androidx.room.RoomDatabase 8 | import androidx.sqlite.db.SupportSQLiteDatabase 9 | import app.suhasdissa.memerize.backend.database.dao.CommunityDAO 10 | import app.suhasdissa.memerize.backend.database.dao.LemmyMemeDAO 11 | import app.suhasdissa.memerize.backend.database.dao.RedditMemeDao 12 | import app.suhasdissa.memerize.backend.database.dao.SubredditDAO 13 | import app.suhasdissa.memerize.backend.database.entity.LemmyCommunity 14 | import app.suhasdissa.memerize.backend.database.entity.LemmyMeme 15 | import app.suhasdissa.memerize.backend.database.entity.RedditCommunity 16 | import app.suhasdissa.memerize.backend.database.entity.RedditMeme 17 | 18 | @Database( 19 | entities = [RedditMeme::class, RedditCommunity::class, LemmyMeme::class, LemmyCommunity::class], 20 | version = 6, 21 | exportSchema = true, 22 | autoMigrations = [ 23 | AutoMigration(from = 4, to = 5), 24 | AutoMigration(from = 5, to = 6) 25 | ] 26 | ) 27 | abstract class MemeDatabase : RoomDatabase() { 28 | 29 | abstract fun redditMemeDao(): RedditMemeDao 30 | abstract fun subredditDao(): SubredditDAO 31 | abstract fun communityDao(): CommunityDAO 32 | abstract fun lemmyMemeDao(): LemmyMemeDAO 33 | 34 | companion object { 35 | @Volatile 36 | private var INSTANCE: MemeDatabase? = null 37 | 38 | fun getDatabase(context: Context): MemeDatabase { 39 | return INSTANCE ?: synchronized(this) { 40 | val instance = Room.databaseBuilder( 41 | context.applicationContext, 42 | MemeDatabase::class.java, 43 | "meme_database" 44 | ).allowMainThreadQueries() 45 | .addCallback(initSubreddits()) 46 | .build() 47 | INSTANCE = instance 48 | instance 49 | } 50 | } 51 | 52 | class initSubreddits : Callback() { 53 | val redditList = listOf( 54 | RedditCommunity( 55 | "maybemaybemaybe", 56 | "https://styles.redditmedia.com/t5_38e1l/styles/communityIcon_hcpveq6pu5p41.png", 57 | "Maybe Maybe Maybe" 58 | ), 59 | RedditCommunity( 60 | "holup", 61 | "https://styles.redditmedia.com/t5_qir9n/styles/communityIcon_yvasg0bnblaa1.png", 62 | "HolUP" 63 | ), 64 | RedditCommunity( 65 | "funny", 66 | "https://a.thumbs.redditmedia.com/kIpBoUR8zJLMQlF8azhN-kSBsjVUidHjvZNLuHDONm8.png", 67 | "Funny" 68 | ), 69 | RedditCommunity( 70 | "facepalm", 71 | "https://styles.redditmedia.com/t5_2r5rp/styles/communityIcon_qzjxzx1g08z91.jpg", 72 | "FacePalm" 73 | ), 74 | RedditCommunity( 75 | "memes", 76 | "https://styles.redditmedia.com/t5_2qjpg/styles/communityIcon_uzvo7sibvc3a1.jpg", 77 | "Memes" 78 | ), 79 | RedditCommunity( 80 | "dankmemes", 81 | "https://styles.redditmedia.com/t5_2zmfe/styles/communityIcon_g5xoywnpe2l91.png", 82 | "Dank Memes" 83 | ) 84 | ) 85 | 86 | override fun onCreate(db: SupportSQLiteDatabase) { 87 | redditList.forEach { 88 | db.execSQL( 89 | "INSERT INTO subreddit (id, icon_url, name) " + 90 | "VALUES ('${it.id}', '${it.iconUrl}', '${it.name}');" 91 | ) 92 | } 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/database/dao/CommunityDAO.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/3/23, 7:58 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.database.dao 9 | 10 | import androidx.room.Dao 11 | import androidx.room.Delete 12 | import androidx.room.Insert 13 | import androidx.room.OnConflictStrategy 14 | import androidx.room.Query 15 | import app.suhasdissa.memerize.backend.database.entity.LemmyCommunity 16 | import kotlinx.coroutines.flow.Flow 17 | 18 | @Dao 19 | interface CommunityDAO { 20 | @Query("SELECT * FROM community") 21 | fun getAll(): Flow> 22 | 23 | @Insert(onConflict = OnConflictStrategy.REPLACE) 24 | fun insertAll(community: List) 25 | 26 | @Insert(onConflict = OnConflictStrategy.REPLACE) 27 | fun insert(community: LemmyCommunity) 28 | 29 | @Delete 30 | fun delete(community: LemmyCommunity) 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/database/dao/LemmyMemeDAO.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/3/23, 8:01 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.database.dao 9 | 10 | import androidx.room.Dao 11 | import androidx.room.Insert 12 | import androidx.room.OnConflictStrategy 13 | import androidx.room.Query 14 | import app.suhasdissa.memerize.backend.database.entity.LemmyMeme 15 | 16 | @Dao 17 | interface LemmyMemeDAO { 18 | @Query("SELECT * FROM lemmy_table WHERE id=:community AND instance=:instance") 19 | fun getAll(community: String, instance: String): List 20 | 21 | @Insert(onConflict = OnConflictStrategy.REPLACE) 22 | fun insertAll(memes: List) 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/database/dao/RedditMemeDao.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 7/29/23, 8:14 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.database.dao 9 | 10 | import androidx.room.Dao 11 | import androidx.room.Delete 12 | import androidx.room.Insert 13 | import androidx.room.OnConflictStrategy 14 | import androidx.room.Query 15 | import app.suhasdissa.memerize.backend.database.entity.RedditMeme 16 | 17 | @Dao 18 | interface RedditMemeDao { 19 | @Query("SELECT * FROM reddit_table WHERE subreddit=:subreddit") 20 | fun getAll(subreddit: String): List 21 | 22 | @Insert(onConflict = OnConflictStrategy.IGNORE) 23 | fun insertAll(memes: List) 24 | 25 | @Delete 26 | fun delete(meme: RedditMeme) 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/database/dao/SubredditDAO.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 7/30/23, 12:36 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.database.dao 9 | 10 | import androidx.room.Dao 11 | import androidx.room.Delete 12 | import androidx.room.Insert 13 | import androidx.room.OnConflictStrategy 14 | import androidx.room.Query 15 | import app.suhasdissa.memerize.backend.database.entity.RedditCommunity 16 | import kotlinx.coroutines.flow.Flow 17 | 18 | @Dao 19 | interface SubredditDAO { 20 | @Query("SELECT * FROM subreddit") 21 | fun getAll(): Flow> 22 | 23 | @Insert(onConflict = OnConflictStrategy.REPLACE) 24 | fun insertAll(subreddits: List) 25 | 26 | @Insert(onConflict = OnConflictStrategy.REPLACE) 27 | fun insert(subreddits: RedditCommunity) 28 | 29 | @Delete 30 | fun delete(subreddit: RedditCommunity) 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/database/entity/AboutCommunity.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/4/23, 9:20 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.database.entity 9 | 10 | interface AboutCommunity { 11 | val id: String 12 | val name: String 13 | val iconUrl: String? 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/database/entity/LemmyCommunity.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 7/30/23, 12:30 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.database.entity 9 | 10 | import androidx.room.ColumnInfo 11 | import androidx.room.Entity 12 | 13 | @Entity(tableName = "community", primaryKeys = ["community", "instance"]) 14 | data class LemmyCommunity( 15 | @ColumnInfo(name = "community") override val id: String, 16 | @ColumnInfo(name = "instance") val instance: String, 17 | @ColumnInfo(name = "icon_url") override val iconUrl: String? = null, 18 | @ColumnInfo(name = "name") override val name: String = "" 19 | ) : AboutCommunity 20 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/database/entity/LemmyMeme.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.memerize.backend.database.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "lemmy_table") 8 | data class LemmyMeme( 9 | @PrimaryKey override val id: String, 10 | @ColumnInfo(name = "url") override val url: String, 11 | @ColumnInfo(name = "title", defaultValue = "") override val title: String, 12 | @ColumnInfo(name = "is_video") override val isVideo: Boolean, 13 | @ColumnInfo(name = "preview", defaultValue = "") override val preview: String, 14 | @ColumnInfo(name = "community", defaultValue = "") val community: String, 15 | @ColumnInfo(name = "instance", defaultValue = "") val instance: String, 16 | @ColumnInfo(name = "post_link", defaultValue = "NULL") override val postLink: String? 17 | ) : Meme 18 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/database/entity/Meme.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.memerize.backend.database.entity 2 | 3 | interface Meme { 4 | val id: String 5 | val url: String 6 | val title: String 7 | val isVideo: Boolean 8 | val preview: String 9 | val postLink: String? 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/database/entity/RedditCommunity.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 7/30/23, 12:30 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.database.entity 9 | 10 | import androidx.room.ColumnInfo 11 | import androidx.room.Entity 12 | import androidx.room.PrimaryKey 13 | 14 | @Entity(tableName = "subreddit") 15 | data class RedditCommunity( 16 | @PrimaryKey override val id: String, 17 | @ColumnInfo(name = "icon_url") override val iconUrl: String? = null, 18 | @ColumnInfo(name = "name") override val name: String = "" 19 | ) : AboutCommunity 20 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/database/entity/RedditMeme.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.memerize.backend.database.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "reddit_table") 8 | data class RedditMeme( 9 | @PrimaryKey override val id: String, 10 | @ColumnInfo(name = "url") override val url: String, 11 | @ColumnInfo(name = "title", defaultValue = "") override val title: String, 12 | @ColumnInfo(name = "is_video") override val isVideo: Boolean, 13 | @ColumnInfo(name = "preview", defaultValue = "") override val preview: String, 14 | @ColumnInfo(name = "subreddit", defaultValue = "") val subreddit: String, 15 | @ColumnInfo(name = "post_link", defaultValue = "NULL") override val postLink: String? 16 | ) : Meme 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/model/LemmyAbout.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.memerize.backend.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class LemmyAbout( 8 | @SerialName("community_view") val communityView: CommunityView? = CommunityView() 9 | ) 10 | 11 | @Serializable 12 | data class CommunityView( 13 | @SerialName("community") val community: Community? = Community() 14 | ) 15 | 16 | @Serializable 17 | data class Community( 18 | @SerialName("id") val id: Int? = null, 19 | @SerialName("name") val name: String? = null, 20 | @SerialName("title") val title: String? = null, 21 | @SerialName("icon") val icon: String? = null 22 | ) 23 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/model/LemmyResponse.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.memerize.backend.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class LemmyResponse( 8 | @SerialName("posts") val posts: ArrayList = arrayListOf() 9 | ) 10 | 11 | @Serializable 12 | data class Posts( 13 | @SerialName("post") val post: Post? = Post() 14 | ) 15 | 16 | @Serializable 17 | data class Post( 18 | @SerialName("id") val id: Int? = null, 19 | @SerialName("name") val name: String? = null, 20 | @SerialName("url") val url: String? = null, 21 | @SerialName("thumbnail_url") val thumbnailUrl: String? = null, 22 | @SerialName("ap_id") val postLink: String? = null 23 | ) 24 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/model/RedditAboutResponse.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.memerize.backend.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class RedditAboutResponse( 8 | @SerialName("data") val data: AboutData? = AboutData() 9 | ) 10 | 11 | @Serializable 12 | data class AboutData( 13 | @SerialName("community_icon") val communityIcon: String? = null, 14 | @SerialName("display_name") val displayName: String? = null, 15 | @SerialName("display_name_prefixed") val displayNamePrefixed: String? = null 16 | ) { 17 | val communityIconUrl 18 | get() = communityIcon?.replace("&", "&") 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/model/RedditResponse.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.memerize.backend.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Reddit( 8 | @SerialName("data") val data: Data? = Data() 9 | ) 10 | 11 | @Serializable 12 | data class Data( 13 | @SerialName("children") val children: ArrayList? = arrayListOf() 14 | ) 15 | 16 | @Serializable 17 | data class Children( 18 | @SerialName("data") val childdata: ChildData? = ChildData() 19 | ) 20 | 21 | @Serializable 22 | data class ChildData( 23 | @SerialName("title") val title: String? = null, 24 | @SerialName("secure_media") val secure_media: SecureMedia? = SecureMedia(), 25 | @SerialName("url") val url: String? = null, 26 | @SerialName("permalink") val permalink: String? = null, 27 | @SerialName("preview") val preview: Preview? = Preview() 28 | 29 | ) 30 | 31 | @Serializable 32 | data class SecureMedia( 33 | @SerialName("reddit_video") val reddit_video: RedditVideo? = RedditVideo() 34 | ) 35 | 36 | @Serializable 37 | data class RedditVideo( 38 | @SerialName("dash_url") val dash_url: String? = null 39 | ) 40 | 41 | @Serializable 42 | data class Preview( 43 | @SerialName("images") val images: ArrayList = arrayListOf(), 44 | @SerialName("reddit_video_preview") val redditVideo: RedditVideo? = null 45 | ) 46 | 47 | @Serializable 48 | data class Images( 49 | @SerialName("source") val source: Source? = Source() 50 | ) 51 | 52 | @Serializable 53 | data class Source( 54 | @SerialName("url") val url: String? = null 55 | ) 56 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/model/Sort.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 7/30/23, 6:12 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.model 9 | 10 | import app.suhasdissa.memerize.R 11 | 12 | sealed class Sort(open val name: Int, val redditSort: String, open val lemmySort: String) { 13 | object Hot : Sort(R.string.hot, "hot", "Hot") 14 | object New : Sort(R.string.sort_new, "new", "New") 15 | object Rising : Sort(R.string.rising, "rising", "Active") 16 | sealed class Top(val redditT: String) : Sort(R.string.top, "top", "") { 17 | object Today : Top("today") { 18 | override val name = R.string.reddit_today_btn 19 | override val lemmySort = "TopDay" 20 | } 21 | 22 | object Week : Top("week") { 23 | override val name = R.string.reddit_week_btn 24 | override val lemmySort = "TopWeek" 25 | } 26 | 27 | object Month : Top("month") { 28 | override val name = R.string.reddit_month_btn 29 | override val lemmySort = "TopMonth" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/repositories/CommunityRepository.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/4/23, 9:44 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.repositories 9 | 10 | import app.suhasdissa.memerize.backend.database.entity.AboutCommunity 11 | import kotlinx.coroutines.flow.Flow 12 | 13 | interface CommunityRepository { 14 | fun getCommunities(): Flow> 15 | suspend fun getCommunityInfo(community: T): T? 16 | suspend fun insertCommunity(community: T) 17 | suspend fun removeCommunity(community: T) 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/repositories/LemmyCommunityRepository.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/4/23, 10:15 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.repositories 9 | 10 | import android.util.Log 11 | import app.suhasdissa.memerize.backend.apis.LemmyApi 12 | import app.suhasdissa.memerize.backend.database.dao.CommunityDAO 13 | import app.suhasdissa.memerize.backend.database.entity.LemmyCommunity 14 | import kotlinx.coroutines.flow.Flow 15 | 16 | interface LemmyCommunityRepository : CommunityRepository 17 | class LemmyCommunityRepositoryImpl( 18 | private val communityDAO: CommunityDAO, 19 | private val lemmyApi: LemmyApi 20 | ) : LemmyCommunityRepository { 21 | 22 | override fun getCommunities(): Flow> = communityDAO.getAll() 23 | 24 | override suspend fun getCommunityInfo(community: LemmyCommunity): LemmyCommunity? { 25 | return try { 26 | val comm = 27 | lemmyApi.getCommunity(community.instance, community.id).communityView?.community 28 | ?: return null 29 | 30 | return community.copy(name = comm.name ?: community.id, iconUrl = comm.icon) 31 | } catch (e: Exception) { 32 | Log.e("Lemmy Repository", e.toString()) 33 | null 34 | } 35 | } 36 | 37 | override suspend fun insertCommunity(community: LemmyCommunity) = communityDAO.insert(community) 38 | 39 | override suspend fun removeCommunity(community: LemmyCommunity) = communityDAO.delete(community) 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/repositories/LemmyMemeRepository.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.memerize.backend.repositories 2 | 3 | import android.util.Log 4 | import androidx.annotation.WorkerThread 5 | import app.suhasdissa.memerize.backend.apis.LemmyApi 6 | import app.suhasdissa.memerize.backend.database.dao.LemmyMemeDAO 7 | import app.suhasdissa.memerize.backend.database.entity.LemmyCommunity 8 | import app.suhasdissa.memerize.backend.database.entity.LemmyMeme 9 | import app.suhasdissa.memerize.backend.model.Sort 10 | 11 | interface LemmyMemeRepository : MemeRepository 12 | class LemmyMemeRepositoryImpl( 13 | private val lemmyDAO: LemmyMemeDAO, 14 | private val lemmyApi: LemmyApi 15 | ) : LemmyMemeRepository { 16 | 17 | override suspend fun getOnlineData( 18 | community: LemmyCommunity, 19 | sort: Sort 20 | ): List? { 21 | return try { 22 | val memesList = getNetworkData(community, sort.lemmySort) 23 | Thread { 24 | insertMemes(memesList) 25 | }.start() 26 | memesList 27 | } catch (e: Exception) { 28 | Log.e("Lemmy Repository", e.toString()) 29 | null 30 | } 31 | } 32 | 33 | private suspend fun getNetworkData( 34 | community: LemmyCommunity, 35 | time: String 36 | ): List { 37 | val memeList: ArrayList = arrayListOf() 38 | val lemmyData = lemmyApi.getLemmyData( 39 | instance = community.instance, 40 | community = community.id, 41 | sort = time 42 | ).posts 43 | lemmyData.forEach { post -> 44 | val url = post.post?.url ?: "" 45 | val title = post.post?.name ?: "" 46 | if (url.endsWith("jpg") || url.endsWith("jpeg") || url.endsWith("png")) { 47 | val id = url.hashCode().toString() 48 | memeList.add( 49 | LemmyMeme( 50 | id, 51 | url, 52 | title, 53 | false, 54 | url, 55 | community.name, 56 | community.instance, 57 | post.post?.postLink 58 | ) 59 | ) 60 | } 61 | } 62 | return memeList 63 | } 64 | 65 | override suspend fun getLocalData(community: LemmyCommunity): List = 66 | lemmyDAO.getAll(community.id, community.instance) 67 | 68 | @WorkerThread 69 | private fun insertMemes(memes: List) { 70 | lemmyDAO.insertAll(memes) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/repositories/MemeRepository.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.repositories 9 | 10 | import app.suhasdissa.memerize.backend.database.entity.AboutCommunity 11 | import app.suhasdissa.memerize.backend.database.entity.Meme 12 | import app.suhasdissa.memerize.backend.model.Sort 13 | 14 | interface MemeRepository { 15 | suspend fun getOnlineData(community: C, sort: Sort): List? 16 | suspend fun getLocalData(community: C): List 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/repositories/RedditCommunityRepository.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/4/23, 10:15 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.repositories 9 | 10 | import app.suhasdissa.memerize.backend.apis.RedditApi 11 | import app.suhasdissa.memerize.backend.database.dao.SubredditDAO 12 | import app.suhasdissa.memerize.backend.database.entity.RedditCommunity 13 | import kotlinx.coroutines.flow.Flow 14 | 15 | interface RedditCommunityRepository : CommunityRepository 16 | class RedditCommunityRepositoryImpl( 17 | private val subredditDAO: SubredditDAO, 18 | private val redditApi: RedditApi 19 | ) : RedditCommunityRepository { 20 | 21 | override fun getCommunities(): Flow> = subredditDAO.getAll() 22 | 23 | override suspend fun getCommunityInfo(community: RedditCommunity): RedditCommunity? { 24 | return try { 25 | val info = redditApi.getAboutSubreddit(community.id).data ?: return null 26 | community.copy(iconUrl = info.communityIconUrl, name = info.displayName ?: community.id) 27 | } catch (_: Exception) { 28 | null 29 | } 30 | } 31 | 32 | override suspend fun insertCommunity(community: RedditCommunity) = 33 | subredditDAO.insert(community) 34 | 35 | override suspend fun removeCommunity(community: RedditCommunity) = 36 | subredditDAO.delete(community) 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/repositories/RedditMemeRepository.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.memerize.backend.repositories 2 | 3 | import android.util.Log 4 | import androidx.annotation.WorkerThread 5 | import app.suhasdissa.memerize.backend.apis.RedditApi 6 | import app.suhasdissa.memerize.backend.database.dao.RedditMemeDao 7 | import app.suhasdissa.memerize.backend.database.entity.RedditCommunity 8 | import app.suhasdissa.memerize.backend.database.entity.RedditMeme 9 | import app.suhasdissa.memerize.backend.model.Sort 10 | 11 | interface RedditMemeRepository : MemeRepository 12 | 13 | class RedditMemeRepositoryImpl( 14 | private val redditMemeDao: RedditMemeDao, 15 | private val redditApi: RedditApi 16 | ) : RedditMemeRepository { 17 | 18 | private val imageRegex = Regex("^.+\\.(jpg|jpeg|png|webp)\$") 19 | override suspend fun getOnlineData( 20 | community: RedditCommunity, 21 | sort: Sort 22 | ): List? { 23 | val srt = when (sort) { 24 | is Sort.Top -> sort.redditSort to sort.redditT 25 | else -> sort.redditSort to null 26 | } 27 | return try { 28 | val memesList = getNetworkData(community.id, srt.first, srt.second) 29 | Thread { 30 | insertMemes(memesList) 31 | }.start() 32 | memesList 33 | } catch (e: Exception) { 34 | Log.e("Reddit Repository", e.message, e) 35 | null 36 | } 37 | } 38 | 39 | override suspend fun getLocalData(community: RedditCommunity): List = 40 | redditMemeDao.getAll(community.id) 41 | 42 | private suspend fun getNetworkData( 43 | subreddit: String, 44 | sort: String, 45 | time: String? 46 | ): List { 47 | val memeList: ArrayList = arrayListOf() 48 | val redditData = 49 | redditApi.getRedditData(subreddit, sort, time).data?.children ?: return emptyList() 50 | redditData.forEach { child -> 51 | val url = child.childdata?.url 52 | if (url?.matches(imageRegex) == true) { 53 | val id = url.hashCode().toString() 54 | memeList.add( 55 | RedditMeme( 56 | id, 57 | url, 58 | child.childdata.title ?: "", 59 | false, 60 | "", 61 | subreddit, 62 | child.childdata.permalink?.let { "https://www.reddit.com$it" } 63 | ) 64 | ) 65 | } else if (url?.contains("v.redd.it") == true || child.childdata?.preview?.redditVideo?.dash_url != null) { 66 | val dashUrl = child.childdata.secure_media?.reddit_video?.dash_url 67 | ?: child.childdata.preview?.redditVideo?.dash_url 68 | val previewUrl = child.childdata.preview?.images?.get(0)?.source?.url 69 | if (dashUrl != null && previewUrl != null) { 70 | val id = url.hashCode().toString() 71 | memeList.add( 72 | RedditMeme( 73 | id, 74 | dashUrl, 75 | child.childdata.title ?: "", 76 | true, 77 | previewUrl.replace("&", "&"), 78 | subreddit, 79 | child.childdata.permalink?.let { "https://www.reddit.com$it" } 80 | ) 81 | ) 82 | } 83 | } 84 | } 85 | return memeList 86 | } 87 | 88 | @WorkerThread 89 | private fun insertMemes(memes: List) { 90 | redditMemeDao.insertAll(memes) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/viewmodels/CheckUpdateViewModel.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 7/9/23, 3:34 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.viewmodels 9 | 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.setValue 13 | import androidx.lifecycle.ViewModel 14 | import androidx.lifecycle.viewModelScope 15 | import app.suhasdissa.memerize.utils.UpdateUtil 16 | import kotlinx.coroutines.launch 17 | 18 | class CheckUpdateViewModel : ViewModel() { 19 | var latestVersion: Float? by mutableStateOf(null) 20 | val currentVersion = UpdateUtil.currentVersion 21 | 22 | init { 23 | getLatestRelease() 24 | } 25 | 26 | private fun getLatestRelease() { 27 | viewModelScope.launch { 28 | latestVersion = UpdateUtil.getLatestVersion() 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/viewmodels/LemmyCommunityViewModel.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 7/30/23, 2:18 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.viewmodels 9 | 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.setValue 13 | import androidx.lifecycle.ViewModel 14 | import androidx.lifecycle.ViewModelProvider 15 | import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY 16 | import androidx.lifecycle.viewModelScope 17 | import androidx.lifecycle.viewmodel.initializer 18 | import androidx.lifecycle.viewmodel.viewModelFactory 19 | import app.suhasdissa.memerize.MemerizeApplication 20 | import app.suhasdissa.memerize.backend.database.entity.LemmyCommunity 21 | import app.suhasdissa.memerize.backend.repositories.LemmyCommunityRepository 22 | import app.suhasdissa.memerize.backend.viewmodels.state.AboutCommunityState 23 | import kotlinx.coroutines.flow.SharingStarted 24 | import kotlinx.coroutines.flow.stateIn 25 | import kotlinx.coroutines.launch 26 | 27 | class LemmyCommunityViewModel(private val lemmyRepository: LemmyCommunityRepository) : 28 | ViewModel() { 29 | 30 | val communities = lemmyRepository.getCommunities().stateIn( 31 | viewModelScope, 32 | started = SharingStarted.WhileSubscribed(5000L), 33 | initialValue = listOf() 34 | ) 35 | 36 | var aboutCommutnityState: AboutCommunityState by mutableStateOf( 37 | AboutCommunityState.Loading( 38 | LemmyCommunity("", "") 39 | ) 40 | ) 41 | 42 | fun removeCommunity(community: LemmyCommunity) { 43 | viewModelScope.launch { 44 | lemmyRepository.removeCommunity(community) 45 | } 46 | } 47 | 48 | fun getInfo(instance: String, community: String) { 49 | viewModelScope.launch { 50 | aboutCommutnityState = AboutCommunityState.Loading(LemmyCommunity(community, instance)) 51 | val lemmyInfo = lemmyRepository.getCommunityInfo(LemmyCommunity(community, instance)) 52 | if (lemmyInfo == null) { 53 | aboutCommutnityState = 54 | AboutCommunityState.Error(LemmyCommunity(community, instance)) 55 | } else { 56 | aboutCommutnityState = AboutCommunityState.Success(lemmyInfo) 57 | lemmyRepository.insertCommunity(lemmyInfo) 58 | } 59 | } 60 | } 61 | 62 | companion object { 63 | val Factory: ViewModelProvider.Factory = viewModelFactory { 64 | initializer { 65 | val application = (this[APPLICATION_KEY] as MemerizeApplication) 66 | val lemmyRepository = application.container.lemmyCommunityRepository 67 | LemmyCommunityViewModel(lemmyRepository) 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/viewmodels/LemmyViewModel.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.viewmodels 9 | 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.setValue 13 | import androidx.lifecycle.ViewModel 14 | import androidx.lifecycle.ViewModelProvider 15 | import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY 16 | import androidx.lifecycle.viewModelScope 17 | import androidx.lifecycle.viewmodel.initializer 18 | import androidx.lifecycle.viewmodel.viewModelFactory 19 | import app.suhasdissa.memerize.MemerizeApplication 20 | import app.suhasdissa.memerize.backend.database.entity.LemmyCommunity 21 | import app.suhasdissa.memerize.backend.model.Sort 22 | import app.suhasdissa.memerize.backend.repositories.LemmyMemeRepository 23 | import app.suhasdissa.memerize.backend.viewmodels.state.MemeUiState 24 | import kotlinx.coroutines.launch 25 | 26 | class LemmyViewModel(private val lemmyRepository: LemmyMemeRepository) : 27 | ViewModel() { 28 | var memeUiState: MemeUiState by mutableStateOf(MemeUiState.Loading) 29 | private set 30 | 31 | var currentCommunity: LemmyCommunity? = null 32 | private set 33 | var currentSortTime: Sort = Sort.Top.Today 34 | private set 35 | 36 | fun getMemePhotos( 37 | community: LemmyCommunity? = currentCommunity, 38 | sort: Sort = Sort.Top.Today 39 | ) { 40 | currentCommunity = community!! 41 | currentSortTime = sort 42 | viewModelScope.launch { 43 | memeUiState = MemeUiState.Loading 44 | 45 | memeUiState = when (val data = lemmyRepository.getOnlineData(community, sort)) { 46 | null -> { 47 | MemeUiState.Error("") 48 | } 49 | 50 | else -> { 51 | MemeUiState.Success(data) 52 | } 53 | } 54 | } 55 | } 56 | 57 | fun getLocalMemes(community: LemmyCommunity = currentCommunity!!) { 58 | viewModelScope.launch { 59 | memeUiState = MemeUiState.Loading 60 | 61 | memeUiState = MemeUiState.Success(lemmyRepository.getLocalData(community)) 62 | } 63 | } 64 | 65 | companion object { 66 | val Factory: ViewModelProvider.Factory = viewModelFactory { 67 | initializer { 68 | val application = (this[APPLICATION_KEY] as MemerizeApplication) 69 | val lemmyRepository = application.container.lemmyMemeRepository 70 | LemmyViewModel(lemmyRepository) 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/viewmodels/PhotoViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.memerize.backend.viewmodels 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.graphics.Bitmap 6 | import android.net.Uri 7 | import android.os.Environment 8 | import android.util.Log 9 | import android.widget.Toast 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.setValue 13 | import androidx.core.content.FileProvider 14 | import androidx.core.graphics.drawable.toBitmap 15 | import androidx.documentfile.provider.DocumentFile 16 | import androidx.lifecycle.ViewModel 17 | import androidx.lifecycle.viewModelScope 18 | import app.suhasdissa.memerize.BuildConfig 19 | import app.suhasdissa.memerize.backend.database.entity.Meme 20 | import app.suhasdissa.memerize.utils.SaveDirectoryKey 21 | import app.suhasdissa.memerize.utils.preferences 22 | import coil.ImageLoader 23 | import coil.request.ImageRequest 24 | import coil.request.SuccessResult 25 | import java.io.File 26 | import java.io.FileOutputStream 27 | import java.util.UUID 28 | import kotlinx.coroutines.Dispatchers 29 | import kotlinx.coroutines.launch 30 | import kotlinx.coroutines.withContext 31 | 32 | class PhotoViewModel : ViewModel() { 33 | 34 | var downloadState: DownloadState by mutableStateOf(DownloadState.NotStarted) 35 | 36 | private suspend fun getBitmapFromUrl(url: String, context: Context): Bitmap? { 37 | val imageLoader = ImageLoader.Builder(context).build() 38 | val request = ImageRequest.Builder(context) 39 | .data(url) 40 | .build() 41 | val result = imageLoader.execute(request) 42 | 43 | if (result is SuccessResult) { 44 | return result.drawable.toBitmap() 45 | } 46 | return null 47 | } 48 | 49 | fun savePhotoToDisk(meme: Meme, context: Context) { 50 | viewModelScope.launch(Dispatchers.IO) { 51 | withContext(Dispatchers.Main) { 52 | downloadState = DownloadState.Loading 53 | } 54 | val bitmap = getBitmapFromUrl(meme.url, context) 55 | val prefDir = 56 | context.preferences.getString(SaveDirectoryKey, null) 57 | 58 | val saveDir = when { 59 | prefDir.isNullOrBlank() -> { 60 | val dir = 61 | Environment.getExternalStoragePublicDirectory( 62 | Environment.DIRECTORY_DOWNLOADS 63 | ) 64 | DocumentFile.fromFile(dir) 65 | } 66 | 67 | else -> DocumentFile.fromTreeUri(context, Uri.parse(prefDir))!! 68 | } 69 | val outputFile = 70 | saveDir.createFile( 71 | "image/jpg", 72 | "${meme.title.take(64)}-${ 73 | UUID.randomUUID().toString().take(8) 74 | }.jpg".replace("[\\\\/:*?\"<>|]".toRegex(), "") 75 | ) 76 | if (outputFile == null) { 77 | withContext(Dispatchers.Main) { 78 | downloadState = DownloadState.Error 79 | } 80 | withContext(Dispatchers.Main) { 81 | Toast.makeText(context, "Failed to create file", Toast.LENGTH_LONG).show() 82 | } 83 | return@launch 84 | } 85 | if (bitmap != null) { 86 | try { 87 | val outputStream = context.contentResolver.openOutputStream(outputFile.uri)!! 88 | bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) 89 | outputStream.flush() 90 | outputStream.close() 91 | withContext(Dispatchers.Main) { 92 | Toast.makeText(context, "Download Finished", Toast.LENGTH_LONG).show() 93 | downloadState = DownloadState.NotStarted 94 | } 95 | } catch (e: Exception) { 96 | Log.e("Photo save", e.toString()) 97 | withContext(Dispatchers.Main) { 98 | Toast.makeText(context, "Download Failed", Toast.LENGTH_LONG).show() 99 | downloadState = DownloadState.Error 100 | } 101 | } 102 | } else { 103 | withContext(Dispatchers.Main) { 104 | downloadState = DownloadState.Error 105 | } 106 | } 107 | } 108 | } 109 | 110 | fun shareImage(url: String, context: Context) { 111 | viewModelScope.launch(Dispatchers.IO) { 112 | val bitmap = getBitmapFromUrl(url, context) 113 | if (bitmap != null) { 114 | try { 115 | val outputFile = File( 116 | context.cacheDir, 117 | "${UUID.randomUUID()}.jpg" 118 | ) 119 | val outputStream = FileOutputStream(outputFile) 120 | bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) 121 | outputStream.flush() 122 | outputStream.close() 123 | val sendIntent: Intent = Intent().apply { 124 | action = Intent.ACTION_SEND 125 | putExtra( 126 | Intent.EXTRA_STREAM, 127 | FileProvider.getUriForFile( 128 | context, 129 | BuildConfig.APPLICATION_ID + ".provider", 130 | outputFile 131 | ) 132 | ) 133 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 134 | type = "image/jpg" 135 | } 136 | val shareIntent = Intent.createChooser(sendIntent, "Send Photo to..") 137 | context.startActivity(shareIntent) 138 | } catch (e: Exception) { 139 | Log.e("Share Image", e.toString()) 140 | } 141 | } 142 | } 143 | } 144 | } 145 | 146 | sealed interface DownloadState { 147 | object NotStarted : DownloadState 148 | object Success : DownloadState 149 | object Error : DownloadState 150 | object Loading : DownloadState 151 | } 152 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/viewmodels/PlayerViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.memerize.backend.viewmodels 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.widget.Toast 6 | import androidx.annotation.RequiresApi 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.setValue 10 | import androidx.lifecycle.ViewModel 11 | import androidx.lifecycle.viewModelScope 12 | import androidx.media3.common.Player 13 | import app.suhasdissa.memerize.backend.database.entity.Meme 14 | import app.suhasdissa.memerize.utils.RedditVideoDownloader 15 | import java.util.UUID 16 | import kotlinx.coroutines.launch 17 | 18 | class PlayerViewModel() : ViewModel() { 19 | var downloadState: DownloadState by mutableStateOf(DownloadState.NotStarted) 20 | 21 | var muted by mutableStateOf(false) 22 | 23 | @RequiresApi(Build.VERSION_CODES.O) 24 | fun downloadVideo(context: Context, meme: Meme) { 25 | val fileName = "${meme.title.take(64)}-${ 26 | UUID.randomUUID().toString().take(8) 27 | }".replace("[\\\\/:*?\"<>|]".toRegex(), "") 28 | viewModelScope.launch { 29 | downloadState = DownloadState.Loading 30 | val downloader = RedditVideoDownloader() 31 | val result = 32 | downloader.downloadRedditVideo(context.applicationContext, meme.url, fileName) 33 | downloadState = if (result) { 34 | Toast.makeText(context, "Download Finished", Toast.LENGTH_LONG).show() 35 | DownloadState.NotStarted 36 | } else { 37 | Toast.makeText(context, "Download Failed", Toast.LENGTH_LONG).show() 38 | DownloadState.Error 39 | } 40 | } 41 | } 42 | } 43 | 44 | fun Player.playPause() { 45 | if (isPlaying) pause() else play() 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/viewmodels/RedditCommunityViewModel.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 7/30/23, 2:18 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.viewmodels 9 | 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.setValue 13 | import androidx.lifecycle.ViewModel 14 | import androidx.lifecycle.ViewModelProvider 15 | import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY 16 | import androidx.lifecycle.viewModelScope 17 | import androidx.lifecycle.viewmodel.initializer 18 | import androidx.lifecycle.viewmodel.viewModelFactory 19 | import app.suhasdissa.memerize.MemerizeApplication 20 | import app.suhasdissa.memerize.backend.database.entity.RedditCommunity 21 | import app.suhasdissa.memerize.backend.repositories.RedditCommunityRepository 22 | import app.suhasdissa.memerize.backend.viewmodels.state.AboutCommunityState 23 | import kotlinx.coroutines.flow.SharingStarted 24 | import kotlinx.coroutines.flow.stateIn 25 | import kotlinx.coroutines.launch 26 | 27 | class RedditCommunityViewModel(private val redditRepository: RedditCommunityRepository) : 28 | ViewModel() { 29 | 30 | val communities = redditRepository.getCommunities().stateIn( 31 | viewModelScope, 32 | started = SharingStarted.WhileSubscribed(5000L), 33 | initialValue = listOf() 34 | ) 35 | 36 | var aboutCommunityState: AboutCommunityState by mutableStateOf( 37 | AboutCommunityState.Loading( 38 | RedditCommunity("") 39 | ) 40 | ) 41 | 42 | fun removeSubreddit(subreddit: RedditCommunity) { 43 | viewModelScope.launch { 44 | redditRepository.removeCommunity(subreddit) 45 | } 46 | } 47 | 48 | fun getSubredditInfo(subreddit: String) { 49 | viewModelScope.launch { 50 | aboutCommunityState = AboutCommunityState.Loading(RedditCommunity(subreddit)) 51 | val subredditInfo = redditRepository.getCommunityInfo(RedditCommunity(subreddit)) 52 | if (subredditInfo == null) { 53 | aboutCommunityState = AboutCommunityState.Error(RedditCommunity(subreddit)) 54 | } else { 55 | aboutCommunityState = AboutCommunityState.Success(subredditInfo) 56 | redditRepository.insertCommunity(subredditInfo) 57 | } 58 | } 59 | } 60 | 61 | companion object { 62 | val Factory: ViewModelProvider.Factory = viewModelFactory { 63 | initializer { 64 | val application = (this[APPLICATION_KEY] as MemerizeApplication) 65 | val redditRepository = application.container.redditCommunityRepository 66 | RedditCommunityViewModel(redditRepository = redditRepository) 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/viewmodels/RedditViewModel.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.viewmodels 9 | 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.setValue 13 | import androidx.lifecycle.ViewModel 14 | import androidx.lifecycle.ViewModelProvider 15 | import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY 16 | import androidx.lifecycle.viewModelScope 17 | import androidx.lifecycle.viewmodel.initializer 18 | import androidx.lifecycle.viewmodel.viewModelFactory 19 | import app.suhasdissa.memerize.MemerizeApplication 20 | import app.suhasdissa.memerize.backend.database.entity.RedditCommunity 21 | import app.suhasdissa.memerize.backend.database.entity.RedditMeme 22 | import app.suhasdissa.memerize.backend.model.Sort 23 | import app.suhasdissa.memerize.backend.repositories.RedditMemeRepository 24 | import app.suhasdissa.memerize.backend.viewmodels.state.MemeUiState 25 | import kotlinx.coroutines.async 26 | import kotlinx.coroutines.awaitAll 27 | import kotlinx.coroutines.launch 28 | 29 | class RedditViewModel(private val redditRepository: RedditMemeRepository) : 30 | ViewModel() { 31 | var memeUiState: MemeUiState by mutableStateOf(MemeUiState.Loading) 32 | private set 33 | 34 | var currentSubreddit: RedditCommunity? = null 35 | private set 36 | 37 | var currentSortTime: Sort = Sort.Top.Today 38 | private set 39 | 40 | fun getMemePhotos( 41 | subreddit: RedditCommunity? = currentSubreddit, 42 | sort: Sort = Sort.Top.Today 43 | ) { 44 | currentSubreddit = subreddit!! 45 | currentSortTime = sort 46 | viewModelScope.launch { 47 | memeUiState = MemeUiState.Loading 48 | 49 | memeUiState = when ( 50 | val data = 51 | redditRepository.getOnlineData(subreddit, sort) 52 | ) { 53 | null -> { 54 | MemeUiState.Error("") 55 | } 56 | 57 | else -> { 58 | MemeUiState.Success(data) 59 | } 60 | } 61 | } 62 | } 63 | 64 | fun getLocalMemes(subreddit: RedditCommunity = currentSubreddit!!) { 65 | viewModelScope.launch { 66 | memeUiState = MemeUiState.Loading 67 | 68 | memeUiState = 69 | MemeUiState.Success(redditRepository.getLocalData(subreddit)) 70 | } 71 | } 72 | 73 | fun getMultiMemes(communities: List) { 74 | viewModelScope.launch { 75 | currentSubreddit = null 76 | memeUiState = MemeUiState.Loading 77 | val results = communities.map { 78 | async { redditRepository.getOnlineData(it, Sort.Top.Today) } 79 | }.awaitAll() 80 | val memeList: List = results.filterNotNull().flatten().shuffled() 81 | memeUiState = if (memeList.isEmpty()) { 82 | MemeUiState.Error("") 83 | } else { 84 | MemeUiState.Success(memeList) 85 | } 86 | } 87 | } 88 | 89 | companion object { 90 | val Factory: ViewModelProvider.Factory = viewModelFactory { 91 | initializer { 92 | val application = (this[APPLICATION_KEY] as MemerizeApplication) 93 | val redditRepository = application.container.redditMemeRepository 94 | RedditViewModel(redditRepository = redditRepository) 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/viewmodels/state/AboutCommunityState.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/4/23, 9:26 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.viewmodels.state 9 | 10 | import app.suhasdissa.memerize.backend.database.entity.AboutCommunity 11 | 12 | sealed interface AboutCommunityState { 13 | data class Success(val community: AboutCommunity) : AboutCommunityState 14 | data class Error(val community: AboutCommunity) : AboutCommunityState 15 | data class Loading(val community: AboutCommunity) : AboutCommunityState 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/backend/viewmodels/state/MemeUiState.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/4/23, 12:13 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.backend.viewmodels.state 9 | 10 | import app.suhasdissa.memerize.backend.database.entity.Meme 11 | 12 | sealed interface MemeUiState { 13 | data class Success(val memes: List) : MemeUiState 14 | data class Error(val error: String) : MemeUiState 15 | object Loading : MemeUiState 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/MemerizeApp.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/25/22, 6:10 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui 9 | 10 | import android.view.SoundEffectConstants 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.material3.DrawerValue 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.ModalNavigationDrawer 15 | import androidx.compose.material3.Surface 16 | import androidx.compose.material3.rememberDrawerState 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.runtime.rememberCoroutineScope 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.platform.LocalView 25 | import androidx.navigation.compose.rememberNavController 26 | import app.suhasdissa.memerize.AppNavHost 27 | import app.suhasdissa.memerize.Destination 28 | import app.suhasdissa.memerize.navigateTo 29 | import app.suhasdissa.memerize.ui.components.NavDrawerContent 30 | import kotlinx.coroutines.launch 31 | 32 | @Composable 33 | fun MemerizeApp() { 34 | val navController = rememberNavController() 35 | val drawerState = rememberDrawerState(DrawerValue.Closed) 36 | val scope = rememberCoroutineScope() 37 | var currentDestination by remember { 38 | mutableStateOf(Destination.Home) 39 | } 40 | val view = LocalView.current 41 | ModalNavigationDrawer( 42 | drawerState = drawerState, 43 | gesturesEnabled = drawerState.isOpen, 44 | drawerContent = { 45 | NavDrawerContent(currentDestination = currentDestination, onDestinationSelected = { 46 | scope.launch { 47 | drawerState.close() 48 | } 49 | navController.navigateTo(it.route) 50 | currentDestination = it 51 | }) 52 | } 53 | ) { 54 | Surface( 55 | modifier = Modifier 56 | .fillMaxSize(), 57 | color = MaterialTheme.colorScheme.surface 58 | ) { 59 | AppNavHost( 60 | navController = navController, 61 | onDrawerOpen = { 62 | view.playSoundEffect(SoundEffectConstants.CLICK) 63 | scope.launch { 64 | drawerState.open() 65 | } 66 | }, 67 | modifier = Modifier.fillMaxSize() 68 | ) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/components/CacheSizeDialog.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/9/23, 9:56 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.components 9 | 10 | import androidx.compose.foundation.layout.Arrangement 11 | import androidx.compose.foundation.lazy.grid.GridCells 12 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 13 | import androidx.compose.foundation.lazy.grid.items 14 | import androidx.compose.material3.AlertDialog 15 | import androidx.compose.material3.Button 16 | import androidx.compose.material3.ExperimentalMaterial3Api 17 | import androidx.compose.material3.FilterChip 18 | import androidx.compose.material3.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.setValue 22 | import androidx.compose.ui.res.stringResource 23 | import androidx.compose.ui.unit.dp 24 | import app.suhasdissa.memerize.R 25 | import app.suhasdissa.memerize.utils.defaultImageCacheSize 26 | import app.suhasdissa.memerize.utils.imageCacheKey 27 | import app.suhasdissa.memerize.utils.rememberPreference 28 | 29 | @OptIn(ExperimentalMaterial3Api::class) 30 | @Composable 31 | fun CacheSizeDialog(onDismissRequest: () -> Unit) { 32 | val cacheSizes = listOf(16, 32, 64, 128, 256, 512, 1024, 2048) 33 | var prefSize by rememberPreference(key = imageCacheKey, defaultValue = defaultImageCacheSize) 34 | AlertDialog( 35 | onDismissRequest, 36 | title = { Text(stringResource(R.string.change_image_cache_size)) }, 37 | confirmButton = { 38 | Button(onClick = { 39 | onDismissRequest.invoke() 40 | }) { 41 | Text(text = stringResource(R.string.ok)) 42 | } 43 | }, 44 | text = { 45 | LazyVerticalGrid( 46 | columns = GridCells.Fixed(3), 47 | verticalArrangement = Arrangement.spacedBy(8.dp), 48 | horizontalArrangement = Arrangement.spacedBy(8.dp) 49 | ) { 50 | items(items = cacheSizes) { 51 | FilterChip( 52 | selected = prefSize == it, 53 | onClick = { prefSize = it }, 54 | label = { 55 | Text("$it MB") 56 | } 57 | ) 58 | } 59 | } 60 | } 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/components/ErrorScreen.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.components 9 | 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.res.stringResource 19 | import app.suhasdissa.memerize.R 20 | 21 | @Composable 22 | fun ErrorScreen(memeUiState: String, modifier: Modifier = Modifier) { 23 | Box( 24 | contentAlignment = Alignment.Center, 25 | modifier = modifier.fillMaxSize() 26 | ) { 27 | Column { 28 | Text( 29 | stringResource(R.string.loading_failed), 30 | style = MaterialTheme.typography.bodyLarge 31 | ) 32 | Text(memeUiState, color = MaterialTheme.colorScheme.error) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/components/HighlightCard.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.components 9 | 10 | import android.view.SoundEffectConstants 11 | import androidx.compose.foundation.ExperimentalFoundationApi 12 | import androidx.compose.foundation.combinedClickable 13 | import androidx.compose.foundation.layout.Arrangement 14 | import androidx.compose.foundation.layout.Row 15 | import androidx.compose.foundation.layout.aspectRatio 16 | import androidx.compose.foundation.layout.fillMaxSize 17 | import androidx.compose.foundation.layout.fillMaxWidth 18 | import androidx.compose.foundation.layout.height 19 | import androidx.compose.foundation.layout.padding 20 | import androidx.compose.foundation.layout.size 21 | import androidx.compose.foundation.shape.CircleShape 22 | import androidx.compose.material3.CardDefaults 23 | import androidx.compose.material3.ElevatedCard 24 | import androidx.compose.material3.MaterialTheme 25 | import androidx.compose.material3.Text 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.ui.Alignment 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.draw.clip 30 | import androidx.compose.ui.hapticfeedback.HapticFeedbackType 31 | import androidx.compose.ui.layout.ContentScale 32 | import androidx.compose.ui.platform.LocalHapticFeedback 33 | import androidx.compose.ui.platform.LocalView 34 | import androidx.compose.ui.res.painterResource 35 | import androidx.compose.ui.text.style.TextAlign 36 | import androidx.compose.ui.tooling.preview.Preview 37 | import androidx.compose.ui.unit.dp 38 | import app.suhasdissa.memerize.R 39 | import coil.compose.AsyncImage 40 | 41 | @OptIn(ExperimentalFoundationApi::class) 42 | @Composable 43 | fun HighlightCard( 44 | onClick: () -> Unit, 45 | name: String, 46 | thumbnail_url: String? = null, 47 | highlighted: Boolean = false, 48 | onLongClick: () -> Unit = {} 49 | ) { 50 | val view = LocalView.current 51 | val haptic = LocalHapticFeedback.current 52 | ElevatedCard( 53 | modifier = Modifier 54 | .fillMaxWidth() 55 | .height(128.dp) 56 | .padding(8.dp), 57 | colors = if (highlighted) { 58 | CardDefaults.elevatedCardColors( 59 | containerColor = MaterialTheme.colorScheme.primary, 60 | contentColor = MaterialTheme.colorScheme.onPrimary 61 | ) 62 | } else { 63 | CardDefaults.elevatedCardColors() 64 | } 65 | ) { 66 | Row( 67 | modifier = Modifier 68 | .combinedClickable( 69 | onClick = { 70 | view.playSoundEffect(SoundEffectConstants.CLICK) 71 | onClick.invoke() 72 | }, 73 | onLongClick = { 74 | haptic.performHapticFeedback(HapticFeedbackType.LongPress) 75 | onLongClick.invoke() 76 | } 77 | ) 78 | .fillMaxSize() 79 | .padding(16.dp), 80 | verticalAlignment = Alignment.CenterVertically, 81 | horizontalArrangement = Arrangement.SpaceEvenly 82 | ) { 83 | if (thumbnail_url != null) { 84 | AsyncImage( 85 | model = thumbnail_url, 86 | contentDescription = null, 87 | modifier = Modifier 88 | .size(90.dp) 89 | .aspectRatio(1f) 90 | .clip(CircleShape), 91 | contentScale = ContentScale.Crop, 92 | error = painterResource(R.drawable.reddit_placeholder), 93 | placeholder = painterResource(R.drawable.reddit_placeholder) 94 | ) 95 | } 96 | Text( 97 | text = name, 98 | style = MaterialTheme.typography.titleMedium, 99 | modifier = Modifier.weight(1f), 100 | textAlign = TextAlign.Center 101 | ) 102 | } 103 | } 104 | } 105 | 106 | @Preview() 107 | @Composable 108 | fun HighlightCardPreview() { 109 | HighlightCard(onClick = {}, name = "Preview") 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/components/ImageCard.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.components 9 | 10 | import android.view.SoundEffectConstants 11 | import androidx.compose.foundation.clickable 12 | import androidx.compose.foundation.layout.Column 13 | import androidx.compose.foundation.layout.Spacer 14 | import androidx.compose.foundation.layout.fillMaxWidth 15 | import androidx.compose.foundation.layout.height 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.material3.ElevatedCard 18 | import androidx.compose.material3.MaterialTheme 19 | import androidx.compose.material3.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.layout.ContentScale 24 | import androidx.compose.ui.platform.LocalView 25 | import androidx.compose.ui.res.painterResource 26 | import androidx.compose.ui.res.stringResource 27 | import androidx.compose.ui.text.style.TextAlign 28 | import androidx.compose.ui.unit.dp 29 | import app.suhasdissa.memerize.R 30 | import coil.compose.AsyncImage 31 | 32 | @Composable 33 | fun ImageCard( 34 | clickAction: () -> Unit, 35 | photoUrl: String, 36 | title: String 37 | ) { 38 | val view = LocalView.current 39 | ElevatedCard( 40 | modifier = Modifier 41 | .padding(8.dp) 42 | .fillMaxWidth() 43 | .clickable { 44 | view.playSoundEffect(SoundEffectConstants.CLICK) 45 | clickAction() 46 | } 47 | ) { 48 | Column( 49 | modifier = Modifier.fillMaxWidth(), 50 | horizontalAlignment = Alignment.CenterHorizontally 51 | ) { 52 | Text( 53 | text = title, 54 | modifier = Modifier.padding(horizontal = 8.dp), 55 | style = MaterialTheme.typography.titleLarge, 56 | color = MaterialTheme.colorScheme.primary, 57 | textAlign = TextAlign.Center 58 | ) 59 | Spacer(modifier = Modifier.height(20.dp)) 60 | AsyncImage( 61 | model = photoUrl, 62 | contentDescription = stringResource(R.string.meme_photo), 63 | contentScale = ContentScale.FillWidth, 64 | modifier = Modifier.fillMaxWidth(), 65 | error = painterResource(R.drawable.ic_broken_image), 66 | placeholder = painterResource(R.drawable.loading_img) 67 | ) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/components/LoadingScreen.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.components 9 | 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.fillMaxSize 12 | import androidx.compose.material3.CircularProgressIndicator 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | 17 | @Composable 18 | fun LoadingScreen(modifier: Modifier = Modifier) { 19 | Box(contentAlignment = Alignment.Center, modifier = modifier.fillMaxSize()) { 20 | CircularProgressIndicator() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/components/MemeCard.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 12/20/22, 8:56 AM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.components 9 | 10 | import androidx.compose.runtime.Composable 11 | 12 | @Composable 13 | fun MemeCard( 14 | onClickMeme: () -> Unit, 15 | photo: String, 16 | title: String 17 | ) { 18 | ImageCard({ onClickMeme.invoke() }, photo, title) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/components/MemeGrid.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/4/23, 12:09 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.components 9 | 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.PaddingValues 13 | import androidx.compose.foundation.layout.fillMaxSize 14 | import androidx.compose.foundation.layout.fillMaxWidth 15 | import androidx.compose.foundation.layout.padding 16 | import androidx.compose.foundation.lazy.grid.GridCells 17 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 18 | import androidx.compose.foundation.lazy.grid.itemsIndexed 19 | import androidx.compose.material3.MaterialTheme 20 | import androidx.compose.material3.Text 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.res.stringResource 25 | import androidx.compose.ui.unit.dp 26 | import app.suhasdissa.memerize.R 27 | import app.suhasdissa.memerize.backend.database.entity.Meme 28 | 29 | @Composable 30 | fun MemeGrid( 31 | memes: List, 32 | onClickCard: (id: Int) -> Unit 33 | ) { 34 | Column( 35 | modifier = Modifier 36 | .fillMaxSize() 37 | .padding(horizontal = 8.dp), 38 | horizontalAlignment = Alignment.CenterHorizontally 39 | ) { 40 | if (memes.isNotEmpty()) { 41 | LazyVerticalGrid( 42 | columns = GridCells.Adaptive(375.dp), 43 | modifier = Modifier.fillMaxWidth(), 44 | contentPadding = PaddingValues(8.dp) 45 | ) { 46 | itemsIndexed(items = memes) { index, meme -> 47 | if (meme.isVideo) { 48 | VideoCard(onClickVideo = { 49 | onClickCard.invoke(index) 50 | }, meme.title, meme.preview, Modifier) 51 | } else { 52 | MemeCard(onClickMeme = { 53 | onClickCard.invoke(index) 54 | }, meme.url, meme.title) 55 | } 56 | } 57 | } 58 | } else { 59 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 60 | Text( 61 | stringResource(R.string.no_memes_here), 62 | style = MaterialTheme.typography.bodyLarge, 63 | color = MaterialTheme.colorScheme.tertiary 64 | ) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/components/NavDrawerContent.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 7/30/23, 12:15 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.components 9 | 10 | import android.view.SoundEffectConstants 11 | import androidx.compose.foundation.Image 12 | import androidx.compose.foundation.layout.Row 13 | import androidx.compose.foundation.layout.Spacer 14 | import androidx.compose.foundation.layout.fillMaxWidth 15 | import androidx.compose.foundation.layout.height 16 | import androidx.compose.foundation.layout.size 17 | import androidx.compose.foundation.layout.width 18 | import androidx.compose.material.icons.Icons 19 | import androidx.compose.material.icons.filled.Group 20 | import androidx.compose.material.icons.filled.Home 21 | import androidx.compose.material.icons.filled.Settings 22 | import androidx.compose.material3.Icon 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.material3.ModalDrawerSheet 25 | import androidx.compose.material3.NavigationDrawerItem 26 | import androidx.compose.material3.Text 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.ui.Alignment 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.platform.LocalView 31 | import androidx.compose.ui.res.painterResource 32 | import androidx.compose.ui.res.stringResource 33 | import androidx.compose.ui.unit.dp 34 | import app.suhasdissa.memerize.Destination 35 | import app.suhasdissa.memerize.R 36 | 37 | @Composable 38 | fun NavDrawerContent( 39 | currentDestination: Destination, 40 | onDestinationSelected: (Destination) -> Unit 41 | ) { 42 | val view = LocalView.current 43 | ModalDrawerSheet(modifier = Modifier.width(250.dp)) { 44 | Spacer(Modifier.height(48.dp)) 45 | Row( 46 | modifier = Modifier 47 | .fillMaxWidth(), 48 | verticalAlignment = Alignment.CenterVertically 49 | ) { 50 | Image( 51 | modifier = Modifier.size(96.dp), 52 | painter = painterResource(id = R.drawable.ic_launcher_foreground), 53 | contentDescription = null 54 | ) 55 | Text( 56 | stringResource(id = R.string.app_name), 57 | color = MaterialTheme.colorScheme.primary, 58 | style = MaterialTheme.typography.headlineMedium 59 | ) 60 | } 61 | Spacer(Modifier.height(16.dp)) 62 | NavigationDrawerItem( 63 | icon = { 64 | Icon( 65 | imageVector = Icons.Default.Home, 66 | contentDescription = null 67 | ) 68 | }, 69 | label = { Text(text = stringResource(id = R.string.home)) }, 70 | selected = currentDestination == Destination.Home, 71 | onClick = { 72 | view.playSoundEffect(SoundEffectConstants.CLICK) 73 | onDestinationSelected(Destination.Home) 74 | } 75 | ) 76 | Spacer(Modifier.height(16.dp)) 77 | NavigationDrawerItem( 78 | icon = { 79 | Icon( 80 | painter = painterResource(id = R.drawable.reddit_placeholder), 81 | contentDescription = null 82 | ) 83 | }, 84 | label = { Text(text = stringResource(id = R.string.subreddits)) }, 85 | selected = currentDestination == Destination.Subreddits, 86 | onClick = { 87 | view.playSoundEffect(SoundEffectConstants.CLICK) 88 | onDestinationSelected(Destination.Subreddits) 89 | } 90 | ) 91 | Spacer(Modifier.height(16.dp)) 92 | NavigationDrawerItem( 93 | icon = { 94 | Icon( 95 | imageVector = Icons.Default.Group, 96 | contentDescription = null 97 | ) 98 | }, 99 | label = { Text(text = stringResource(id = R.string.lemmy_communities)) }, 100 | selected = currentDestination == Destination.Communities, 101 | onClick = { 102 | view.playSoundEffect(SoundEffectConstants.CLICK) 103 | onDestinationSelected(Destination.Communities) 104 | } 105 | ) 106 | Spacer(Modifier.height(16.dp)) 107 | NavigationDrawerItem( 108 | icon = { 109 | Icon( 110 | imageVector = Icons.Default.Settings, 111 | contentDescription = null 112 | ) 113 | }, 114 | label = { Text(text = stringResource(id = R.string.settings_title)) }, 115 | selected = false, 116 | onClick = { 117 | view.playSoundEffect(SoundEffectConstants.CLICK) 118 | onDestinationSelected(Destination.Settings) 119 | } 120 | ) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/components/RetryScreen.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 5/10/23, 8:57 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.components 9 | 10 | import android.view.SoundEffectConstants 11 | import androidx.compose.foundation.layout.Box 12 | import androidx.compose.foundation.layout.Column 13 | import androidx.compose.foundation.layout.fillMaxSize 14 | import androidx.compose.material3.Button 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.platform.LocalView 21 | 22 | @Composable 23 | fun RetryScreen( 24 | message: String, 25 | btnText: String, 26 | modifier: Modifier = Modifier, 27 | onRetry: () -> Unit 28 | ) { 29 | val view = LocalView.current 30 | Box( 31 | contentAlignment = Alignment.Center, 32 | modifier = modifier.fillMaxSize() 33 | ) { 34 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 35 | Text( 36 | message, 37 | color = MaterialTheme.colorScheme.error, 38 | style = MaterialTheme.typography.bodyLarge 39 | ) 40 | Button(onClick = { 41 | view.playSoundEffect(SoundEffectConstants.CLICK) 42 | onRetry() 43 | }) { 44 | Text(btnText) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/components/SettingItem.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.memerize.ui.components 2 | 3 | import android.view.SoundEffectConstants 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Surface 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.vector.ImageVector 18 | import androidx.compose.ui.platform.LocalView 19 | import androidx.compose.ui.tooling.preview.Preview 20 | import androidx.compose.ui.unit.dp 21 | 22 | @Composable 23 | fun SettingItem(title: String, description: String, icon: ImageVector?, onClick: () -> Unit) { 24 | val view = LocalView.current 25 | Surface( 26 | modifier = Modifier.clickable { 27 | view.playSoundEffect(SoundEffectConstants.CLICK) 28 | onClick() 29 | } 30 | ) { 31 | Row( 32 | modifier = Modifier 33 | .fillMaxWidth() 34 | .padding(16.dp, 24.dp), 35 | verticalAlignment = Alignment.CenterVertically 36 | ) { 37 | icon?.let { 38 | Icon( 39 | imageVector = icon, 40 | contentDescription = null, 41 | modifier = Modifier 42 | .padding(start = 8.dp, end = 16.dp) 43 | .size(24.dp), 44 | tint = MaterialTheme.colorScheme.secondary 45 | ) 46 | } 47 | Column( 48 | modifier = Modifier 49 | .weight(1f) 50 | .padding(start = if (icon == null) 16.dp else 0.dp) 51 | ) { 52 | Text( 53 | text = title, 54 | maxLines = 1, 55 | style = MaterialTheme.typography.titleLarge, 56 | color = MaterialTheme.colorScheme.onSurface 57 | ) 58 | Text( 59 | text = description, 60 | color = MaterialTheme.colorScheme.onSurfaceVariant, 61 | maxLines = 1, 62 | style = MaterialTheme.typography.bodyMedium 63 | ) 64 | } 65 | } 66 | } 67 | } 68 | 69 | @Preview 70 | @Composable 71 | fun SettingItemPreview() { 72 | SettingItem(title = "Setting Item", description = "Description", onClick = {}, icon = null) 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/components/SortBottomSheet.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/14/23, 1:10 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.components 9 | 10 | import androidx.compose.foundation.layout.Arrangement 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.Row 13 | import androidx.compose.foundation.layout.fillMaxWidth 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.material3.ExperimentalMaterial3Api 16 | import androidx.compose.material3.FilterChip 17 | import androidx.compose.material3.MaterialTheme 18 | import androidx.compose.material3.ModalBottomSheet 19 | import androidx.compose.material3.Text 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.res.stringResource 24 | import androidx.compose.ui.unit.dp 25 | import app.suhasdissa.memerize.R 26 | import app.suhasdissa.memerize.backend.model.Sort 27 | 28 | @OptIn(ExperimentalMaterial3Api::class) 29 | @Composable 30 | fun SortBottomSheet(currentSort: Sort, onSelect: (Sort) -> Unit, onDismissRequest: () -> Unit) { 31 | val options = remember { listOf(Sort.Hot, Sort.New, Sort.Rising) } 32 | val topOptions = remember { listOf(Sort.Top.Today, Sort.Top.Week, Sort.Top.Month) } 33 | 34 | ModalBottomSheet(onDismissRequest) { 35 | Column( 36 | modifier = Modifier 37 | .fillMaxWidth() 38 | .padding(16.dp) 39 | ) { 40 | Text( 41 | text = stringResource(R.string.sort_by), 42 | style = MaterialTheme.typography.titleLarge 43 | ) 44 | Row(Modifier.fillMaxWidth(), Arrangement.SpaceEvenly) { 45 | options.forEach { 46 | SortFilterChip(selected = currentSort == it, sort = it, onSelect) 47 | } 48 | } 49 | Text( 50 | text = stringResource(id = R.string.top), 51 | style = MaterialTheme.typography.titleMedium 52 | ) 53 | Row(Modifier.fillMaxWidth(), Arrangement.SpaceEvenly) { 54 | topOptions.forEach { 55 | SortFilterChip(selected = currentSort == it, sort = it, onSelect) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | @OptIn(ExperimentalMaterial3Api::class) 63 | @Composable 64 | fun SortFilterChip(selected: Boolean, sort: Sort, onSelect: (Sort) -> Unit) { 65 | FilterChip( 66 | selected, 67 | onClick = { onSelect(sort) }, 68 | label = { Text(stringResource(id = sort.name)) } 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/components/SubredditCard.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 7/30/23, 1:22 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.components 9 | 10 | import android.view.SoundEffectConstants 11 | import androidx.compose.foundation.clickable 12 | import androidx.compose.foundation.layout.Column 13 | import androidx.compose.foundation.layout.Row 14 | import androidx.compose.foundation.layout.aspectRatio 15 | import androidx.compose.foundation.layout.fillMaxWidth 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.layout.size 18 | import androidx.compose.foundation.shape.CircleShape 19 | import androidx.compose.material3.MaterialTheme 20 | import androidx.compose.material3.Text 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.draw.clip 25 | import androidx.compose.ui.layout.ContentScale 26 | import androidx.compose.ui.platform.LocalView 27 | import androidx.compose.ui.text.style.TextOverflow 28 | import androidx.compose.ui.unit.dp 29 | import coil.compose.AsyncImage 30 | 31 | @Composable 32 | fun SubredditCardCompact( 33 | thumbnail: String?, 34 | title: String, 35 | onClickCard: () -> Unit, 36 | TrailingContent: @Composable () -> Unit, 37 | modifier: Modifier = Modifier 38 | ) { 39 | val view = LocalView.current 40 | Row( 41 | modifier 42 | .fillMaxWidth() 43 | .clickable { 44 | view.playSoundEffect(SoundEffectConstants.CLICK) 45 | onClickCard() 46 | }, 47 | verticalAlignment = Alignment.CenterVertically 48 | ) { 49 | AsyncImage( 50 | modifier = Modifier 51 | .size(64.dp) 52 | .padding(8.dp) 53 | .aspectRatio(1f) 54 | .clip(CircleShape), 55 | model = thumbnail, 56 | contentDescription = null, 57 | contentScale = ContentScale.Crop 58 | ) 59 | Column( 60 | Modifier 61 | .weight(1f) 62 | .padding(8.dp) 63 | ) { 64 | Text( 65 | title, 66 | style = MaterialTheme.typography.titleMedium, 67 | maxLines = 1, 68 | overflow = TextOverflow.Ellipsis 69 | ) 70 | } 71 | 72 | Column( 73 | Modifier 74 | .padding(8.dp) 75 | ) { 76 | TrailingContent() 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/components/VideoCard.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 12/20/22, 8:56 AM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.components 9 | 10 | import androidx.compose.foundation.clickable 11 | import androidx.compose.foundation.layout.Box 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.layout.size 14 | import androidx.compose.foundation.shape.CircleShape 15 | import androidx.compose.material.icons.Icons 16 | import androidx.compose.material.icons.filled.PlayCircle 17 | import androidx.compose.material3.Card 18 | import androidx.compose.material3.Icon 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.res.stringResource 23 | import androidx.compose.ui.tooling.preview.Preview 24 | import androidx.compose.ui.unit.dp 25 | 26 | @Composable 27 | fun VideoCard( 28 | onClickVideo: () -> Unit, 29 | title: String, 30 | preview: String, 31 | modifier: Modifier = Modifier 32 | ) { 33 | Box( 34 | contentAlignment = Alignment.Center, 35 | modifier = modifier 36 | .fillMaxSize() 37 | ) { 38 | ImageCard({ onClickVideo.invoke() }, preview, title) 39 | Card(modifier.clickable(onClick = { onClickVideo.invoke() }), shape = CircleShape) { 40 | Icon( 41 | modifier = modifier.size(64.dp), 42 | imageVector = Icons.Default.PlayCircle, 43 | contentDescription = stringResource( 44 | app.suhasdissa.memerize.R.string.play_video_hint 45 | ) 46 | ) 47 | } 48 | } 49 | } 50 | 51 | @Preview 52 | @Composable 53 | fun VideoCardPreview() { 54 | VideoCard(onClickVideo = {}, title = "Preview", preview = "") 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/screens/primary/LemmyMemeScreen.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/4/23, 11:03 AM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.screens.primary 9 | 10 | import androidx.activity.ComponentActivity 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.Row 13 | import androidx.compose.foundation.layout.Spacer 14 | import androidx.compose.foundation.layout.aspectRatio 15 | import androidx.compose.foundation.layout.fillMaxSize 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.layout.size 18 | import androidx.compose.foundation.layout.width 19 | import androidx.compose.foundation.shape.CircleShape 20 | import androidx.compose.material.icons.Icons 21 | import androidx.compose.material.icons.filled.FilterList 22 | import androidx.compose.material3.ExperimentalMaterial3Api 23 | import androidx.compose.material3.Icon 24 | import androidx.compose.material3.IconButton 25 | import androidx.compose.material3.MaterialTheme 26 | import androidx.compose.material3.Scaffold 27 | import androidx.compose.material3.Text 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.setValue 34 | import androidx.compose.ui.Alignment 35 | import androidx.compose.ui.Modifier 36 | import androidx.compose.ui.draw.clip 37 | import androidx.compose.ui.layout.ContentScale 38 | import androidx.compose.ui.platform.LocalContext 39 | import androidx.compose.ui.res.stringResource 40 | import androidx.compose.ui.unit.dp 41 | import androidx.lifecycle.viewmodel.compose.viewModel 42 | import app.suhasdissa.memerize.R 43 | import app.suhasdissa.memerize.backend.viewmodels.LemmyViewModel 44 | import app.suhasdissa.memerize.backend.viewmodels.state.MemeUiState 45 | import app.suhasdissa.memerize.ui.components.LoadingScreen 46 | import app.suhasdissa.memerize.ui.components.MemeGrid 47 | import app.suhasdissa.memerize.ui.components.RetryScreen 48 | import app.suhasdissa.memerize.ui.components.SortBottomSheet 49 | import coil.compose.AsyncImage 50 | import coil.request.ImageRequest 51 | 52 | @OptIn(ExperimentalMaterial3Api::class) 53 | @Composable 54 | fun LemmyMemeScreen( 55 | modifier: Modifier = Modifier, 56 | lemmyViewModel: LemmyViewModel = viewModel( 57 | LocalContext.current as ComponentActivity, 58 | factory = LemmyViewModel.Factory 59 | ), 60 | onClickCard: (Int) -> Unit 61 | ) { 62 | var showFilterButtons by remember { mutableStateOf(false) } 63 | Scaffold( 64 | modifier = Modifier.fillMaxSize(), 65 | topBar = { 66 | TopAppBar(title = { 67 | Row(verticalAlignment = Alignment.CenterVertically) { 68 | lemmyViewModel.currentCommunity?.let { 69 | AsyncImage( 70 | modifier = Modifier 71 | .size(36.dp) 72 | .aspectRatio(1f) 73 | .clip(CircleShape), 74 | model = ImageRequest.Builder(context = LocalContext.current) 75 | .data(it.iconUrl).crossfade(true).build(), 76 | contentDescription = null, 77 | contentScale = ContentScale.Crop 78 | ) 79 | } 80 | Spacer(Modifier.width(8.dp)) 81 | Column { 82 | Text( 83 | stringResource(R.string.lemmy) 84 | ) 85 | lemmyViewModel.currentCommunity?.let { 86 | Text(it.name, style = MaterialTheme.typography.bodySmall) 87 | } 88 | } 89 | } 90 | }, actions = { 91 | lemmyViewModel.currentCommunity?.let { 92 | IconButton(onClick = { showFilterButtons = !showFilterButtons }) { 93 | Icon( 94 | imageVector = Icons.Default.FilterList, 95 | contentDescription = stringResource(R.string.filter_by_time) 96 | ) 97 | } 98 | } 99 | }) 100 | } 101 | ) { paddingValues -> 102 | Column(Modifier.padding(paddingValues)) { 103 | when (val memeDataState = lemmyViewModel.memeUiState) { 104 | is MemeUiState.Loading -> LoadingScreen(modifier) 105 | is MemeUiState.Error -> RetryScreen( 106 | stringResource(R.string.error_loading_online_memes), 107 | stringResource(R.string.show_offline_memes), 108 | modifier, 109 | onRetry = { lemmyViewModel.getLocalMemes() } 110 | ) 111 | 112 | is MemeUiState.Success -> MemeGrid( 113 | memeDataState.memes, 114 | onClickCard 115 | ) 116 | } 117 | } 118 | } 119 | if (showFilterButtons) { 120 | SortBottomSheet(currentSort = lemmyViewModel.currentSortTime, onSelect = { 121 | showFilterButtons = false 122 | lemmyViewModel.getMemePhotos(sort = it) 123 | }, onDismissRequest = { showFilterButtons = false }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/screens/primary/RedditMemeScreen.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.screens.primary 9 | 10 | import androidx.activity.ComponentActivity 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.Row 13 | import androidx.compose.foundation.layout.Spacer 14 | import androidx.compose.foundation.layout.aspectRatio 15 | import androidx.compose.foundation.layout.fillMaxSize 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.layout.size 18 | import androidx.compose.foundation.layout.width 19 | import androidx.compose.foundation.shape.CircleShape 20 | import androidx.compose.material.icons.Icons 21 | import androidx.compose.material.icons.filled.FilterList 22 | import androidx.compose.material3.ExperimentalMaterial3Api 23 | import androidx.compose.material3.Icon 24 | import androidx.compose.material3.IconButton 25 | import androidx.compose.material3.MaterialTheme 26 | import androidx.compose.material3.Scaffold 27 | import androidx.compose.material3.Text 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.setValue 34 | import androidx.compose.ui.Alignment 35 | import androidx.compose.ui.Modifier 36 | import androidx.compose.ui.draw.clip 37 | import androidx.compose.ui.layout.ContentScale 38 | import androidx.compose.ui.platform.LocalContext 39 | import androidx.compose.ui.res.stringResource 40 | import androidx.compose.ui.unit.dp 41 | import androidx.lifecycle.viewmodel.compose.viewModel 42 | import app.suhasdissa.memerize.R 43 | import app.suhasdissa.memerize.backend.viewmodels.RedditViewModel 44 | import app.suhasdissa.memerize.backend.viewmodels.state.MemeUiState 45 | import app.suhasdissa.memerize.ui.components.LoadingScreen 46 | import app.suhasdissa.memerize.ui.components.MemeGrid 47 | import app.suhasdissa.memerize.ui.components.RetryScreen 48 | import app.suhasdissa.memerize.ui.components.SortBottomSheet 49 | import coil.compose.AsyncImage 50 | import coil.request.ImageRequest 51 | 52 | @OptIn(ExperimentalMaterial3Api::class) 53 | @Composable 54 | fun RedditMemeScreen( 55 | modifier: Modifier = Modifier, 56 | redditViewModel: RedditViewModel = viewModel( 57 | LocalContext.current as ComponentActivity, 58 | factory = RedditViewModel.Factory 59 | ), 60 | onClickCard: (Int) -> Unit 61 | ) { 62 | var showFilterButtons by remember { mutableStateOf(false) } 63 | Scaffold( 64 | modifier = Modifier.fillMaxSize(), 65 | topBar = { 66 | TopAppBar(title = { 67 | Row(verticalAlignment = Alignment.CenterVertically) { 68 | redditViewModel.currentSubreddit?.let { 69 | AsyncImage( 70 | modifier = Modifier 71 | .size(36.dp) 72 | .aspectRatio(1f) 73 | .clip(CircleShape), 74 | model = ImageRequest.Builder(context = LocalContext.current) 75 | .data(it.iconUrl).crossfade(true).build(), 76 | contentDescription = null, 77 | contentScale = ContentScale.Crop 78 | ) 79 | } 80 | Spacer(Modifier.width(8.dp)) 81 | Column { 82 | Text( 83 | stringResource(R.string.reddit) 84 | ) 85 | redditViewModel.currentSubreddit?.let { 86 | Text(it.name, style = MaterialTheme.typography.bodySmall) 87 | } 88 | } 89 | } 90 | }, actions = { 91 | redditViewModel.currentSubreddit?.let { 92 | IconButton(onClick = { showFilterButtons = !showFilterButtons }) { 93 | Icon( 94 | imageVector = Icons.Default.FilterList, 95 | contentDescription = stringResource(R.string.filter_by_time) 96 | ) 97 | } 98 | } 99 | }) 100 | } 101 | ) { paddingValues -> 102 | Column(Modifier.padding(paddingValues)) { 103 | when (val memeDataState = redditViewModel.memeUiState) { 104 | is MemeUiState.Loading -> LoadingScreen(modifier) 105 | is MemeUiState.Error -> RetryScreen( 106 | stringResource(R.string.error_loading_online_memes), 107 | stringResource(R.string.show_offline_memes), 108 | modifier, 109 | onRetry = { redditViewModel.getLocalMemes() } 110 | ) 111 | 112 | is MemeUiState.Success -> MemeGrid( 113 | memeDataState.memes, 114 | onClickCard 115 | ) 116 | } 117 | } 118 | } 119 | if (showFilterButtons) { 120 | SortBottomSheet(currentSort = redditViewModel.currentSortTime, onSelect = { 121 | showFilterButtons = false 122 | redditViewModel.getMemePhotos(sort = it) 123 | }, onDismissRequest = { showFilterButtons = false }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/screens/secondary/MemeFeedView.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/15/23, 12:48 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.screens.secondary 9 | 10 | import androidx.activity.ComponentActivity 11 | import androidx.compose.foundation.ExperimentalFoundationApi 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.pager.VerticalPager 14 | import androidx.compose.foundation.pager.rememberPagerState 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.platform.LocalContext 18 | import androidx.lifecycle.viewmodel.compose.viewModel 19 | import app.suhasdissa.memerize.backend.database.entity.Meme 20 | import app.suhasdissa.memerize.backend.viewmodels.LemmyViewModel 21 | import app.suhasdissa.memerize.backend.viewmodels.RedditViewModel 22 | import app.suhasdissa.memerize.backend.viewmodels.state.MemeUiState 23 | 24 | @Composable 25 | fun RedditMemeFeed( 26 | initialPage: Int, 27 | redditViewModel: RedditViewModel = viewModel( 28 | LocalContext.current as ComponentActivity, 29 | factory = RedditViewModel.Factory 30 | ) 31 | ) { 32 | when (val state = redditViewModel.memeUiState) { 33 | is MemeUiState.Success -> MemeFeedView(initialPage, memes = state.memes) 34 | else -> {} 35 | } 36 | } 37 | 38 | @Composable 39 | fun LemmyMemeFeed( 40 | initialPage: Int, 41 | lemmyViewModel: LemmyViewModel = viewModel( 42 | LocalContext.current as ComponentActivity, 43 | factory = LemmyViewModel.Factory 44 | ) 45 | ) { 46 | when (val state = lemmyViewModel.memeUiState) { 47 | is MemeUiState.Success -> MemeFeedView(initialPage, memes = state.memes) 48 | else -> {} 49 | } 50 | } 51 | 52 | @OptIn(ExperimentalFoundationApi::class) 53 | @Composable 54 | private fun MemeFeedView(initialPage: Int, memes: List) { 55 | val pagerState = rememberPagerState( 56 | initialPage = initialPage, 57 | initialPageOffsetFraction = 0f 58 | ) { 59 | memes.size 60 | } 61 | VerticalPager(modifier = Modifier.fillMaxSize(), state = pagerState) { 62 | with(memes[it]) { 63 | if (isVideo) { 64 | VideoView(this, playWhenReady = (it == pagerState.currentPage)) 65 | } else { 66 | PhotoView(this) 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/screens/settings/AboutScreen.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 7/9/23, 3:39 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.screens.settings 9 | 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.lazy.LazyColumn 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.filled.ContactSupport 15 | import androidx.compose.material.icons.filled.Description 16 | import androidx.compose.material.icons.filled.Info 17 | import androidx.compose.material.icons.filled.NewReleases 18 | import androidx.compose.material3.CenterAlignedTopAppBar 19 | import androidx.compose.material3.ExperimentalMaterial3Api 20 | import androidx.compose.material3.MaterialTheme 21 | import androidx.compose.material3.Scaffold 22 | import androidx.compose.material3.Text 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.platform.LocalContext 26 | import androidx.compose.ui.res.stringResource 27 | import androidx.compose.ui.tooling.preview.Preview 28 | import androidx.lifecycle.viewmodel.compose.viewModel 29 | import app.suhasdissa.memerize.R 30 | import app.suhasdissa.memerize.backend.viewmodels.CheckUpdateViewModel 31 | import app.suhasdissa.memerize.ui.components.SettingItem 32 | import app.suhasdissa.memerize.utils.openBrowser 33 | 34 | @OptIn(ExperimentalMaterial3Api::class) 35 | @Composable 36 | fun AboutScreen( 37 | modifier: Modifier = Modifier, 38 | updateViewModel: CheckUpdateViewModel = viewModel() 39 | ) { 40 | val context = LocalContext.current 41 | val githubRepo = "https://github.com/SuhasDissa/MemerizeApp" 42 | 43 | Scaffold( 44 | modifier = Modifier.fillMaxSize(), 45 | topBar = { 46 | CenterAlignedTopAppBar(title = { 47 | Text( 48 | stringResource(R.string.about), 49 | color = MaterialTheme.colorScheme.primary 50 | ) 51 | }) 52 | } 53 | ) { paddingValues -> 54 | LazyColumn( 55 | modifier 56 | .fillMaxSize() 57 | .padding(paddingValues) 58 | ) { 59 | item { 60 | SettingItem( 61 | title = stringResource(R.string.readme), 62 | description = stringResource(R.string.check_repo_and_readme), 63 | onClick = { openBrowser(context, githubRepo) }, 64 | icon = Icons.Default.Description 65 | ) 66 | } 67 | item { 68 | SettingItem( 69 | title = stringResource(R.string.latest_release), 70 | description = "${updateViewModel.latestVersion}", 71 | onClick = { 72 | openBrowser( 73 | context, 74 | "$githubRepo/releases/latest" 75 | ) 76 | }, 77 | icon = Icons.Default.NewReleases 78 | ) 79 | } 80 | item { 81 | SettingItem( 82 | title = stringResource(R.string.github_issue), 83 | description = stringResource(R.string.github_issue_description), 84 | onClick = { 85 | openBrowser( 86 | context, 87 | "$githubRepo/issues" 88 | ) 89 | }, 90 | icon = Icons.Default.ContactSupport 91 | ) 92 | } 93 | item { 94 | SettingItem( 95 | title = stringResource(R.string.current_version), 96 | description = "${updateViewModel.currentVersion}", 97 | onClick = {}, 98 | icon = Icons.Default.Info 99 | ) 100 | } 101 | } 102 | } 103 | } 104 | 105 | @Composable 106 | @Preview 107 | fun AboutScreenPreview() { 108 | AboutScreen() 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/screens/settings/SettingsScreen.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.screens.settings 9 | 10 | import android.content.Intent 11 | import android.net.Uri 12 | import android.util.Log 13 | import androidx.activity.compose.rememberLauncherForActivityResult 14 | import androidx.activity.result.contract.ActivityResultContracts 15 | import androidx.compose.foundation.layout.Arrangement 16 | import androidx.compose.foundation.layout.fillMaxSize 17 | import androidx.compose.foundation.layout.padding 18 | import androidx.compose.foundation.lazy.LazyColumn 19 | import androidx.compose.material.icons.Icons 20 | import androidx.compose.material.icons.filled.Menu 21 | import androidx.compose.material.icons.filled.Storage 22 | import androidx.compose.material.icons.outlined.Folder 23 | import androidx.compose.material.icons.outlined.Info 24 | import androidx.compose.material3.CenterAlignedTopAppBar 25 | import androidx.compose.material3.ExperimentalMaterial3Api 26 | import androidx.compose.material3.Icon 27 | import androidx.compose.material3.IconButton 28 | import androidx.compose.material3.MaterialTheme 29 | import androidx.compose.material3.Scaffold 30 | import androidx.compose.material3.Text 31 | import androidx.compose.runtime.Composable 32 | import androidx.compose.runtime.getValue 33 | import androidx.compose.runtime.mutableStateOf 34 | import androidx.compose.runtime.remember 35 | import androidx.compose.runtime.setValue 36 | import androidx.compose.ui.Modifier 37 | import androidx.compose.ui.platform.LocalContext 38 | import androidx.compose.ui.res.stringResource 39 | import androidx.compose.ui.unit.dp 40 | import androidx.core.content.edit 41 | import app.suhasdissa.memerize.R 42 | import app.suhasdissa.memerize.ui.components.CacheSizeDialog 43 | import app.suhasdissa.memerize.ui.components.SettingItem 44 | import app.suhasdissa.memerize.utils.SaveDirectoryKey 45 | import app.suhasdissa.memerize.utils.preferences 46 | 47 | @OptIn(ExperimentalMaterial3Api::class) 48 | @Composable 49 | fun SettingsScreen( 50 | onDrawerOpen: () -> Unit, 51 | onAboutClick: () -> Unit 52 | ) { 53 | val context = LocalContext.current 54 | val directoryPicker = 55 | rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { 56 | it ?: return@rememberLauncherForActivityResult 57 | context.contentResolver.takePersistableUriPermission( 58 | it, 59 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 60 | ) 61 | Log.d("FIle path", it.toString()) 62 | context.preferences.edit { putString(SaveDirectoryKey, it.toString()) } 63 | } 64 | var showImageCacheDialog by remember { mutableStateOf(false) } 65 | Scaffold(modifier = Modifier.fillMaxSize(), topBar = { 66 | CenterAlignedTopAppBar(navigationIcon = { 67 | IconButton(onClick = { 68 | onDrawerOpen.invoke() 69 | }) { 70 | Icon( 71 | imageVector = Icons.Default.Menu, 72 | contentDescription = stringResource(R.string.open_navigation_drawer) 73 | ) 74 | } 75 | }, title = { 76 | Text( 77 | stringResource(R.string.settings), 78 | color = MaterialTheme.colorScheme.primary 79 | ) 80 | }) 81 | }) { paddingValues -> 82 | LazyColumn( 83 | Modifier.fillMaxSize().padding(paddingValues), 84 | verticalArrangement = Arrangement.spacedBy(24.dp) 85 | ) { 86 | item { 87 | SettingItem( 88 | title = stringResource(R.string.download_location), 89 | description = stringResource(R.string.select_meme_download_location), 90 | onClick = { 91 | val lastDir = context.preferences.getString(SaveDirectoryKey, null) 92 | .takeIf { !it.isNullOrBlank() } 93 | directoryPicker.launch(lastDir?.let { Uri.parse(it) }) 94 | }, 95 | icon = Icons.Outlined.Folder 96 | ) 97 | } 98 | item { 99 | SettingItem( 100 | title = stringResource(R.string.image_cache_limit), 101 | description = stringResource(R.string.set_image_cache_limit), 102 | onClick = { 103 | showImageCacheDialog = true 104 | }, 105 | icon = Icons.Default.Storage 106 | ) 107 | } 108 | item { 109 | SettingItem( 110 | title = stringResource(R.string.about), 111 | description = stringResource(R.string.developer_contact), 112 | onClick = { onAboutClick() }, 113 | icon = Icons.Outlined.Info 114 | ) 115 | } 116 | } 117 | } 118 | 119 | if (showImageCacheDialog) { 120 | CacheSizeDialog { 121 | showImageCacheDialog = false 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.theme 9 | 10 | import androidx.compose.ui.graphics.Color 11 | 12 | val md_theme_light_primary = Color(0xFF984717) 13 | val md_theme_light_onPrimary = Color(0xFFFFFFFF) 14 | val md_theme_light_primaryContainer = Color(0xFFFFDBCB) 15 | val md_theme_light_onPrimaryContainer = Color(0xFF341100) 16 | val md_theme_light_secondary = Color(0xFF765849) 17 | val md_theme_light_onSecondary = Color(0xFFFFFFFF) 18 | val md_theme_light_secondaryContainer = Color(0xFFFFDBCB) 19 | val md_theme_light_onSecondaryContainer = Color(0xFF2C160B) 20 | val md_theme_light_tertiary = Color(0xFF655F31) 21 | val md_theme_light_onTertiary = Color(0xFFFFFFFF) 22 | val md_theme_light_tertiaryContainer = Color(0xFFECE4AA) 23 | val md_theme_light_onTertiaryContainer = Color(0xFF1F1C00) 24 | val md_theme_light_error = Color(0xFFBA1A1A) 25 | val md_theme_light_errorContainer = Color(0xFFFFDAD6) 26 | val md_theme_light_onError = Color(0xFFFFFFFF) 27 | val md_theme_light_onErrorContainer = Color(0xFF410002) 28 | val md_theme_light_background = Color(0xFFFFFBFF) 29 | val md_theme_light_onBackground = Color(0xFF201A18) 30 | val md_theme_light_surface = Color(0xFFFFFBFF) 31 | val md_theme_light_onSurface = Color(0xFF201A18) 32 | val md_theme_light_surfaceVariant = Color(0xFFF4DED5) 33 | val md_theme_light_onSurfaceVariant = Color(0xFF52443D) 34 | val md_theme_light_outline = Color(0xFF85736C) 35 | val md_theme_light_inverseOnSurface = Color(0xFFFBEEE9) 36 | val md_theme_light_inverseSurface = Color(0xFF362F2C) 37 | val md_theme_light_inversePrimary = Color(0xFFFFB692) 38 | val md_theme_light_surfaceTint = Color(0xFF984717) 39 | val md_theme_light_outlineVariant = Color(0xFFD7C2B9) 40 | val md_theme_light_scrim = Color(0xFF000000) 41 | 42 | val md_theme_dark_primary = Color(0xFFFFB692) 43 | val md_theme_dark_onPrimary = Color(0xFF562000) 44 | val md_theme_dark_primaryContainer = Color(0xFF793000) 45 | val md_theme_dark_onPrimaryContainer = Color(0xFFFFDBCB) 46 | val md_theme_dark_secondary = Color(0xFFE6BEAC) 47 | val md_theme_dark_onSecondary = Color(0xFF432A1E) 48 | val md_theme_dark_secondaryContainer = Color(0xFF5C4033) 49 | val md_theme_dark_onSecondaryContainer = Color(0xFFFFDBCB) 50 | val md_theme_dark_tertiary = Color(0xFFD0C890) 51 | val md_theme_dark_onTertiary = Color(0xFF353107) 52 | val md_theme_dark_tertiaryContainer = Color(0xFF4C481C) 53 | val md_theme_dark_onTertiaryContainer = Color(0xFFECE4AA) 54 | val md_theme_dark_error = Color(0xFFFFB4AB) 55 | val md_theme_dark_errorContainer = Color(0xFF93000A) 56 | val md_theme_dark_onError = Color(0xFF690005) 57 | val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) 58 | val md_theme_dark_background = Color(0xFF201A18) 59 | val md_theme_dark_onBackground = Color(0xFFEDE0DB) 60 | val md_theme_dark_surface = Color(0xFF201A18) 61 | val md_theme_dark_onSurface = Color(0xFFEDE0DB) 62 | val md_theme_dark_surfaceVariant = Color(0xFF52443D) 63 | val md_theme_dark_onSurfaceVariant = Color(0xFFD7C2B9) 64 | val md_theme_dark_outline = Color(0xFFA08D85) 65 | val md_theme_dark_inverseOnSurface = Color(0xFF201A18) 66 | val md_theme_dark_inverseSurface = Color(0xFFEDE0DB) 67 | val md_theme_dark_inversePrimary = Color(0xFF984717) 68 | val md_theme_dark_surfaceTint = Color(0xFFFFB692) 69 | val md_theme_dark_outlineVariant = Color(0xFF52443D) 70 | val md_theme_dark_scrim = Color(0xFF000000) 71 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.theme 9 | 10 | import android.os.Build 11 | import androidx.compose.foundation.isSystemInDarkTheme 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.darkColorScheme 14 | import androidx.compose.material3.dynamicDarkColorScheme 15 | import androidx.compose.material3.dynamicLightColorScheme 16 | import androidx.compose.material3.lightColorScheme 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.platform.LocalContext 19 | 20 | private val LightColorScheme = lightColorScheme( 21 | primary = md_theme_light_primary, 22 | onPrimary = md_theme_light_onPrimary, 23 | primaryContainer = md_theme_light_primaryContainer, 24 | onPrimaryContainer = md_theme_light_onPrimaryContainer, 25 | secondary = md_theme_light_secondary, 26 | onSecondary = md_theme_light_onSecondary, 27 | secondaryContainer = md_theme_light_secondaryContainer, 28 | onSecondaryContainer = md_theme_light_onSecondaryContainer, 29 | tertiary = md_theme_light_tertiary, 30 | onTertiary = md_theme_light_onTertiary, 31 | tertiaryContainer = md_theme_light_tertiaryContainer, 32 | onTertiaryContainer = md_theme_light_onTertiaryContainer, 33 | error = md_theme_light_error, 34 | errorContainer = md_theme_light_errorContainer, 35 | onError = md_theme_light_onError, 36 | onErrorContainer = md_theme_light_onErrorContainer, 37 | background = md_theme_light_background, 38 | onBackground = md_theme_light_onBackground, 39 | surface = md_theme_light_surface, 40 | onSurface = md_theme_light_onSurface, 41 | surfaceVariant = md_theme_light_surfaceVariant, 42 | onSurfaceVariant = md_theme_light_onSurfaceVariant, 43 | outline = md_theme_light_outline, 44 | inverseOnSurface = md_theme_light_inverseOnSurface, 45 | inverseSurface = md_theme_light_inverseSurface, 46 | inversePrimary = md_theme_light_inversePrimary, 47 | surfaceTint = md_theme_light_surfaceTint, 48 | outlineVariant = md_theme_light_outlineVariant, 49 | scrim = md_theme_light_scrim 50 | ) 51 | 52 | private val DarkColorScheme = darkColorScheme( 53 | primary = md_theme_dark_primary, 54 | onPrimary = md_theme_dark_onPrimary, 55 | primaryContainer = md_theme_dark_primaryContainer, 56 | onPrimaryContainer = md_theme_dark_onPrimaryContainer, 57 | secondary = md_theme_dark_secondary, 58 | onSecondary = md_theme_dark_onSecondary, 59 | secondaryContainer = md_theme_dark_secondaryContainer, 60 | onSecondaryContainer = md_theme_dark_onSecondaryContainer, 61 | tertiary = md_theme_dark_tertiary, 62 | onTertiary = md_theme_dark_onTertiary, 63 | tertiaryContainer = md_theme_dark_tertiaryContainer, 64 | onTertiaryContainer = md_theme_dark_onTertiaryContainer, 65 | error = md_theme_dark_error, 66 | errorContainer = md_theme_dark_errorContainer, 67 | onError = md_theme_dark_onError, 68 | onErrorContainer = md_theme_dark_onErrorContainer, 69 | background = md_theme_dark_background, 70 | onBackground = md_theme_dark_onBackground, 71 | surface = md_theme_dark_surface, 72 | onSurface = md_theme_dark_onSurface, 73 | surfaceVariant = md_theme_dark_surfaceVariant, 74 | onSurfaceVariant = md_theme_dark_onSurfaceVariant, 75 | outline = md_theme_dark_outline, 76 | inverseOnSurface = md_theme_dark_inverseOnSurface, 77 | inverseSurface = md_theme_dark_inverseSurface, 78 | inversePrimary = md_theme_dark_inversePrimary, 79 | surfaceTint = md_theme_dark_surfaceTint, 80 | outlineVariant = md_theme_dark_outlineVariant, 81 | scrim = md_theme_dark_scrim 82 | ) 83 | 84 | @Composable 85 | fun MemerizeTheme( 86 | darkTheme: Boolean = isSystemInDarkTheme(), 87 | // Dynamic color is available on Android 12+ 88 | dynamicColor: Boolean = true, 89 | content: @Composable () -> Unit 90 | ) { 91 | val colorScheme = when { 92 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 93 | val context = LocalContext.current 94 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 95 | } 96 | 97 | darkTheme -> DarkColorScheme 98 | else -> LightColorScheme 99 | } 100 | 101 | MaterialTheme( 102 | colorScheme = colorScheme, 103 | typography = Typography, 104 | content = content 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.ui.theme 9 | 10 | import androidx.compose.material3.Typography 11 | import androidx.compose.ui.text.TextStyle 12 | import androidx.compose.ui.text.font.FontFamily 13 | import androidx.compose.ui.text.font.FontWeight 14 | import androidx.compose.ui.unit.sp 15 | 16 | // Set of Material typography styles to start with 17 | val Typography = Typography( 18 | bodyLarge = TextStyle( 19 | fontFamily = FontFamily.Default, 20 | fontWeight = FontWeight.Normal, 21 | fontSize = 16.sp, 22 | lineHeight = 24.sp, 23 | letterSpacing = 0.5.sp 24 | ) 25 | /* Other default text styles to override 26 | titleLarge = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Normal, 29 | fontSize = 22.sp, 30 | lineHeight = 28.sp, 31 | letterSpacing = 0.sp 32 | ), 33 | labelSmall = TextStyle( 34 | fontFamily = FontFamily.Default, 35 | fontWeight = FontWeight.Medium, 36 | fontSize = 11.sp, 37 | lineHeight = 16.sp, 38 | letterSpacing = 0.5.sp 39 | ) 40 | */ 41 | ) 42 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/utils/CheckUpdate.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.utils 9 | 10 | import android.content.Context 11 | import android.content.pm.PackageManager 12 | import android.os.Build 13 | import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory 14 | import kotlinx.serialization.SerialName 15 | import kotlinx.serialization.Serializable 16 | import kotlinx.serialization.json.Json 17 | import okhttp3.MediaType.Companion.toMediaType 18 | import retrofit2.Retrofit 19 | import retrofit2.http.GET 20 | import retrofit2.http.Headers 21 | import java.util.regex.Pattern 22 | 23 | object UpdateUtil { 24 | var currentVersion = 0f 25 | 26 | private suspend fun getLatestRelease(): LatestRelease? { 27 | return try { 28 | UpdateApi.retrofitService.getLatestRelease() 29 | } catch (e: Exception) { 30 | null 31 | } 32 | } 33 | 34 | suspend fun getLatestVersion(): Float? { 35 | return getLatestRelease()?.let { 36 | it.tagName.toVersion() 37 | } 38 | } 39 | 40 | fun getCurrentVersion(context: Context) { 41 | currentVersion = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 42 | context.packageManager.getPackageInfo( 43 | context.packageName, 44 | PackageManager.PackageInfoFlags.of(0) 45 | ).versionName.toFloat() 46 | } else { 47 | context.packageManager.getPackageInfo( 48 | context.packageName, 49 | 0 50 | ).versionName.toFloat() 51 | } 52 | } 53 | 54 | private val pattern = Pattern.compile("""v(.+)""") 55 | 56 | private fun String?.toVersion(): Float = this?.run { 57 | val matcher = pattern.matcher(this) 58 | if (matcher.find()) { 59 | matcher.group(1)?.toFloat() ?: 0f 60 | } else { 61 | 0f 62 | } 63 | } ?: 0f 64 | } 65 | 66 | @Serializable 67 | data class LatestRelease( 68 | @SerialName("tag_name") val tagName: String? = null 69 | ) 70 | 71 | private val jsonFormat = Json { ignoreUnknownKeys = true } 72 | 73 | private val retrofitVideo = Retrofit.Builder() 74 | .baseUrl("https://api.github.com/") 75 | .addConverterFactory(jsonFormat.asConverterFactory("application/json".toMediaType())) 76 | .build() 77 | 78 | interface UpdateApiService { 79 | @Headers( 80 | "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36" 81 | ) 82 | @GET("repos/SuhasDissa/MemerizeApp/releases/latest") 83 | suspend fun getLatestRelease(): LatestRelease 84 | } 85 | 86 | object UpdateApi { 87 | val retrofitService: UpdateApiService by lazy { 88 | retrofitVideo.create(UpdateApiService::class.java) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/utils/OpenBrowser.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.utils 9 | 10 | import android.content.Context 11 | import android.content.Intent 12 | import android.net.Uri 13 | 14 | fun openBrowser(context: Context, url: String) { 15 | val viewIntent: Intent = Intent().apply { 16 | action = Intent.ACTION_VIEW 17 | data = Uri.parse(url) 18 | } 19 | context.startActivity(viewIntent) 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/utils/PlayerState.kt: -------------------------------------------------------------------------------- 1 | package app.suhasdissa.memerize.utils 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.State 5 | import androidx.compose.runtime.produceState 6 | import androidx.media3.common.MediaItem 7 | import androidx.media3.common.Player 8 | import kotlinx.coroutines.delay 9 | import kotlinx.coroutines.isActive 10 | import kotlinx.coroutines.launch 11 | 12 | @Composable 13 | fun Player.positionAndDurationState(): State> { 14 | return produceState( 15 | initialValue = (currentPosition to duration.let { if (it < 0) null else it }), 16 | this 17 | ) { 18 | var isSeeking = false 19 | val listener = object : Player.Listener { 20 | override fun onPlaybackStateChanged(playbackState: Int) { 21 | if (playbackState == Player.STATE_READY) { 22 | isSeeking = false 23 | } 24 | } 25 | 26 | override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { 27 | value = currentPosition to value.second 28 | } 29 | 30 | override fun onPositionDiscontinuity( 31 | oldPosition: Player.PositionInfo, 32 | newPosition: Player.PositionInfo, 33 | reason: Int 34 | ) { 35 | if (reason == Player.DISCONTINUITY_REASON_SEEK) { 36 | isSeeking = true 37 | value = currentPosition to duration.let { if (it < 0) null else it } 38 | } 39 | } 40 | } 41 | addListener(listener) 42 | 43 | val pollJob = launch { 44 | while (isActive) { 45 | if (!isSeeking) { 46 | value = currentPosition to duration.let { if (it < 0) null else it } 47 | } 48 | delay(500) 49 | } 50 | } 51 | if (!isActive) { 52 | pollJob.cancel() 53 | removeListener(listener) 54 | } 55 | } 56 | } 57 | 58 | enum class PlayerState { 59 | Buffer, Play, Pause 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/utils/Preferences.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 8/4/23, 2:32 PM 3 | Copyright (c) 2023 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.utils 9 | 10 | import android.content.Context 11 | import android.content.SharedPreferences 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.MutableState 14 | import androidx.compose.runtime.SnapshotMutationPolicy 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.ui.platform.LocalContext 18 | import androidx.core.content.edit 19 | 20 | const val SaveDirectoryKey = "saveDirectory" 21 | const val imageCacheKey = "imageCacheLimit" 22 | 23 | const val defaultImageCacheSize: Int = 256 24 | 25 | inline fun > SharedPreferences.getEnum( 26 | key: String, 27 | defaultValue: T 28 | ): T = 29 | getString(key, null)?.let { 30 | try { 31 | enumValueOf(it) 32 | } catch (e: IllegalArgumentException) { 33 | null 34 | } 35 | } ?: defaultValue 36 | 37 | inline fun > SharedPreferences.Editor.putEnum( 38 | key: String, 39 | value: T 40 | ): SharedPreferences.Editor = 41 | putString(key, value.name) 42 | 43 | val Context.preferences: SharedPreferences 44 | get() = getSharedPreferences("preferences", Context.MODE_PRIVATE) 45 | 46 | @Composable 47 | fun rememberPreference(key: String, defaultValue: Boolean): MutableState { 48 | val context = LocalContext.current 49 | return remember { 50 | mutableStatePreferenceOf(context.preferences.getBoolean(key, defaultValue)) { 51 | context.preferences.edit { putBoolean(key, it) } 52 | } 53 | } 54 | } 55 | 56 | @Composable 57 | fun rememberPreference(key: String, defaultValue: Int): MutableState { 58 | val context = LocalContext.current 59 | return remember { 60 | mutableStatePreferenceOf(context.preferences.getInt(key, defaultValue)) { 61 | context.preferences.edit { putInt(key, it) } 62 | } 63 | } 64 | } 65 | 66 | @Composable 67 | fun rememberPreference(key: String, defaultValue: String): MutableState { 68 | val context = LocalContext.current 69 | return remember { 70 | mutableStatePreferenceOf(context.preferences.getString(key, null) ?: defaultValue) { 71 | context.preferences.edit { putString(key, it) } 72 | } 73 | } 74 | } 75 | 76 | @Composable 77 | inline fun > rememberPreference(key: String, defaultValue: T): MutableState { 78 | val context = LocalContext.current 79 | return remember { 80 | mutableStatePreferenceOf(context.preferences.getEnum(key, defaultValue)) { 81 | context.preferences.edit { putEnum(key, it) } 82 | } 83 | } 84 | } 85 | 86 | inline fun mutableStatePreferenceOf( 87 | value: T, 88 | crossinline onStructuralInequality: (newValue: T) -> Unit 89 | ) = 90 | mutableStateOf( 91 | value = value, 92 | policy = object : SnapshotMutationPolicy { 93 | override fun equivalent(a: T, b: T): Boolean { 94 | val areEquals = a == b 95 | if (!areEquals) onStructuralInequality(b) 96 | return areEquals 97 | } 98 | } 99 | ) 100 | -------------------------------------------------------------------------------- /app/src/main/java/app/suhasdissa/memerize/utils/ShareUrl.kt: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | Created By Suhas Dissanayake on 11/23/22, 4:16 PM 3 | Copyright (c) 2022 4 | https://github.com/SuhasDissa/ 5 | All Rights Reserved 6 | ******************************************************************************/ 7 | 8 | package app.suhasdissa.memerize.utils 9 | 10 | import android.content.Context 11 | import android.content.Intent 12 | 13 | fun shareUrl(context: Context, url: String) { 14 | var shareurl = url 15 | if (url.contains("v.redd.it")) { 16 | shareurl = url.split("/").slice(0..3).joinToString("/") 17 | } 18 | val sendIntent: Intent = Intent().apply { 19 | action = Intent.ACTION_SEND 20 | putExtra(Intent.EXTRA_TEXT, shareurl) 21 | type = "text/plain" 22 | } 23 | val shareIntent = Intent.createChooser(sendIntent, "Send Photo to..") 24 | context.startActivity(shareIntent) 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_broken_image.xml: -------------------------------------------------------------------------------- 1 | 16 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/loading_img.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 28 | 34 | 40 | 46 | 52 | 58 | 64 | 70 | 76 | 82 | 88 | 94 | 95 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/reddit_placeholder.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuhasDissa/MemerizeApp/17ffdf78ce0b8e85aef31d31f3d8c4bd5f72b7c0/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuhasDissa/MemerizeApp/17ffdf78ce0b8e85aef31d31f3d8c4bd5f72b7c0/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuhasDissa/MemerizeApp/17ffdf78ce0b8e85aef31d31f3d8c4bd5f72b7c0/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuhasDissa/MemerizeApp/17ffdf78ce0b8e85aef31d31f3d8c4bd5f72b7c0/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuhasDissa/MemerizeApp/17ffdf78ce0b8e85aef31d31f3d8c4bd5f72b7c0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-de-rDE/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Failed to load 4 | Meme Photo 5 | Today 6 | This Week 7 | This Month 8 | Play Video 9 | Settings 10 | README 11 | Check repository and README 12 | Latest Release 13 | Current Version 14 | Github Issue 15 | Submit bug reports and feature requests 16 | Home 17 | Subreddits 18 | Pause 19 | Play 20 | Lemmy Communities 21 | Change Image Cache Size 22 | OK 23 | No Memes Here 24 | Add new community 25 | Open Navigation Drawer 26 | Remove subreddit 27 | Add new RedditCommunity 28 | Save 29 | Cancel 30 | Instance Url 31 | Community Name 32 | Failed to fetch Community info 33 | Multi Reddit Feed 34 | Add new subreddit 35 | Remove Community 36 | Subreddit name from url 37 | Failed to fetch subreddit Info 38 | Filter by time 39 | Error Loading Online Memes 40 | Show Offline Memes 41 | Download Photo 42 | Share Photo 43 | Download Video 44 | Share Video 45 | Mute Sound 46 | Un-mute Sound 47 | Turn off repeat 48 | Turn on repeat 49 | About 50 | Settings 51 | Download Location 52 | Select Meme download location 53 | Image Cache Limit 54 | Set Image Cache Limit 55 | Developer Contact 56 | Hot 57 | Rising 58 | New 59 | Top 60 | Sort By 61 | Open post 62 | Show more options 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Memerize 3 | 読み込みに失敗しました 4 | ミーム写真 5 | 今日 6 | 今週 7 | 今月 8 | 動画を再生 9 | 設定 10 | README 11 | リポジトリとREADMEを確認してください 12 | 最新のリリース 13 | 現在のバージョン 14 | Github Issue 15 | バグレポートや機能リクエストを送信 16 | ホーム 17 | Subreddits 18 | 一時停止 19 | 再生 20 | レミーコミュニティ 21 | 画像キャッシュサイズの変更 22 | OK 23 | ここにはミームはありません 24 | 新しいコミュニティを追加する 25 | ナビゲーションドロワーを開く 26 | subredditを削除する 27 | 新しいRedditコミュニティを追加 28 | Save 29 | キャンセル 30 | インスタンスURL 31 | コミュニティ名 32 | コミュニティ情報の取得に失敗しました 33 | マルチ Reddit フィード 34 | 新しいsubredditを追加する 35 | コミュニティを削除する 36 | URLからサブレディット名を取得 37 | subreddit情報の取得に失敗しました 38 | 時間による絞り込み 39 | オンラインミームの読み込みエラー 40 | オフラインでミームを表示 41 | Reddit 42 | 写真をダウンロード 43 | 写真を共有 44 | 動画をダウンロード 45 | 動画を共有 46 | 消音 47 | 消音を解除 48 | リピートをオフにする 49 | リピートをオンにする 50 | 詳細 51 | 設定 52 | ダウンロード場所 53 | ミームのダウンロード場所を選択してください 54 | 画像キャッシュの制限 55 | 画像キャッシュの制限を設定する 56 | 開発者の連絡先 57 | Lemmy 58 | ホット 59 | 上昇 60 | 新しい 61 | トップ 62 | 並べ替え 63 | オープンポスト 64 | さらにオプションを表示 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFB700 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Memerize 3 | Failed to load 4 | Meme Photo 5 | Today 6 | This Week 7 | This Month 8 | Play Video 9 | Settings 10 | README 11 | Check repository and README 12 | Latest Release 13 | Current Version 14 | Github Issue 15 | Submit bug reports and feature requests 16 | Home 17 | Subreddits 18 | Pause 19 | Play 20 | Lemmy Communities 21 | Change Image Cache Size 22 | OK 23 | No Memes Here 24 | Add new community 25 | Open Navigation Drawer 26 | Remove subreddit 27 | Add new RedditCommunity 28 | Save 29 | Cancel 30 | Instance Url 31 | Community Name 32 | Failed to fetch Community info 33 | Multi Reddit Feed 34 | Add new subreddit 35 | Remove Community 36 | Subreddit name from url 37 | Failed to fetch subreddit Info 38 | Filter by time 39 | Error Loading Online Memes 40 | Show Offline Memes 41 | Reddit 42 | Download Photo 43 | Share Photo 44 | Download Video 45 | Share Video 46 | Mute Sound 47 | Un-mute Sound 48 | Turn off repeat 49 | Turn on repeat 50 | About 51 | Settings 52 | Download Location 53 | Select Meme download location 54 | Image Cache Limit 55 | Set Image Cache Limit 56 | Developer Contact 57 | Lemmy 58 | Hot 59 | Rising 60 | New 61 | Top 62 | Sort By 63 | Open post 64 | Show more options 65 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |