├── .gitattributes
├── README.md
├── app
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ ├── themes.xml
│ │ │ │ └── colors.xml
│ │ │ ├── font
│ │ │ │ └── rubik_bold.ttf
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_foreground.xml
│ │ ├── ic_launcher-playstore.png
│ │ ├── java
│ │ │ └── app
│ │ │ │ └── ice
│ │ │ │ └── readmanga
│ │ │ │ ├── ui
│ │ │ │ ├── viewmodels
│ │ │ │ │ ├── HomeViewModel.kt
│ │ │ │ │ └── SettingsViewModel.kt
│ │ │ │ ├── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Types.kt
│ │ │ │ │ ├── theme_colors
│ │ │ │ │ │ └── LimeColors.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ ├── Lime.kt
│ │ │ │ │ └── Theme.kt
│ │ │ │ ├── navigator
│ │ │ │ │ ├── Routes.kt
│ │ │ │ │ └── Navigator.kt
│ │ │ │ ├── hilt
│ │ │ │ │ └── prefModule.kt
│ │ │ │ ├── pages
│ │ │ │ │ ├── Library.kt
│ │ │ │ │ ├── Updates.kt
│ │ │ │ │ ├── info
│ │ │ │ │ │ ├── InfoSharedViewModel.kt
│ │ │ │ │ │ ├── InfoSection.kt
│ │ │ │ │ │ ├── Info.kt
│ │ │ │ │ │ └── ReadSection.kt
│ │ │ │ │ ├── MainScreen.kt
│ │ │ │ │ ├── Settings.kt
│ │ │ │ │ ├── Search.kt
│ │ │ │ │ ├── Home.kt
│ │ │ │ │ └── read
│ │ │ │ │ │ └── Read.kt
│ │ │ │ └── models
│ │ │ │ │ ├── BottomNavigationBar.kt
│ │ │ │ │ └── Cards.kt
│ │ │ │ ├── MyApp.kt
│ │ │ │ ├── types
│ │ │ │ ├── SettingKeys.kt
│ │ │ │ ├── anilistResponses
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── Data.kt
│ │ │ │ │ │ ├── Page.kt
│ │ │ │ │ │ ├── Characters.kt
│ │ │ │ │ │ ├── Relation.kt
│ │ │ │ │ │ ├── Recommendation.kt
│ │ │ │ │ │ └── Media.kt
│ │ │ │ │ └── AnilistResponses.kt
│ │ │ │ ├── Enums.kt
│ │ │ │ ├── MangaProgress.kt
│ │ │ │ ├── Provider.kt
│ │ │ │ ├── Preferences.kt
│ │ │ │ ├── Database.kt
│ │ │ │ └── MangaDex.kt
│ │ │ │ ├── core
│ │ │ │ ├── source_handler
│ │ │ │ │ ├── Sources.kt
│ │ │ │ │ └── SourceHandler.kt
│ │ │ │ ├── providers
│ │ │ │ │ ├── Provider.kt
│ │ │ │ │ ├── MangaDex.kt
│ │ │ │ │ └── MangaReader.kt
│ │ │ │ ├── local
│ │ │ │ │ ├── SettingPreferences.kt
│ │ │ │ │ └── MangaProgress.kt
│ │ │ │ ├── downloader
│ │ │ │ │ ├── Downloader.kt
│ │ │ │ │ └── DownloadWorker.kt
│ │ │ │ └── database
│ │ │ │ │ └── anilist
│ │ │ │ │ ├── AnilistQueries.kt
│ │ │ │ │ └── Anilist.kt
│ │ │ │ ├── utils
│ │ │ │ ├── Misc.kt
│ │ │ │ └── Network.kt
│ │ │ │ └── MainActivity.kt
│ │ ├── proto
│ │ │ └── userPreferences.proto
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── app
│ │ │ └── ice
│ │ │ └── readmanga
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── app
│ │ └── ice
│ │ └── readmanga
│ │ └── ExampleInstrumentedTest.kt
├── .gitignore
├── proguard-rules.pro
└── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── settings.gradle
├── .gitignore
├── gradle.properties
├── gradlew.bat
└── gradlew
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ReadManga
2 | Just another app to read mangas for free!
3 |
4 | will update this readme later!
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ReadManga
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frostnova721/ReadManga/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/font/rubik_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frostnova721/ReadManga/HEAD/app/src/main/res/font/rubik_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frostnova721/ReadManga/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /debug
3 | /debug/output-metadata.json
4 | /alpha
5 | /alpha/output-metadata.json
6 | /google/*
7 | /fdroid/*
8 | /release
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frostnova721/ReadManga/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frostnova721/ReadManga/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frostnova721/ReadManga/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frostnova721/ReadManga/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frostnova721/ReadManga/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frostnova721/ReadManga/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frostnova721/ReadManga/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frostnova721/ReadManga/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frostnova721/ReadManga/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frostnova721/ReadManga/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #927090
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/viewmodels/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.viewmodels
2 |
3 | import androidx.lifecycle.ViewModel
4 |
5 | class HomeViewModel: ViewModel() {
6 |
7 |
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/MyApp.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class MyApp: Application()
8 |
--------------------------------------------------------------------------------
/app/src/main/proto/userPreferences.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 | option java_package = "app.ice.readmanga";
3 | option java_multiple_files = true;
4 |
5 | message UserPreferences {
6 | bool materialTheme = 1;
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/types/SettingKeys.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.types
2 |
3 | import androidx.datastore.preferences.core.stringPreferencesKey
4 |
5 | object SettingKeys {
6 | public val MaterialTheme = stringPreferencesKey("material_theme")
7 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 28 00:01:19 IST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/core/source_handler/Sources.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.core.source_handler
2 |
3 | object MangaSources {
4 | const val MANGA_READER = "manga_reader"
5 | const val MANGADEX = "mangadex"
6 |
7 | val sourcesAsList = listOf(MANGADEX, MANGA_READER)
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/types/anilistResponses/components/Data.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.types.anilistResponses.components
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class AnilistData(
8 | @SerialName("Page")
9 | val page: AnilistPage?
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/types/anilistResponses/components/Page.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.types.anilistResponses.components
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class AnilistPage(
8 | @SerialName("media")
9 | val media: List?
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "ReadManga"
16 | include ':app'
17 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/types/anilistResponses/AnilistResponses.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.types.anilistResponses
2 |
3 | import app.ice.readmanga.types.anilistResponses.components.AnilistData
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class AnilistResponse(
9 | @SerialName("data")
10 | val data: AnilistData?
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/types/Enums.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.types
2 |
3 | enum class Countries(val countryCode: String) {
4 | JAPAN("JP"),
5 | CHINA("CN"),
6 | KOREA("KR"),
7 | ANY("ANY")
8 | }
9 |
10 | enum class MangaTypes(val country: Countries) {
11 | MANGA(Countries.JAPAN),
12 | MANHUA(Countries.CHINA),
13 | MANHWA(Countries.KOREA),
14 | NOVEL(Countries.ANY),
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/types/MangaProgress.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.types
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | @Parcelize
9 | data class MangaProgressList(
10 | val id: Int,
11 | val title: String,
12 | var read: Float?,
13 | val total: Float?,
14 | val cover: String,
15 | ) : Parcelable
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/theme/Types.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | data class ThemeItem(
6 | val primary: Color,
7 | val background: Color,
8 | val secondary: Color,
9 | val surface: Color,
10 | val primaryContainer: Color,
11 | val onPrimaryContainer: Color,
12 | val onSurface: Color,
13 | val onBackground: Color,
14 | val onPrimary: Color,
15 | )
--------------------------------------------------------------------------------
/app/src/test/java/app/ice/readmanga/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/navigator/Routes.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.navigator
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | object Routes {
6 | @Serializable
7 | object MainScreenRoute
8 |
9 | @Serializable
10 | data class InfoRoute(val id: Int)
11 |
12 | @Serializable
13 | data class ReadRoute(val chapterLink: String, val chapterNumber: String, val id: Int)
14 |
15 | @Serializable
16 | object SettingsRoute
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/types/Provider.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.types
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | data class SearchResult(
7 | val id: String,
8 | val title: String,
9 | val cover: String,
10 | )
11 |
12 | @Parcelize
13 | data class Chapters(
14 | val chapter: String,
15 | val link: String,
16 | ): Parcelable
17 |
18 | data class ChaptersResult(
19 | val lang: String,
20 | val chapters: List
21 | )
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/core/providers/Provider.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.core.providers
2 |
3 | import android.content.Context
4 | import app.ice.readmanga.types.ChaptersResult
5 | import app.ice.readmanga.types.SearchResult
6 |
7 | abstract class Provider() {
8 | abstract suspend fun search(query: String): List
9 |
10 | abstract suspend fun getChapters(id: String): List
11 |
12 | abstract suspend fun getPages(chapterLink: String, quality:String = "medium"): List?
13 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Gradle files
2 | .gradle/
3 | build/
4 | .navigation
5 |
6 | # Local configuration file (sdk path, etc)
7 | local.properties
8 |
9 | # Log/OS Files
10 | *.log
11 |
12 | # Secrets
13 | key.properties
14 |
15 | # Java class files
16 | *.class
17 |
18 | # Android Studio generated files and folders
19 | captures/
20 | .externalNativeBuild/
21 | .cxx/
22 | *.apk
23 | output.json
24 | *.ap_
25 | *.aab
26 |
27 | # IntelliJ
28 | *.iml
29 | .idea/
30 |
31 | # Keystore files
32 | *.jks
33 | *.keystore
34 |
35 | # Android Profiling
36 | *.hprof
37 |
38 | #other
39 | scripts/
40 |
41 | #crowdin
42 | crowdin.yml
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/hilt/prefModule.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.hilt
2 |
3 | import android.content.Context
4 | import app.ice.readmanga.core.local.SettingPreferences
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import dagger.hilt.components.SingletonComponent
10 | import javax.inject.Singleton
11 |
12 | @Module
13 | @InstallIn(SingletonComponent::class)
14 | object PreferencesModule {
15 |
16 | @Provides
17 | @Singleton
18 | fun provideSettingPreferences(@ApplicationContext context: Context): SettingPreferences {
19 | return SettingPreferences(context)
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/app/ice/readmanga/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("app.ice.readmanga", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/pages/Library.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.pages
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.safeDrawingPadding
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.navigation.NavHostController
12 |
13 | @Composable
14 | fun Library(rootController: NavHostController, barController: NavHostController) {
15 | Box(Modifier.safeDrawingPadding()) {
16 | Column(
17 | modifier = Modifier.fillMaxSize(),
18 | horizontalAlignment = Alignment.CenterHorizontally
19 | ) {
20 | Text(text = "To be implemented")
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/pages/Updates.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.pages
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.safeDrawingPadding
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.navigation.NavHostController
12 |
13 | @Composable
14 | fun Updates(rootController: NavHostController, barController: NavHostController) {
15 | Box(Modifier.safeDrawingPadding()) {
16 | Column(
17 | modifier = Modifier.fillMaxSize(),
18 | horizontalAlignment = Alignment.CenterHorizontally
19 | ) {
20 | Text(text = "To be implemented")
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/utils/Misc.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.utils
2 |
3 | import android.content.Context
4 | import android.widget.Toast
5 |
6 | data class MonthNumberToMonthNameResult(
7 | val full: String,
8 | val short: String,
9 | );
10 |
11 | fun showToast(context: Context, message: String) {
12 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
13 | }
14 |
15 | fun MonthNumberToName(monthNumber: Int): MonthNumberToMonthNameResult {
16 | val monthName = listOf(
17 | "January",
18 | "February",
19 | "March",
20 | "April",
21 | "May",
22 | "June",
23 | "July",
24 | "August",
25 | "September",
26 | "October",
27 | "November",
28 | "December",
29 | );
30 | return MonthNumberToMonthNameResult(full = monthName[monthNumber-1], short = monthName[monthNumber-1].substring(0,3))
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/theme/theme_colors/LimeColors.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.theme.theme_colors
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import app.ice.readmanga.ui.theme.ThemeItem
5 |
6 | val limeThemeColors = ThemeItem(
7 | primary = Color(0xFFCAF979),
8 | background = Color.Black,
9 | surface = Color.Black,
10 | secondary = Color(0xFFCAF979),
11 | primaryContainer = Color(0x8FCAF979),
12 | onPrimaryContainer = Color.Black,
13 | onSurface = Color.White,
14 | onBackground = Color.White,
15 | onPrimary = Color.Black
16 | )
17 |
18 | val limeLightColors = ThemeItem(
19 | primary = Color(0xFF568700),
20 | background = Color.Black,
21 | surface = Color.Black,
22 | secondary = Color(0xFFCAF979),
23 | primaryContainer = Color(0x8FCAF979),
24 | onPrimaryContainer = Color.Black,
25 | onSurface = Color.White,
26 | onBackground = Color.White,
27 | onPrimary = Color.Black
28 | )
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | -keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
24 | ;
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/core/local/SettingPreferences.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.core.local
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import androidx.datastore.dataStore
6 | import androidx.datastore.preferences.core.stringPreferencesKey
7 | import app.ice.readmanga.UserPreferences
8 | import app.ice.readmanga.types.PreferencesSerializer
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.serialization.json.Json
11 |
12 | val Context.settingPreferenceDataStore by dataStore("user_perfs.pb", PreferencesSerializer)
13 |
14 | class SettingPreferences (private val context: Context) {
15 | private val json = Json { ignoreUnknownKeys = true }
16 |
17 | private val key = stringPreferencesKey("pref")
18 |
19 | fun getPrefences(): Flow {
20 | return context.settingPreferenceDataStore.data
21 | }
22 |
23 | suspend fun savePreferences(preferences: UserPreferences) {
24 | Log.i("SAVE", "Saving preference...")
25 | context.settingPreferenceDataStore.updateData {
26 | preferences
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/types/anilistResponses/components/Characters.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.types.anilistResponses.components
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 | import kotlinx.serialization.SerialName
6 | import kotlinx.serialization.Serializable
7 |
8 | @Serializable
9 | data class Characters(
10 | @SerialName("edges")
11 | val edges: List?
12 | )
13 |
14 | @Serializable
15 | data class CharacterEdges(
16 | @SerialName("node")
17 | val node: CharacterNode?,
18 |
19 | @SerialName("role")
20 | val role: String?
21 | )
22 |
23 | @Serializable
24 | data class CharacterNode(
25 | @SerialName("name")
26 | val name: CharacterName?,
27 |
28 | @SerialName("image")
29 | val image: CharacterImage?
30 |
31 | )
32 |
33 | @Parcelize
34 | @Serializable
35 | data class CharacterName(
36 | @SerialName("full")
37 | val full: String?,
38 |
39 | @SerialName("native")
40 | val native: String?,
41 | ) : Parcelable
42 |
43 | @Serializable
44 | data class CharacterImage(
45 | @SerialName("large")
46 | val large: String?
47 | )
48 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/types/anilistResponses/components/Relation.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.types.anilistResponses.components
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class Relations(
8 | @SerialName("edges")
9 | val edges: List?
10 | )
11 |
12 | @Serializable
13 | data class RelationEdge(
14 | @SerialName("relationType")
15 | val relationType: String?,
16 |
17 | @SerialName("node")
18 | val node: RelationNode?
19 | )
20 |
21 | @Serializable
22 | data class RelationNode(
23 | @SerialName("id")
24 | val id: Int?,
25 |
26 | @SerialName("type")
27 | val type: String?,
28 |
29 | @SerialName("title")
30 | val title: RelationTitle?,
31 |
32 | @SerialName("averageScore")
33 | val averageScore: Int?,
34 |
35 | @SerialName("coverImage")
36 | val coverImage: RelationImage?
37 | )
38 |
39 | @Serializable
40 | data class RelationTitle(
41 | @SerialName("romaji")
42 | val romaji: String?,
43 |
44 | @SerialName("english")
45 | val english: String?
46 | )
47 |
48 | @Serializable
49 | data class RelationImage(
50 | @SerialName("large")
51 | val large: String?
52 | )
53 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/types/anilistResponses/components/Recommendation.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.types.anilistResponses.components
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class Recommendations(
8 | @SerialName("nodes")
9 | val nodes: List?
10 | )
11 |
12 | @Serializable
13 | data class RecommendationNode(
14 | @SerialName("mediaRecommendation")
15 | val mediaRecommendation: MediaRecommendation?
16 | )
17 |
18 | @Serializable
19 | data class MediaRecommendation(
20 | @SerialName("id")
21 | val id: Int?,
22 |
23 | @SerialName("type")
24 | val type: String?,
25 |
26 | @SerialName("averageScore")
27 | val averageScore: Int?,
28 |
29 | @SerialName("title")
30 | val title: RecommendationTitle?,
31 |
32 | @SerialName("coverImage")
33 | val coverImage: RecommendationImage?
34 | )
35 |
36 | @Serializable
37 | data class RecommendationTitle(
38 | @SerialName("romaji")
39 | val romaji: String?,
40 |
41 | @SerialName("english")
42 | val english: String?
43 | )
44 |
45 | @Serializable
46 | data class RecommendationImage(
47 | @SerialName("large")
48 | val large: String?
49 | )
50 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.Font
6 | import androidx.compose.ui.text.font.FontFamily
7 | import androidx.compose.ui.text.font.FontWeight
8 | import androidx.compose.ui.unit.sp
9 | import app.ice.readmanga.R
10 |
11 | val Rubik = FontFamily(Font(R.font.rubik_bold))
12 |
13 | // Set of Material typography styles to start with
14 | val Typography = Typography(
15 | bodyLarge = TextStyle(
16 | fontFamily = FontFamily.Default,
17 | fontWeight = FontWeight.Normal,
18 | fontSize = 16.sp,
19 | lineHeight = 24.sp,
20 | letterSpacing = 0.5.sp
21 | )
22 | /* Other default text styles to override
23 | titleLarge = TextStyle(
24 | fontFamily = FontFamily.Default,
25 | fontWeight = FontWeight.Normal,
26 | fontSize = 22.sp,
27 | lineHeight = 28.sp,
28 | letterSpacing = 0.sp
29 | ),
30 | labelSmall = TextStyle(
31 | fontFamily = FontFamily.Default,
32 | fontWeight = FontWeight.Medium,
33 | fontSize = 11.sp,
34 | lineHeight = 16.sp,
35 | letterSpacing = 0.5.sp
36 | )
37 | */
38 | )
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/theme/Lime.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.theme
2 |
3 | import androidx.compose.material3.ColorScheme
4 | import androidx.compose.material3.darkColorScheme
5 | import androidx.compose.material3.lightColorScheme
6 | import app.ice.readmanga.ui.theme.theme_colors.limeLightColors
7 | import app.ice.readmanga.ui.theme.theme_colors.limeThemeColors
8 |
9 | fun limeTheme(
10 | isDark: Boolean
11 | ): ColorScheme {
12 | val colors = if (isDark) {
13 | darkColorScheme(
14 | primary = limeThemeColors.primary,
15 | background = limeThemeColors.background,
16 | secondary = limeThemeColors.secondary,
17 | surface = limeThemeColors.surface,
18 | primaryContainer = limeThemeColors.primaryContainer,
19 | onPrimaryContainer = limeThemeColors.onPrimaryContainer,
20 | onSurface = limeThemeColors.onSurface,
21 | onBackground = limeThemeColors.onBackground,
22 | onPrimary = limeThemeColors.onPrimary,
23 | )
24 | } else {
25 | lightColorScheme(
26 | primary = limeLightColors.primary,
27 | primaryContainer = limeThemeColors.primaryContainer,
28 | onPrimary = limeThemeColors.onPrimary,
29 | )
30 | }
31 |
32 | return colors
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/types/Preferences.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.types
2 |
3 | import android.util.Log
4 | import kotlinx.serialization.Serializable
5 | import androidx.datastore.core.Serializer
6 | import app.ice.readmanga.UserPreferences
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.withContext
9 | import kotlinx.serialization.decodeFromString
10 | import kotlinx.serialization.json.Json
11 | import java.io.InputStream
12 | import java.io.OutputStream
13 |
14 | //@Serializable
15 | //data class UserPreferences(
16 | // val materialTheme: Boolean = false,
17 | //);
18 |
19 | object PreferencesSerializer : Serializer {
20 | override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
21 | override suspend fun readFrom(input: InputStream): UserPreferences {
22 | return try {
23 | withContext(Dispatchers.IO) {
24 | UserPreferences.parseFrom(input)
25 | }
26 | } catch (err: Exception) {
27 | Log.e("PROTO-READ", "Couldnt read proto!")
28 | defaultValue
29 | }
30 | }
31 |
32 | override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
33 | return withContext(Dispatchers.IO) {
34 | t.writeTo(output)
35 | }
36 | }
37 |
38 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/core/source_handler/SourceHandler.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.core.source_handler
2 |
3 | import app.ice.readmanga.core.providers.MangaDex
4 | import app.ice.readmanga.core.providers.MangaReader
5 | import app.ice.readmanga.core.providers.Provider
6 | import app.ice.readmanga.types.ChaptersResult
7 | import app.ice.readmanga.types.SearchResult
8 |
9 | class SourceHandler(private val source: String) {
10 | private fun getSource(source: String): Provider {
11 | when(source) {
12 | "manga_reader" -> return MangaReader()
13 | "mangadex" -> return MangaDex()
14 | }
15 |
16 | throw Exception("UNKNOWN SOURCE")
17 | }
18 |
19 | suspend fun search(query: String): List {
20 | val src = getSource(source)
21 | val res = src.search(query)
22 | return res;
23 | }
24 |
25 | suspend fun getChapters(id: String): List {
26 | try {
27 | val src = getSource(source)
28 | val res = src.getChapters(id)
29 | return res
30 | } catch (err: Exception) {
31 | return emptyList();
32 | }
33 | }
34 |
35 | suspend fun getPages(chapterLink: String): List? {
36 | try {
37 | val src = getSource(source)
38 | val res = src.getPages(chapterLink)
39 | return res
40 | } catch (err: Exception) {
41 | println(err)
42 | return null;
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
12 |
13 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/utils/Network.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.utils
2 |
3 |
4 | import android.util.Log
5 | import app.ice.readmanga.types.anilistResponses.AnilistResponse
6 | import io.ktor.client.HttpClient
7 | import io.ktor.client.call.body
8 | import io.ktor.client.engine.cio.CIO
9 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
10 | import io.ktor.client.request.get
11 | import io.ktor.client.request.post
12 | import io.ktor.client.request.setBody
13 | import io.ktor.client.statement.HttpResponse
14 | import io.ktor.client.statement.bodyAsText
15 | import io.ktor.http.ContentType
16 | import io.ktor.http.ContentType.Application.Json
17 | import io.ktor.http.contentType
18 | import io.ktor.serialization.kotlinx.json.json
19 | import kotlinx.serialization.json.Json
20 | import org.json.JSONObject
21 |
22 | public val client = HttpClient(CIO) {
23 | install(ContentNegotiation) {
24 | json(Json {
25 | ignoreUnknownKeys = true
26 | })
27 | }
28 | }
29 |
30 | suspend fun get(url: String): HttpResponse {
31 | val res = client.get(url)
32 | return res
33 | }
34 |
35 | suspend fun gqlRequest(url: String, query: String): AnilistResponse? {
36 | try {
37 | val reqBody = JSONObject().put("query", query).toString()
38 |
39 | val res = client.post(url) {
40 | setBody(reqBody)
41 | contentType(ContentType.Application.Json)
42 | }
43 |
44 | return res.body();
45 | } catch (err: Exception) {
46 | Log.e("REQ_ERR", err.message ?: "Error while sending post req")
47 | return null;
48 | }
49 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/viewmodels/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.viewmodels
2 |
3 | import android.content.SharedPreferences
4 | import androidx.compose.runtime.MutableState
5 | import androidx.compose.runtime.collectAsState
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.lifecycle.Lifecycle
8 | import androidx.lifecycle.ViewModel
9 | import androidx.lifecycle.viewModelScope
10 | import app.ice.readmanga.UserPreferences
11 | import app.ice.readmanga.core.local.SettingPreferences
12 | import dagger.hilt.android.lifecycle.HiltViewModel
13 | import kotlinx.coroutines.flow.MutableStateFlow
14 | import kotlinx.coroutines.flow.SharingStarted
15 | import kotlinx.coroutines.flow.StateFlow
16 | import kotlinx.coroutines.flow.collect
17 | import kotlinx.coroutines.flow.first
18 | import kotlinx.coroutines.flow.launchIn
19 | import kotlinx.coroutines.flow.onEach
20 | import kotlinx.coroutines.flow.stateIn
21 | import kotlinx.coroutines.launch
22 | import javax.inject.Inject
23 |
24 | @HiltViewModel
25 | class SettingsViewModel @Inject constructor(private val repo: SettingPreferences) : ViewModel() {
26 |
27 | private val _settings = MutableStateFlow(UserPreferences.getDefaultInstance()) // ✅ Initialize properly
28 | val settings: StateFlow get() = _settings
29 |
30 | init {
31 | viewModelScope.launch {
32 | repo.getPrefences().collect { prefs ->
33 | _settings.value = prefs
34 | }
35 | }
36 | }
37 |
38 | suspend fun saveSettings(value: UserPreferences) {
39 | repo.savePreferences(value)
40 | _settings.value = value
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/pages/info/InfoSharedViewModel.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.pages.info
2 |
3 | import androidx.lifecycle.ViewModel
4 | import app.ice.readmanga.core.source_handler.MangaSources
5 | import app.ice.readmanga.types.Chapters
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.StateFlow
8 |
9 | class InfoSharedViewModel : ViewModel() {
10 | var title: String? = null
11 | var coverImage: String? = null
12 | var id: Int? = null
13 |
14 | private val _source = MutableStateFlow(MangaSources.MANGADEX)
15 | val source: StateFlow get() = _source
16 | fun changeSource(mangaSource: String) {
17 | _source.value = mangaSource
18 | }
19 |
20 | //title that was found in the source
21 | private val _titleFoundInSource = MutableStateFlow(null)
22 | val titleFoundInSource: StateFlow get() = _titleFoundInSource
23 |
24 | fun updateFoundTitle(newTitle: String) {
25 | _titleFoundInSource.value = newTitle
26 | }
27 |
28 | //progress
29 | private val _readChapters = MutableStateFlow(null)
30 | val readChapters: StateFlow get() = _readChapters
31 |
32 | fun updateReadChapters(chapter: Float) {
33 | _readChapters.value = chapter
34 | }
35 |
36 | //chapters
37 | private val _chapterList = MutableStateFlow?>(null)
38 | val chapterList: StateFlow?> get() = _chapterList
39 |
40 | fun addChapters(chapters: List?) {
41 | _chapterList.value = chapters
42 | }
43 |
44 | var selectedChapterLink: String? = null
45 | var selectedChapterNumber: String? = null
46 |
47 | fun clearViewModel() {
48 | _titleFoundInSource.value = null
49 | _chapterList.value = null
50 | selectedChapterNumber = null
51 | selectedChapterLink = null
52 | }
53 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/types/Database.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.types
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import app.ice.readmanga.types.anilistResponses.components.AnilistFuzzyDateFormat
6 | import app.ice.readmanga.types.anilistResponses.components.CharacterName
7 | import app.ice.readmanga.types.anilistResponses.components.Characters
8 | import kotlinx.parcelize.Parcelize
9 |
10 | @Parcelize
11 | data class MangaTitle(
12 | val english: String?,
13 | val romaji: String?
14 | ): Parcelable
15 |
16 | @Parcelize
17 | data class CharactersSimplified(
18 | val name: CharacterName,
19 | val role: String,
20 | val image: String?,
21 | ) : Parcelable
22 |
23 | @Parcelize
24 | data class RecommendationSimplified(
25 | val id: Int,
26 | val title: MangaTitle,
27 | val type: String,
28 | val rating: Int?,
29 | val cover: String,
30 | ): Parcelable
31 |
32 | @Parcelize
33 | data class AnilistSearchResult(
34 | val id: Int,
35 | val title: MangaTitle,
36 | val cover: String,
37 | val rating: Int?,
38 | ): Parcelable
39 |
40 | @Parcelize
41 | data class AnilistInfoResult(
42 | val id: Int,
43 | val title: MangaTitle,
44 | val cover: String,
45 | val banner: String?,
46 | val rating: Int?,
47 | val synonyms: List?,
48 | val genres: List?,
49 | val description: String?,
50 | val source: String?,
51 | val type: String,
52 | val chapters: Int?,
53 | val status: String?,
54 | val tags: List?,
55 | val startDate: AnilistFuzzyDateFormat?,
56 | val endDate: AnilistFuzzyDateFormat?,
57 | val characters: List?,
58 | val recommenations: List?,
59 | val relations: List?,
60 | ) : Parcelable
61 |
62 | @Parcelize
63 | data class AnilistTrendingResult(
64 | val id: Int,
65 | val title: MangaTitle,
66 | val genres: List?,
67 | val rating: Int?,
68 | val banner: String?,
69 | val cover: String,
70 | val chapters: Int?,
71 | ): Parcelable
72 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/types/anilistResponses/components/Media.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.types.anilistResponses.components
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 | import kotlinx.serialization.SerialName
6 | import kotlinx.serialization.Serializable
7 |
8 | @Serializable
9 | data class AnilistMedia(
10 | //only exception of this being null is for info query
11 | @SerialName("id")
12 | val id: Int? = null,
13 |
14 | @SerialName("title")
15 | val title: AnilistMediaTitle? = null,
16 |
17 | @SerialName("coverImage")
18 | val cover: AnilistMediaCoverImage,
19 |
20 | @SerialName("averageScore")
21 | val averageScore: Int? = null,
22 |
23 | @SerialName("bannerImage")
24 | val banner: String? = null,
25 |
26 | @SerialName("synonyms")
27 | val synonyms: List? = null,
28 |
29 | @SerialName("genres")
30 | val genres: List? = null,
31 |
32 | @SerialName("description")
33 | val description: String? = null,
34 |
35 | @SerialName("source")
36 | val source: String? = null,
37 |
38 | @SerialName("type")
39 | val type: String? = null,
40 |
41 | @SerialName("chapters")
42 | val chapters: Int? = null,
43 |
44 | @SerialName("tags")
45 | val tags: List? = null,
46 |
47 | @SerialName("startDate")
48 | val startDate: AnilistFuzzyDateFormat? = null,
49 |
50 | @SerialName("endDate")
51 | val endDate: AnilistFuzzyDateFormat? = null,
52 |
53 | @SerialName("status")
54 | val status: String? = null,
55 |
56 | @SerialName("characters")
57 | val characters: Characters? = null,
58 |
59 | @SerialName("recommendations")
60 | val recommendations: Recommendations? = null,
61 |
62 | @SerialName("relations")
63 | val relations: Relations? = null,
64 | )
65 |
66 | @Parcelize
67 | @Serializable
68 | data class AnilistFuzzyDateFormat(
69 | @SerialName("year")
70 | val year: Int?,
71 |
72 | @SerialName("month")
73 | val month: Int?,
74 |
75 | @SerialName("day")
76 | val day: Int?
77 | ) : Parcelable
78 |
79 | @Serializable
80 | data class AnilistTags(
81 | @SerialName("name")
82 | val name: String,
83 | )
84 |
85 | @Serializable
86 | data class AnilistMediaCoverImage(
87 | @SerialName("large")
88 | val large: String,
89 | )
90 |
91 | @Serializable
92 | data class AnilistMediaTitle(
93 | @SerialName("english")
94 | val english: String?,
95 |
96 | @SerialName("romaji")
97 | val romaji: String?
98 | )
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/pages/MainScreen.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.pages
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.annotation.StringRes
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.material3.ExperimentalMaterial3Api
9 | import androidx.compose.material3.Scaffold
10 | import androidx.compose.material3.Text
11 | import androidx.compose.material3.TopAppBar
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.unit.sp
16 | import androidx.navigation.NavController
17 | import androidx.navigation.NavHostController
18 | import androidx.navigation.compose.currentBackStackEntryAsState
19 | import androidx.navigation.compose.rememberNavController
20 | import app.ice.readmanga.R
21 | import app.ice.readmanga.ui.models.BottomNavigationBar
22 | import app.ice.readmanga.ui.navigator.MainScreenBottomBarGraph
23 | import app.ice.readmanga.ui.theme.Rubik
24 |
25 |
26 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
27 | @OptIn(ExperimentalMaterial3Api::class)
28 | @Composable
29 | fun MainScreen(rootNavController: NavHostController) {
30 |
31 | val navController = rememberNavController();
32 |
33 | val currentRoute = getCurrentRoute(navController = navController)
34 |
35 | val titles = mapOf(
36 | "home" to "ReadManga",
37 | "search" to "Search",
38 | "updates" to "Updates",
39 | "library" to "Library"
40 | )
41 |
42 | Scaffold(
43 | topBar = {
44 | if(titles[currentRoute] != "ReadManga")
45 | TopAppBar(title = {
46 | Row {
47 | Text(text = titles[currentRoute] ?: "ReadManga", fontFamily = Rubik, fontSize = 26.sp )
48 | }
49 | })
50 | },
51 | bottomBar = {
52 | BottomNavigationBar(navController = navController)
53 | },
54 | content = { innerPadding ->
55 | Box(modifier = if(titles[currentRoute] != "ReadManga")Modifier.padding(innerPadding) else Modifier.padding(bottom = innerPadding.calculateBottomPadding())) {
56 | MainScreenBottomBarGraph(rootNavController, navController = navController)
57 | }
58 |
59 | }
60 | )
61 | }
62 |
63 | @Composable
64 | fun getCurrentRoute(navController: NavHostController): String? {
65 | val navBackStackEntry by navController.currentBackStackEntryAsState()
66 | return navBackStackEntry?.destination?.route
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/core/local/MangaProgress.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.core.local
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import androidx.datastore.preferences.core.edit
6 | import androidx.datastore.preferences.core.stringPreferencesKey
7 | import androidx.datastore.preferences.preferencesDataStore
8 | import app.ice.readmanga.types.MangaProgressList
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.first
11 | import kotlinx.coroutines.flow.map
12 | import kotlinx.coroutines.flow.toList
13 | import kotlinx.serialization.decodeFromString
14 | import kotlinx.serialization.encodeToString
15 | import kotlinx.serialization.json.Json
16 |
17 | val Context.mangaProgressDataStore by preferencesDataStore(name= "manga_progress")
18 |
19 | class MangaProgress {
20 | private val progressKey = stringPreferencesKey("progressList")
21 | private val json = Json { ignoreUnknownKeys = true }
22 |
23 | fun getProgress(context: Context): Flow> {
24 | return context.mangaProgressDataStore.data.map { perf ->
25 | val string = perf[progressKey] ?: ""
26 | if(string.isNotEmpty() && string != "[]") {
27 | json.decodeFromString>(string)
28 | } else {
29 | emptyList()
30 | }
31 | }
32 | }
33 |
34 | private suspend fun saveProgress(context: Context, mangaList: List) {
35 | Log.i("SAVE", "Saving progress...")
36 | val jsonString = json.encodeToString(mangaList)
37 | context.mangaProgressDataStore.edit{ perf ->
38 | perf[progressKey] = jsonString
39 | }
40 | }
41 |
42 | suspend fun updateProgressWithId(context: Context, id: Int, progress: Float) {
43 | val currentList = getProgress(context).first()
44 | val toBeUpdatedItem = currentList.first { it.id == id }
45 | val updatedItem = toBeUpdatedItem.copy(read = progress)
46 | val filteredList = if(currentList.size > 40) currentList.filterNot { it.id == id }.subList(0,40) else currentList.filterNot { it.id == id }
47 | val updatedList = filteredList + updatedItem
48 | saveProgress(context, updatedList.reversed())
49 | }
50 |
51 | suspend fun updateProgress(context: Context, manga: MangaProgressList) {
52 | val currentList = getProgress(context).first()
53 | //take the list and trim it to 40
54 | val filtered = if(currentList.size > 40) currentList.filterNot { it.id == manga.id }.subList(0, 40) else currentList.filterNot { it.id == manga.id }
55 | val updated = filtered + manga
56 | saveProgress(context, updated.reversed())
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga
2 |
3 | import android.content.pm.PackageManager
4 | import android.os.Build
5 | import android.os.Bundle
6 | import android.widget.Toast
7 | import androidx.activity.ComponentActivity
8 | import androidx.activity.compose.setContent
9 | import androidx.activity.enableEdgeToEdge
10 | import androidx.activity.result.ActivityResultLauncher
11 | import androidx.activity.result.contract.ActivityResultContracts
12 | import androidx.activity.viewModels
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.Surface
16 | import androidx.compose.ui.Modifier
17 | import androidx.core.content.ContextCompat
18 | import androidx.navigation.compose.rememberNavController
19 | import app.ice.readmanga.ui.navigator.ReadMangaNavGraph
20 | import app.ice.readmanga.ui.theme.ReadMangaTheme
21 | import app.ice.readmanga.ui.viewmodels.SettingsViewModel
22 | import dagger.hilt.android.AndroidEntryPoint
23 |
24 | @AndroidEntryPoint
25 | class MainActivity : ComponentActivity() {
26 |
27 | private val requestPermissionLauncher =
28 | registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
29 | if (isGranted) {
30 | Toast.makeText(this, "Notification permission granted", Toast.LENGTH_SHORT).show()
31 | } else {
32 | Toast.makeText(this, "Notification permission denied", Toast.LENGTH_SHORT).show()
33 | }
34 | }
35 |
36 | override fun onCreate(savedInstanceState: Bundle?) {
37 | enableEdgeToEdge()
38 | requestNotificationPermission(requestPermissionLauncher)
39 | super.onCreate(savedInstanceState)
40 | setContent {
41 | ReadMangaTheme {
42 | // A surface container using the 'background' color from the theme
43 | val rootNavController = rememberNavController()
44 | Surface(
45 | modifier = Modifier.fillMaxSize(),
46 | color = MaterialTheme.colorScheme.background
47 | ) {
48 | ReadMangaNavGraph(navController = rootNavController)
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
55 | fun ComponentActivity.requestNotificationPermission(
56 | requestPermissionLauncher: ActivityResultLauncher
57 | ) {
58 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
59 | if (ContextCompat.checkSelfPermission(
60 | this, "android.permission.POST_NOTIFICATIONS"
61 | ) != PackageManager.PERMISSION_GRANTED
62 | ) {
63 | requestPermissionLauncher.launch("android.permission.POST_NOTIFICATIONS")
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.ColorScheme
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.darkColorScheme
9 | import androidx.compose.material3.dynamicDarkColorScheme
10 | import androidx.compose.material3.dynamicLightColorScheme
11 | import androidx.compose.material3.lightColorScheme
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.SideEffect
14 | import androidx.compose.runtime.collectAsState
15 | import androidx.compose.runtime.getValue
16 | import androidx.compose.runtime.rememberUpdatedState
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.graphics.colorspace.ColorSpaces
19 | import androidx.compose.ui.graphics.toArgb
20 | import androidx.compose.ui.platform.LocalContext
21 | import androidx.compose.ui.platform.LocalView
22 | import androidx.core.view.ViewCompat
23 | import androidx.core.view.WindowCompat
24 | import androidx.core.view.WindowInsetsCompat
25 | import androidx.core.view.updatePadding
26 | import androidx.hilt.navigation.compose.hiltViewModel
27 | import app.ice.readmanga.core.local.SettingPreferences
28 | import app.ice.readmanga.ui.viewmodels.SettingsViewModel
29 | import kotlinx.coroutines.flow.first
30 |
31 | private val DarkColorScheme = darkColorScheme(
32 | primary = Purple80,
33 | secondary = PurpleGrey80,
34 | tertiary = Pink80
35 | )
36 |
37 | private val LightColorScheme = lightColorScheme(
38 | primary = Purple40,
39 | secondary = PurpleGrey40,
40 | tertiary = Pink40
41 |
42 | /* Other default colors to override
43 | background = Color(0xFFFFFBFE),
44 | surface = Color(0xFFFFFBFE),
45 | onPrimary = Color.White,
46 | onSecondary = Color.White,
47 | onTertiary = Color.White,
48 | onBackground = Color(0xFF1C1B1F),
49 | onSurface = Color(0xFF1C1B1F),
50 | */
51 | )
52 |
53 | @Composable
54 | fun ReadMangaTheme(
55 | darkTheme: Boolean = isSystemInDarkTheme(),
56 | // Dynamic color is available on Android 12+
57 | dynamicColor: Boolean = true,
58 | content: @Composable () -> Unit
59 | ) {
60 | val colorScheme = when {
61 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
62 | val context = LocalContext.current
63 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
64 | }
65 |
66 | darkTheme -> DarkColorScheme
67 | else -> LightColorScheme
68 | }
69 | val context = LocalContext.current
70 | val settings = hiltViewModel().settings.collectAsState()
71 | val material by rememberUpdatedState(settings.value.materialTheme)
72 | var theme: ColorScheme = colorScheme
73 | if(!material) {
74 | theme = limeTheme(isSystemInDarkTheme())
75 | }
76 |
77 | MaterialTheme(
78 | colorScheme = theme,
79 | typography = Typography,
80 | content = content
81 | )
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/core/downloader/Downloader.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.core.downloader
2 |
3 | import android.content.Context
4 | import android.graphics.BitmapFactory
5 | import android.graphics.pdf.PdfDocument
6 | import android.os.Environment
7 | import android.util.Log
8 | import androidx.core.content.ContentProviderCompat.requireContext
9 | import androidx.work.Data
10 | import androidx.work.OneTimeWorkRequestBuilder
11 | import androidx.work.WorkManager
12 | import app.ice.readmanga.utils.client
13 | import io.ktor.client.request.get
14 | import io.ktor.client.statement.readBytes
15 | import io.ktor.http.HttpStatusCode
16 | import java.io.File
17 | import java.io.FileOutputStream
18 |
19 | class Downloader {
20 |
21 | private suspend fun getImage(url: String): ByteArray? {
22 | val res = client.get(url)
23 | if(res.status != HttpStatusCode.OK) {
24 | Log.e("E-DOWN", "Couldn't download the image!")
25 | throw Exception("CANT DOWNLOAD THIS IMAGE")
26 | // return null;
27 | };
28 | println("Download success")
29 | val imageData = res.readBytes()
30 |
31 | return imageData;
32 | }
33 |
34 | suspend fun startDownload(urls: List, fileName: String, context: Context) {
35 | val downloadData = Data.Builder()
36 | .putStringArray("urls", urls.toTypedArray())
37 | .putString("fileName", fileName)
38 | .build()
39 |
40 | val downloadWorkRequest = OneTimeWorkRequestBuilder()
41 | .setInputData(downloadData)
42 | .build()
43 |
44 | WorkManager.getInstance(context).enqueue(downloadWorkRequest)
45 | }
46 |
47 | suspend fun downloadAsPdf(urls: List, fileName: String, onProgress: (Int) -> Unit ) {
48 | // val imageArray: MutableList = mutableListOf()
49 | val pdfDocument = PdfDocument()
50 |
51 | //write to pdf
52 | for((index, url) in urls.withIndex()) {
53 | val image = this.getImage(url) ?: return
54 | // imageArray.add(image)
55 | val bmp = BitmapFactory.decodeByteArray(image, 0, image.size)
56 | val pageDetails = PdfDocument.PageInfo.Builder(bmp.width, bmp.height, index + 1).create()
57 | val page = pdfDocument.startPage(pageDetails)
58 | val canvas = page.canvas
59 | canvas.drawBitmap(bmp, 0f, 0f, null)
60 | pdfDocument.finishPage(page)
61 | val progress = (index + 1) * 100 / urls.size
62 | onProgress(progress)
63 | }
64 | val regex = "[^a-zA-Z0-9-\\s]".toRegex()
65 | writeToStorage(pdfDocument, fileName.replace(regex, ""))
66 |
67 | }
68 |
69 | private fun writeToStorage(pdf: PdfDocument, fileName: String) {
70 | val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()
71 | val filePath = "$dir/$fileName.pdf"
72 | val file = File(filePath)
73 |
74 | try {
75 | pdf.writeTo(FileOutputStream(file))
76 | pdf.close()
77 | Log.i("DOWN", "Download complete!")
78 | } catch (err: Exception) {
79 | err.printStackTrace()
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/pages/Settings.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.pages
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Scaffold
12 | import androidx.compose.material3.Switch
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.collectAsState
16 | import androidx.compose.runtime.rememberCoroutineScope
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.platform.LocalContext
20 | import androidx.compose.ui.text.font.FontWeight
21 | import androidx.compose.ui.unit.dp
22 | import androidx.navigation.NavHostController
23 | import app.ice.readmanga.UserPreferences
24 | import app.ice.readmanga.ui.viewmodels.SettingsViewModel
25 | import kotlinx.coroutines.CoroutineScope
26 | import kotlinx.coroutines.launch
27 |
28 | @Composable
29 | fun Settings(rootNavigator: NavHostController, viewModel: SettingsViewModel) {
30 | val context = LocalContext.current
31 | val cosco = rememberCoroutineScope()
32 | Scaffold { innerPad ->
33 | Column(Modifier.padding(top = innerPad.calculateTopPadding())) {
34 | Text(
35 | "Settings",
36 | fontSize = MaterialTheme.typography.headlineLarge.fontSize,
37 | fontWeight = FontWeight.Bold,
38 | modifier = Modifier.padding(16.dp, 32.dp)
39 | )
40 | Box(
41 | modifier = Modifier
42 | .clickable {
43 | cosco.launch {
44 | viewModel.saveSettings(
45 | viewModel.settings.value.toBuilder().setMaterialTheme(!viewModel.settings.value.materialTheme)
46 | .build()
47 | )
48 | }
49 | }
50 | .padding(horizontal = 16.dp, vertical = 10.dp)
51 | ) {
52 | Row(
53 | verticalAlignment = Alignment.CenterVertically,
54 | horizontalArrangement = Arrangement.SpaceBetween,
55 | modifier = Modifier.fillMaxWidth()
56 | ) {
57 | Text(
58 | "Material Theme",
59 | fontSize = MaterialTheme.typography.titleLarge.fontSize,
60 | fontWeight = FontWeight.Bold
61 | )
62 | Switch(
63 | viewModel.settings.collectAsState().value.materialTheme,
64 | onCheckedChange = { value ->
65 | cosco.launch {
66 | viewModel.saveSettings(
67 | viewModel.settings.value.toBuilder().setMaterialTheme(value)
68 | .build()
69 | )
70 | }
71 | })
72 | }
73 | }
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/core/providers/MangaDex.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.core.providers
2 |
3 | import app.ice.readmanga.types.Chapters
4 | import app.ice.readmanga.types.ChaptersResult
5 | import app.ice.readmanga.types.MangaDexPagesResponse
6 | import app.ice.readmanga.types.MangadexItem
7 | import app.ice.readmanga.types.MangadexResponse
8 | import app.ice.readmanga.types.SearchResult
9 | import app.ice.readmanga.utils.get
10 | import io.ktor.client.call.body
11 | import kotlinx.coroutines.Dispatchers
12 | import kotlinx.coroutines.withContext
13 | import java.net.URLEncoder
14 |
15 | class MangaDex : Provider() {
16 | override suspend fun search(query: String): List {
17 | val encodedQuery = withContext(Dispatchers.IO) {
18 | URLEncoder.encode(query, "Utf-8")
19 | }
20 | val res =
21 | get("https://api.mangadex.org/manga?title=$encodedQuery&limit=5&order[relevance]=desc&includes[]=cover_art").body()
22 | val data: List = res.data
23 |
24 | val searchResults = mutableListOf()
25 |
26 | data.forEach { it ->
27 | var cover: String? = null;
28 | it.relationships?.forEach { rel ->
29 | if (rel.type == "cover_art") {
30 | cover =
31 | "https://mangadex.org/covers/${it.id}/${rel.attributes?.fileName}"
32 | }
33 | }
34 | searchResults.add(
35 | SearchResult(
36 | id = it.id,
37 | cover = cover ?: "",
38 | title = (it.attributes?.title?.en ?: it.attributes?.altTitles?.get(0)?.get("en")
39 | ?: it.attributes?.altTitles?.get(0)?.get("ja")).toString() ?: ""
40 | )
41 | )
42 | }
43 |
44 | return searchResults;
45 | }
46 |
47 | private suspend fun getRawChapterList(
48 | id: String,
49 | offset: Int,
50 | total: Int?
51 | ): List {
52 | if (total != null && offset >= total) return emptyList()
53 | val url =
54 | "https://api.mangadex.org/manga/${id}/feed?limit=96&order[volume]=desc&order[chapter]=desc&offset=${offset}&translatedLanguage[]=en"
55 | val res = get(url).body()
56 | return res.data + getRawChapterList(id, offset + 96, res.total)
57 | }
58 |
59 | override suspend fun getChapters(id: String): List {
60 | // val url = "https://api.mangadex.org/manga/$id"
61 | // val res = get(url).body().data
62 | val rawChapterData = getRawChapterList(id, 0, null)
63 |
64 | val chapters = mutableListOf()
65 | println(id)
66 |
67 | for (ch in rawChapterData) {
68 | chapters.add(
69 | Chapters(
70 | chapter = ch.attributes?.chapter ?: "",
71 | link = ch.id
72 | )
73 | )
74 | }
75 | return listOf(ChaptersResult(lang = "en", chapters = chapters))
76 | }
77 |
78 | override suspend fun getPages(chapterLink: String, quality: String): List? {
79 | val url = "https://api.mangadex.org/at-home/server/${chapterLink}"
80 | val res = get(url).body()
81 | val links = mutableListOf()
82 | res.chapter.data.forEach {
83 | links.add("${res.baseUrl}/data/${res.chapter.hash}/$it")
84 | }
85 |
86 | return links
87 | }
88 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/core/providers/MangaReader.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.core.providers
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import android.widget.Toast
6 | import app.ice.readmanga.types.Chapters
7 | import app.ice.readmanga.types.ChaptersResult
8 | import app.ice.readmanga.types.SearchResult
9 | import app.ice.readmanga.utils.get
10 | import io.ktor.client.statement.bodyAsText
11 | import org.json.JSONObject
12 | import org.jsoup.Jsoup
13 | import org.jsoup.nodes.Document
14 |
15 | class MangaReader : Provider() {
16 | private val baseUrl = "https://mangareader.to";
17 |
18 | override suspend fun search(query: String): List {
19 | val searchUrl = "$baseUrl/search?keyword=$query";
20 |
21 | val html = get(searchUrl);
22 |
23 | val document: Document = Jsoup.parse(html.bodyAsText())
24 | val elements = document.select(".item.item-spc")
25 |
26 | val searchRes = mutableListOf()
27 |
28 | for (ele in elements) {
29 | val cover = ele.select("a.manga-poster img").attr("src")
30 | val id = ele.select("a.manga-poster").attr("href").replace("/", "")
31 | val title = ele.select("h3.manga-name a").text()
32 |
33 | searchRes.add(SearchResult(id, title, cover))
34 | }
35 | return searchRes;
36 | }
37 |
38 | override suspend fun getChapters(id: String): List {
39 | try {
40 | val url = "$baseUrl/$id"
41 | val res = get(url)
42 | val doc = Jsoup.parse(res.bodyAsText())
43 |
44 | val chapterResult = mutableListOf()
45 |
46 | val elements =
47 | doc.select(".chapters-list-ul").first()?.children() ?: throw Error("NO ELEMENT")
48 |
49 | for (e in elements) {
50 | val lang = e.attr("id").split("-").first()
51 | val langSpecificChapters = mutableListOf()
52 |
53 | val subElements = e.children()
54 |
55 | for (se in subElements) {
56 | val chapter = se.attr("data-number")
57 | val idPart = se.selectFirst("a")?.attr("href") ?: continue
58 | val link = (baseUrl + idPart)
59 | langSpecificChapters.add(Chapters(chapter, link))
60 | }
61 | if (langSpecificChapters.isEmpty()) continue;
62 | chapterResult.add(ChaptersResult(lang, langSpecificChapters))
63 | }
64 |
65 | return chapterResult;
66 | } catch (e: Exception) {
67 | return emptyList()
68 | }
69 | }
70 |
71 | override suspend fun getPages(chapterLink: String, quality: String): List? {
72 | val res = get(chapterLink)
73 | var doc = Jsoup.parse(res.bodyAsText())
74 |
75 | val readingId = doc.select("div#wrapper").attr("data-reading-id")
76 |
77 | if (readingId.isEmpty()) throw Exception("Couldn't find the reading ID");
78 |
79 | val ajaxLink =
80 | "https://mangareader.to/ajax/image/list/chap/$readingId?mode=vertical&quality=$quality&hozPageSize=1";
81 |
82 | val apiRes = get(ajaxLink)
83 | val jsonObject = JSONObject(apiRes.bodyAsText())
84 | doc = Jsoup.parse(jsonObject.getString("html"))
85 | val pages = mutableListOf()
86 | val elements = doc.select("div.iv-card")
87 | for (e in elements) {
88 | if (e.hasClass("iv-card") && !e.hasClass("shuffled")) {
89 | pages.add(e.attr("data-url").replace("&", "&"))
90 | } else {
91 | Log.w("READMANGA", "Shuffled image!")
92 | return null;
93 | }
94 | }
95 | return pages;
96 | }
97 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/ui/navigator/Navigator.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.ui.navigator
2 |
3 | import androidx.activity.compose.BackHandler
4 | import androidx.compose.runtime.Composable
5 | import androidx.hilt.navigation.compose.hiltViewModel
6 | import androidx.lifecycle.viewmodel.compose.viewModel
7 | import androidx.navigation.NavGraphBuilder
8 | import androidx.navigation.NavHostController
9 | import androidx.navigation.compose.NavHost
10 | import androidx.navigation.compose.composable
11 | import androidx.navigation.toRoute
12 | import app.ice.readmanga.ui.pages.Home
13 | import app.ice.readmanga.ui.pages.Library
14 | import app.ice.readmanga.ui.pages.MainScreen
15 | import app.ice.readmanga.ui.pages.Search
16 | import app.ice.readmanga.ui.pages.Settings
17 | import app.ice.readmanga.ui.pages.Updates
18 | import app.ice.readmanga.ui.pages.info.Info
19 | import app.ice.readmanga.ui.pages.info.InfoSharedViewModel
20 | import app.ice.readmanga.ui.pages.read.Read
21 | import app.ice.readmanga.ui.viewmodels.SettingsViewModel
22 |
23 |
24 | @Composable
25 | fun MainScreenBottomBarGraph(rootController: NavHostController, navController: NavHostController) {
26 | NavHost(navController = navController, startDestination = "home") {
27 | bottomBarGraph(rootController = rootController, barNavController = navController)
28 | }
29 |
30 | }
31 |
32 | @Composable
33 | fun ReadMangaNavGraph(navController: NavHostController) {
34 | val svm: InfoSharedViewModel = viewModel()
35 | val preferencesViewModel: SettingsViewModel = hiltViewModel()
36 | NavHost(
37 | navController = navController,
38 | startDestination = "main_screen",
39 | ) {
40 | composable(route = "main_screen") {
41 | MainScreen(navController)
42 | }
43 | composable { bse ->
44 | val id = bse.toRoute().id
45 | Info(id = id, rootNavigator = navController, infoSharedViewModel = svm)
46 | BackHandler {
47 | svm.clearViewModel()
48 | navController.navigateUp()
49 | }
50 | }
51 | composable { bse ->
52 | val args = bse.toRoute()
53 | Read(
54 | rootNavController = navController,
55 | chapterId = args.chapterLink,
56 | chapterNumber = args.chapterNumber,
57 | id = args.id,
58 | svm
59 | )
60 | }
61 |
62 | composable { bse ->
63 | Settings(navController, preferencesViewModel)
64 | }
65 | // composable(
66 | // "info/{id}",
67 | // ) { navBackStackEntry ->
68 | // val args = requireNotNull(navBackStackEntry.arguments)
69 | // val id = args.getString("id")?.toInt()
70 | // val svm : InfoSharedViewModel = viewModel()
71 | // Info(id = id!!, navController, svm)
72 | // }
73 | // composable("read/{chapterId}/{chapterNumber}") { navBackStackEntry ->
74 | // val args = requireNotNull(navBackStackEntry.arguments)
75 | // val chapterId = args.getString("chapterId")
76 | // val chapterNumber = args.getString("chapterNumber")!!
77 | // Read(rootNavController = navController, chapterId!!, chapterNumber)
78 | // }
79 | }
80 | }
81 |
82 | private fun NavGraphBuilder.bottomBarGraph(
83 | rootController: NavHostController, barNavController: NavHostController
84 | ) {
85 |
86 | composable(route = "home") {
87 | Home(rootController, barNavController)
88 | }
89 | composable(route = "search") {
90 | Search(rootController, barNavController)
91 | }
92 | composable(route = "updates") {
93 | Updates(rootController, barNavController)
94 | }
95 | composable(route = "library") {
96 | Library(rootController, barNavController)
97 | }
98 |
99 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/core/downloader/DownloadWorker.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.core.downloader
2 |
3 | import android.app.Notification
4 | import android.app.NotificationChannel
5 | import android.app.NotificationManager
6 | import android.app.PendingIntent
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.graphics.drawable.Icon
10 | import android.os.Build
11 | import androidx.core.app.NotificationCompat
12 | import androidx.core.content.ContextCompat
13 | import androidx.core.graphics.drawable.IconCompat
14 | import androidx.work.CoroutineWorker
15 | import androidx.work.ForegroundInfo
16 | import androidx.work.WorkerParameters
17 | import androidx.work.workDataOf
18 | import app.ice.readmanga.MainActivity
19 | import app.ice.readmanga.R
20 | import kotlinx.coroutines.Dispatchers
21 | import kotlinx.coroutines.withContext
22 | import kotlin.properties.Delegates
23 |
24 | class DownloadWorker(
25 | context: Context, workerParams: WorkerParameters
26 | ) : CoroutineWorker(context, workerParams) {
27 |
28 | private val notificationChannelId = "readmangadownloadchannel"
29 |
30 | private val notificationId = System.currentTimeMillis();
31 |
32 | override suspend fun doWork(): Result {
33 | return withContext(Dispatchers.IO) {
34 | try {
35 | val url = inputData.getStringArray("urls")?.toList()
36 | ?: return@withContext Result.failure()
37 | val fileName =
38 | inputData.getString("fileName") ?: return@withContext Result.failure()
39 |
40 | val notificationManager = ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
41 | val notificationBuilder = createNotificationBuilder()
42 |
43 | Downloader().downloadAsPdf(url, fileName) { progress ->
44 | setProgressAsync(workDataOf("progress" to progress))
45 | notificationBuilder.setProgress(100, progress, false)
46 | notificationManager?.notify(notificationId.toInt(), notificationBuilder.build())
47 | }
48 |
49 | notificationManager?.cancel(notificationId.toInt())
50 | Result.success()
51 | } catch (e: Exception) {
52 | Result.failure()
53 | }
54 | }
55 | }
56 |
57 | private fun createNotificationBuilder(): NotificationCompat.Builder {
58 | createChannel()
59 | val mainActivityIntent = Intent(applicationContext, MainActivity::class.java)
60 | var pending by Delegates.notNull()
61 | pending = PendingIntent.FLAG_IMMUTABLE
62 |
63 | val mainActivityPendingIntent =
64 | PendingIntent.getActivity(applicationContext, 0, mainActivityIntent, pending)
65 |
66 | return NotificationCompat.Builder(
67 | applicationContext, notificationChannelId
68 | )
69 | .setSmallIcon(R.drawable.ic_launcher_foreground)
70 | .setContentTitle("Downloading")
71 | .setSilent(true)
72 | .setOngoing(true)
73 | .setProgress(100, 0, true)
74 | .setContentIntent(mainActivityPendingIntent)
75 |
76 | }
77 |
78 | override suspend fun getForegroundInfo(): ForegroundInfo {
79 | return ForegroundInfo(0, createNotificationBuilder().build())
80 | }
81 |
82 | private fun createChannel() {
83 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
84 | val nchannel = NotificationChannel(
85 | this.notificationChannelId,
86 | "ReadManga Downloader",
87 | NotificationManager.IMPORTANCE_DEFAULT
88 | )
89 | val nmgr: NotificationManager? =
90 | ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
91 |
92 | nmgr?.createNotificationChannel(nchannel)
93 | }
94 | }
95 | }
--------------------------------------------------------------------------------
/app/src/main/java/app/ice/readmanga/types/MangaDex.kt:
--------------------------------------------------------------------------------
1 | package app.ice.readmanga.types
2 |
3 | import kotlinx.serialization.Contextual
4 | import kotlinx.serialization.KSerializer
5 | import kotlinx.serialization.SerialName
6 | import kotlinx.serialization.Serializable
7 | import kotlinx.serialization.SerializationException
8 | import kotlinx.serialization.descriptors.SerialDescriptor
9 | import kotlinx.serialization.descriptors.buildClassSerialDescriptor
10 | import kotlinx.serialization.descriptors.element
11 | import kotlinx.serialization.encoding.Decoder
12 | import kotlinx.serialization.encoding.Encoder
13 | import kotlinx.serialization.json.JsonDecoder
14 | import kotlinx.serialization.json.JsonObject
15 | import kotlinx.serialization.json.JsonPrimitive
16 | import kotlinx.serialization.json.jsonPrimitive
17 |
18 | @Serializable
19 | data class MangadexResponse(
20 | @SerialName("total")
21 | val total: Int? = null,
22 |
23 | @SerialName("offset")
24 | val offset: Int? = null,
25 |
26 | @SerialName("data")
27 | val data: List
28 | )
29 |
30 | @Serializable
31 | data class MangaDexPagesResponse(
32 | @SerialName("chapter")
33 | val chapter: MangaDexPagesChapters,
34 |
35 | @SerialName("baseUrl")
36 | val baseUrl: String,
37 | )
38 |
39 | @Serializable
40 | data class MangaDexPagesChapters(
41 | @SerialName("hash")
42 | val hash: String,
43 |
44 | @SerialName("data")
45 | val data: List,
46 |
47 | @SerialName("dataSaver")
48 | val dataSaver: List,
49 | )
50 |
51 | @Serializable
52 | data class MangadexItem(
53 | @SerialName("id")
54 | val id: String,
55 |
56 | @SerialName("attributes")
57 | val attributes: Attributes?,
58 |
59 | @SerialName("relationships")
60 | val relationships: List?
61 | )
62 |
63 | @Serializable
64 | data class Attributes(
65 | @Serializable(with = TitleSerializer::class)
66 | @SerialName("title")
67 | val title: Title? = null,
68 |
69 | @SerialName("altTitles")
70 | val altTitles: List