├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── ic_launcher-web.png
│ │ ├── ic_launcher-playstore.png
│ │ ├── res
│ │ │ ├── font
│ │ │ │ ├── circularstd_bold.ttf
│ │ │ │ ├── circularstd_book.ttf
│ │ │ │ ├── circularstd_black.ttf
│ │ │ │ └── circularstd_medium.ttf
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── values
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── styles.xml
│ │ │ │ └── strings.xml
│ │ │ ├── xml
│ │ │ │ └── searchable.xml
│ │ │ ├── drawable
│ │ │ │ ├── header_bg_gradient.xml
│ │ │ │ ├── ic_at_state_in.xml
│ │ │ │ ├── ic_at_state_out.xml
│ │ │ │ ├── ic_at_state_buffer_in.xml
│ │ │ │ ├── ic_at_state_buffer_out.xml
│ │ │ │ ├── ic_check.xml
│ │ │ │ ├── ic_home.xml
│ │ │ │ ├── ic_chevron_right.xml
│ │ │ │ ├── ic_at_play.xml
│ │ │ │ ├── ic_keyboard_arrow_down.xml
│ │ │ │ ├── ic_at_pause.xml
│ │ │ │ ├── ic_file_download.xml
│ │ │ │ ├── ic_dismiss.xml
│ │ │ │ ├── ic_at_forward.xml
│ │ │ │ ├── ic_at_logo.xml
│ │ │ │ ├── ic_play_circle_outline.xml
│ │ │ │ ├── player_gradient.xml
│ │ │ │ ├── ic_at_state.xml
│ │ │ │ ├── player_gradient_reverse.xml
│ │ │ │ ├── ic_at_seeker.xml
│ │ │ │ ├── ic_account_circle.xml
│ │ │ │ ├── ic_day_night.xml
│ │ │ │ ├── ic_search.xml
│ │ │ │ ├── ic_screen_rotation.xml
│ │ │ │ ├── ic_animated_spinner.xml
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ ├── ic_no_connection.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── anim
│ │ │ │ ├── left_in.xml
│ │ │ │ ├── right_in.xml
│ │ │ │ ├── left_out.xml
│ │ │ │ ├── right_out.xml
│ │ │ │ ├── scale_in.xml
│ │ │ │ └── scale_out.xml
│ │ │ ├── menu
│ │ │ │ ├── bottom_nav_menu.xml
│ │ │ │ ├── toolbar_menu.xml
│ │ │ │ └── home_menu.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── values-night
│ │ │ │ ├── colors.xml
│ │ │ │ └── styles.xml
│ │ │ ├── animator
│ │ │ │ ├── state_path_in.xml
│ │ │ │ ├── state_path_out.xml
│ │ │ │ ├── state_path_buffer_in.xml
│ │ │ │ └── state_path_buffer_out.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_auth.xml
│ │ │ │ ├── activity_anime_player.xml
│ │ │ │ ├── animelist_item.xml
│ │ │ │ ├── episode_item.xml
│ │ │ │ ├── fragment_search.xml
│ │ │ │ ├── anime_search_item.xml
│ │ │ │ ├── activity_main.xml
│ │ │ │ └── fragment_login.xml
│ │ │ ├── values-v23
│ │ │ │ └── styles.xml
│ │ │ ├── values-night-v23
│ │ │ │ └── styles.xml
│ │ │ ├── values-v27
│ │ │ │ └── styles.xml
│ │ │ ├── values-night-v27
│ │ │ │ └── styles.xml
│ │ │ ├── navigation
│ │ │ │ ├── nav_graph_auth.xml
│ │ │ │ └── nav_graph.xml
│ │ │ └── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ │ └── dev
│ │ │ │ └── smoketrees
│ │ │ │ └── twist
│ │ │ │ ├── model
│ │ │ │ └── twist
│ │ │ │ │ ├── LoginDetails.kt
│ │ │ │ │ ├── ApiResponse.kt
│ │ │ │ │ ├── NejireAnimeModel.kt
│ │ │ │ │ ├── PatchLibRequest.kt
│ │ │ │ │ ├── RegisterDetails.kt
│ │ │ │ │ ├── LoginResponse.kt
│ │ │ │ │ ├── PatchLibResponse.kt
│ │ │ │ │ ├── AnimeWithEpisodes.kt
│ │ │ │ │ ├── LibraryEpisode.kt
│ │ │ │ │ ├── Motd.kt
│ │ │ │ │ ├── AnimeDetailsEntity.kt
│ │ │ │ │ ├── Slug.kt
│ │ │ │ │ ├── Result.kt
│ │ │ │ │ ├── AnimeSource.kt
│ │ │ │ │ ├── Episode.kt
│ │ │ │ │ ├── TrendingAnimeItem.kt
│ │ │ │ │ ├── AnimeDetails.kt
│ │ │ │ │ └── AnimeItem.kt
│ │ │ │ ├── di
│ │ │ │ ├── module
│ │ │ │ │ ├── RepoModule.kt
│ │ │ │ │ ├── CacheModule.kt
│ │ │ │ │ ├── ViewModelModule.kt
│ │ │ │ │ ├── RoomModule.kt
│ │ │ │ │ └── ApiModule.kt
│ │ │ │ └── AppComponent.kt
│ │ │ │ ├── utils
│ │ │ │ ├── BindingAdapters.kt
│ │ │ │ ├── Constants.kt
│ │ │ │ ├── AutoClearedValue.kt
│ │ │ │ ├── PreferenceHelper.kt
│ │ │ │ ├── Messages.kt
│ │ │ │ ├── search
│ │ │ │ │ └── WinklerWeightedRatio.kt
│ │ │ │ ├── Extensions.kt
│ │ │ │ └── CryptoHelper.kt
│ │ │ │ ├── DownloadInfoReceiver.kt
│ │ │ │ ├── ui
│ │ │ │ ├── player
│ │ │ │ │ ├── EpisodesViewModel.kt
│ │ │ │ │ ├── PlayerViewModel.kt
│ │ │ │ │ └── EpisodesFragment.kt
│ │ │ │ ├── auth
│ │ │ │ │ ├── AuthActivity.kt
│ │ │ │ │ ├── LoginFragment.kt
│ │ │ │ │ └── AccountFragment.kt
│ │ │ │ ├── base
│ │ │ │ │ └── BaseFragment.kt
│ │ │ │ ├── home
│ │ │ │ │ ├── AnimeViewModel.kt
│ │ │ │ │ └── MainActivity.kt
│ │ │ │ └── search
│ │ │ │ │ └── SearchFragment.kt
│ │ │ │ ├── App.kt
│ │ │ │ ├── db
│ │ │ │ ├── TrendingAnimeDao.kt
│ │ │ │ ├── EpisodeListTypeConverter.kt
│ │ │ │ ├── AnimeDb.kt
│ │ │ │ ├── AnimeDetailsDao.kt
│ │ │ │ └── AnimeDao.kt
│ │ │ │ ├── api
│ │ │ │ ├── Helper.kt
│ │ │ │ ├── BaseApiClient.kt
│ │ │ │ └── anime
│ │ │ │ │ ├── AnimeWebService.kt
│ │ │ │ │ └── AnimeWebClient.kt
│ │ │ │ ├── pagination
│ │ │ │ ├── KitsuDataSourceFactory.kt
│ │ │ │ ├── FilteredKitsuDataSourceFactory.kt
│ │ │ │ ├── PagedAnimeDatasource.kt
│ │ │ │ └── FilteredPagedAnimeDatasource.kt
│ │ │ │ ├── repository
│ │ │ │ ├── BaseRepo.kt
│ │ │ │ └── AnimeRepo.kt
│ │ │ │ └── adapters
│ │ │ │ ├── SearchListAdapter.kt
│ │ │ │ ├── EpisodeListAdapter.kt
│ │ │ │ ├── PagedAnimeListAdapter.kt
│ │ │ │ └── AnimeListAdapter.kt
│ │ └── AndroidManifest.xml
│ └── debug
│ │ └── res
│ │ └── values
│ │ └── strings.xml
├── proguard-rules.pro
└── build.gradle.kts
├── settings.gradle
├── keystore.jks
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── PRIV_POLICY.md
├── gradle.properties
├── .github
└── workflows
│ └── android.yml
├── README.md
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /src/main/res/values/secrets.xml
3 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name='Twist.moe'
3 |
--------------------------------------------------------------------------------
/keystore.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/keystore.jks
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/font/circularstd_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/res/font/circularstd_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/circularstd_book.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/res/font/circularstd_book.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/circularstd_black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/res/font/circularstd_black.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/circularstd_medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/res/font/circularstd_medium.ttf
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/debug/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Anime Twist DEBUG
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnimeTwist/twist-mobile/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #000000
4 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/searchable.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/LoginDetails.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | import androidx.annotation.Keep
4 |
5 | @Keep
6 | data class LoginDetails(
7 | var username: String,
8 | var password: String
9 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/di/module/RepoModule.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.di.module
2 |
3 | import dev.smoketrees.twist.repository.AnimeRepo
4 | import org.koin.dsl.module
5 |
6 | val repoModule = module {
7 | factory { AnimeRepo(get(), get(), get(), get()) }
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/ApiResponse.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | import androidx.annotation.Keep
4 |
5 | @Keep
6 | data class ApiResponse(
7 | val status: String?,
8 | val message: String?,
9 | val token: String?
10 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/header_bg_gradient.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/di/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.di
2 |
3 | import dev.smoketrees.twist.di.module.*
4 |
5 | val appComponent = listOf(
6 | apiModule,
7 | cacheModule,
8 | repoModule,
9 | viewModelModule,
10 | roomModule
11 | )
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Jul 09 14:12:12 IST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/left_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/right_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/utils/BindingAdapters.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.utils
2 |
3 | import android.view.View
4 | import androidx.databinding.BindingAdapter
5 |
6 | @BindingAdapter("isGone")
7 | fun bindIsGone(view: View, isGone: Boolean) {
8 | if (isGone) view.hide() else view.show()
9 | }
--------------------------------------------------------------------------------
/app/src/main/res/anim/left_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/right_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom_nav_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/NejireAnimeModel.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | import androidx.annotation.Keep
4 | import androidx.room.Entity
5 |
6 | @Keep
7 | @Entity
8 | data class NejireExtension(
9 | val cover_image: String,
10 | val poster_image: String
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/PatchLibRequest.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | data class PatchLibRequest (
4 | val number: Long,
5 | val animeID: Long,
6 | val progress: Double,
7 | val watchedAt: String,
8 | val episodeID: Long,
9 | val completed: Boolean
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/RegisterDetails.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | import androidx.annotation.Keep
4 |
5 | @Keep
6 | data class RegisterDetails(
7 | var username: String,
8 | var email: String,
9 | var password: String,
10 | var passwordConfirm: String
11 | )
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/DownloadInfoReceiver.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 |
7 | class DownloadInfoReceiver : BroadcastReceiver() {
8 | override fun onReceive(context: Context, intent: Intent) {}
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/LoginResponse.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | import androidx.annotation.Keep
4 |
5 | @Keep
6 | data class LoginResponse(
7 | val donation_rank: Int,
8 | val id: Int,
9 | val rank: Int,
10 | val token: String,
11 | val username: String
12 | )
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_at_state_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
--------------------------------------------------------------------------------
/.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 | .idea/
16 | app/release
17 | secrets.properties
18 | app/debug
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_at_state_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_at_state_buffer_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_at_state_buffer_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/ui/player/EpisodesViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.ui.player
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dev.smoketrees.twist.repository.AnimeRepo
5 |
6 | class EpisodesViewModel(private val repo: AnimeRepo) : ViewModel() {
7 | fun getAnimeDetails(animeName: String, id: Int) = repo.getAnimeDetails(animeName, id)
8 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_check.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_home.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_chevron_right.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_at_play.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/utils/Constants.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.utils
2 |
3 | import dev.smoketrees.twist.BuildConfig
4 |
5 | object Constants {
6 | const val PREF = "${BuildConfig.APPLICATION_ID}.pref"
7 |
8 | const val WEB = "https://twist.moe/"
9 |
10 | object PreferenceKeys {
11 | const val IS_DAY = "is_day"
12 | const val JWT = "jwt"
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_keyboard_arrow_down.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/di/module/CacheModule.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.di.module
2 |
3 | import dev.smoketrees.twist.utils.Constants
4 | import dev.smoketrees.twist.utils.PreferenceHelper
5 | import org.koin.android.ext.koin.androidContext
6 | import org.koin.dsl.module
7 |
8 | val cacheModule = module {
9 | single { PreferenceHelper.customPrefs(androidContext(), Constants.PREF) }
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/PatchLibResponse.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | data class PatchLibResponse (
4 | val id: Long,
5 | val userID: Long,
6 | val animeID: Long,
7 | val episodeID: Long,
8 | val progress: Double,
9 | val completed: Boolean,
10 | val watchedAt: String,
11 | val createdAt: String,
12 | val updatedAt: String
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/AnimeWithEpisodes.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | import androidx.room.Embedded
4 | import androidx.room.Relation
5 |
6 | data class AnimeWithEpisodes(
7 | @Embedded val animeItem: AnimeItem,
8 | @Relation(
9 | parentColumn = "uid",
10 | entityColumn = "anime_id"
11 | )
12 | val watchedEpisodes: List
13 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_at_pause.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_file_download.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/ui/auth/AuthActivity.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.ui.auth
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import dev.smoketrees.twist.R
6 |
7 | class AuthActivity : AppCompatActivity() {
8 | override fun onCreate(savedInstanceState: Bundle?) {
9 | super.onCreate(savedInstanceState)
10 | setContentView(R.layout.activity_auth)
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_dismiss.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_at_forward.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_at_logo.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/toolbar_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | #e53232
6 | #e53232
7 | #e53232
8 |
9 | #FFF
10 | #FAFAFA
11 | #e53232
12 |
13 | #212121
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/scale_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/scale_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | #e53232
6 | #e53232
7 | #e53232
8 |
9 | #1c1f22
10 | #282a2d
11 | #e53232
12 |
13 | #FAFAFA
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/di/module/ViewModelModule.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.di.module
2 |
3 | import dev.smoketrees.twist.ui.home.AnimeViewModel
4 | import dev.smoketrees.twist.ui.player.EpisodesViewModel
5 | import dev.smoketrees.twist.ui.player.PlayerViewModel
6 | import org.koin.android.viewmodel.dsl.viewModel
7 | import org.koin.dsl.module
8 |
9 | val viewModelModule = module {
10 | viewModel { AnimeViewModel(get()) }
11 | viewModel { EpisodesViewModel(get()) }
12 | viewModel { PlayerViewModel(get()) }
13 | }
--------------------------------------------------------------------------------
/PRIV_POLICY.md:
--------------------------------------------------------------------------------
1 | # In short
2 |
3 | We don't log or share your personal information.
4 |
5 | We don't track you, we don't profile you. Period.
6 |
7 | # The longer version
8 |
9 | _We believe privacy is a fundamental human right._
10 |
11 | **We do not store any data about you whatsoever.**
12 |
13 | We physically can't. We have nowhere to store it. We don't even have a server or a database. What happens on your device, stays on your device. We welcome all audits. Anyone can inspect our source [here](https://github.com/AnimeTwist/twist-mobile).
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_play_circle_outline.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/player_gradient.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_at_state.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/player_gradient_reverse.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/animator/state_path_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/animator/state_path_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_at_seeker.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/animator/state_path_buffer_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/animator/state_path_buffer_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/App.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist
2 |
3 | import androidx.multidex.MultiDexApplication
4 | import dev.smoketrees.twist.di.appComponent
5 | import org.koin.android.ext.koin.androidContext
6 | import org.koin.android.ext.koin.androidLogger
7 | import org.koin.core.context.startKoin
8 |
9 | class App : MultiDexApplication() {
10 | override fun onCreate() {
11 | super.onCreate()
12 |
13 | startKoin {
14 | androidLogger()
15 | androidContext(this@App)
16 | modules(appComponent)
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_account_circle.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/LibraryEpisode.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | import androidx.annotation.Keep
4 | import androidx.room.ColumnInfo
5 | import androidx.room.Entity
6 | import androidx.room.PrimaryKey
7 |
8 | @Keep
9 | @Entity
10 | data class LibraryEpisode(
11 | val id: Int,
12 | val user_id: Int,
13 | @ColumnInfo(name = "anime_id") val anime_id: Int,
14 | @PrimaryKey val episode_id: Int,
15 | val progress: Double,
16 | val completed: Int,
17 | val watched_at: String,
18 | val created_at: String,
19 | val updated_at: String
20 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/db/TrendingAnimeDao.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.db
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.room.Dao
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 | import androidx.room.Query
8 | import dev.smoketrees.twist.model.twist.TrendingAnimeItem
9 |
10 | @Dao
11 | interface TrendingAnimeDao {
12 | @Query("SELECT * FROM trendinganimeitem")
13 | fun getTrendingAnime(): LiveData>
14 |
15 | @Insert(onConflict = OnConflictStrategy.REPLACE)
16 | suspend fun saveTrendingAnime(animeItems: List)
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_day_night.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_search.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/di/module/RoomModule.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.di.module
2 |
3 | import androidx.room.Room
4 | import dev.smoketrees.twist.db.AnimeDb
5 | import org.koin.android.ext.koin.androidApplication
6 | import org.koin.dsl.module
7 |
8 | val roomModule = module {
9 | single {
10 | Room.databaseBuilder(androidApplication(), AnimeDb::class.java, "anime-database")
11 | .fallbackToDestructiveMigration()
12 | .build()
13 | }
14 |
15 | single { get().animeDao() }
16 | single { get().episodeDao() }
17 | single { get().trendingAnimeDao() }
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/api/Helper.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.api
2 |
3 | import dev.smoketrees.twist.BuildConfig
4 | import okhttp3.OkHttpClient
5 | import okhttp3.logging.HttpLoggingInterceptor
6 | import java.util.concurrent.TimeUnit
7 |
8 | fun getOkHttpClient(): OkHttpClient {
9 |
10 | val httpClient = OkHttpClient.Builder()
11 |
12 | if (BuildConfig.DEBUG) {
13 | val httpLoggingInterceptor = HttpLoggingInterceptor()
14 | httpClient.addInterceptor(httpLoggingInterceptor.apply {
15 | httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BASIC
16 | })
17 | }
18 |
19 | return httpClient.readTimeout(60, TimeUnit.SECONDS)
20 | .connectTimeout(60, TimeUnit.SECONDS)
21 | .build()
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/ui/player/PlayerViewModel.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.ui.player
2 |
3 | import android.content.pm.ActivityInfo
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import dev.smoketrees.twist.model.twist.AnimeSource
7 | import dev.smoketrees.twist.repository.AnimeRepo
8 |
9 | class PlayerViewModel(private val repo: AnimeRepo) : ViewModel() {
10 | fun getAnimeSources(animeName: String) = repo.getAnimeSources(animeName)
11 |
12 | var playWhenReady = true
13 | var currentWindowIndex = 0
14 | var playbackPosition = 0L
15 | var currEp = MutableLiveData(null)
16 | var sources: List? = null
17 | var orientation: Int = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/pagination/KitsuDataSourceFactory.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.pagination
2 |
3 | import androidx.lifecycle.MutableLiveData
4 | import androidx.paging.DataSource
5 | import dev.smoketrees.twist.api.anime.AnimeWebClient
6 | import dev.smoketrees.twist.model.twist.AnimeItem
7 |
8 | class KitsuDataSourceFactory(
9 | private val webClient: AnimeWebClient,
10 | private val sort: String
11 | ) : DataSource.Factory() {
12 | val animeLiveDataSource = MutableLiveData()
13 |
14 | override fun create(): DataSource {
15 | val animeDataSource = PagedAnimeDatasource(webClient, sort)
16 | animeLiveDataSource.postValue(animeDataSource)
17 | return animeDataSource
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_screen_rotation.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/Motd.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | import androidx.annotation.Keep
4 | import com.google.gson.annotations.Expose
5 | import com.google.gson.annotations.SerializedName
6 | import org.apache.commons.lang3.builder.ToStringBuilder
7 | import java.io.Serializable
8 |
9 | @Keep
10 | class Motd : Serializable {
11 |
12 | @SerializedName("id")
13 | @Expose
14 | var id: Int? = null
15 |
16 | @SerializedName("title")
17 | @Expose
18 | var title: String? = null
19 |
20 | @SerializedName("message")
21 | @Expose
22 | var message: String? = null
23 |
24 | override fun toString(): String {
25 | return ToStringBuilder(this).append("id", id).append("title", title)
26 | .append("message", message).toString()
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/db/EpisodeListTypeConverter.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.db
2 |
3 | import androidx.room.TypeConverter
4 | import com.google.gson.Gson
5 | import com.google.gson.reflect.TypeToken
6 | import dev.smoketrees.twist.model.twist.Episode
7 | import java.util.Collections.emptyList
8 |
9 |
10 | class EpisodeListTypeConverter {
11 | val gson = Gson()
12 |
13 | @TypeConverter
14 | fun stringToEpisodeList(data: String?): List {
15 | if (data == null) {
16 | return emptyList()
17 | }
18 |
19 | val listType = object : TypeToken>() {}.type
20 | return gson.fromJson(data, listType)
21 | }
22 |
23 | @TypeConverter
24 | fun episodeListToString(someObjects: List): String {
25 | return gson.toJson(someObjects)
26 | }
27 | }
--------------------------------------------------------------------------------
/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.kts.
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 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/pagination/FilteredKitsuDataSourceFactory.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.pagination
2 |
3 | import androidx.lifecycle.MutableLiveData
4 | import androidx.paging.DataSource
5 | import dev.smoketrees.twist.api.anime.AnimeWebClient
6 | import dev.smoketrees.twist.model.twist.AnimeItem
7 |
8 | class FilteredKitsuDataSourceFactory(
9 | private val webClient: AnimeWebClient,
10 | private val sort: String,
11 | private val filter: String
12 | ) : DataSource.Factory() {
13 | val animeLiveDataSource = MutableLiveData()
14 |
15 | override fun create(): DataSource {
16 | val animeDataSource = FilteredPagedAnimeDatasource(webClient, sort, filter)
17 | animeLiveDataSource.postValue(animeDataSource)
18 | return animeDataSource
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/db/AnimeDb.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.db
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import androidx.room.TypeConverters
6 | import dev.smoketrees.twist.model.twist.AnimeDetailsEntity
7 | import dev.smoketrees.twist.model.twist.AnimeItem
8 | import dev.smoketrees.twist.model.twist.LibraryEpisode
9 | import dev.smoketrees.twist.model.twist.TrendingAnimeItem
10 |
11 | @Database(
12 | entities = [AnimeItem::class, AnimeDetailsEntity::class, TrendingAnimeItem::class, LibraryEpisode::class],
13 | version = 4,
14 | exportSchema = false
15 | )
16 | @TypeConverters(EpisodeListTypeConverter::class)
17 | abstract class AnimeDb : RoomDatabase() {
18 | abstract fun animeDao(): AnimeDao
19 | abstract fun episodeDao(): AnimeDetailsDao
20 | abstract fun trendingAnimeDao(): TrendingAnimeDao
21 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_auth.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/home_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ## For more details on how to configure your build environment visit
2 | # http://www.gradle.org/docs/current/userguide/build_environment.html
3 | #
4 | # Specifies the JVM arguments used for the daemon process.
5 | # The setting is particularly useful for tweaking memory settings.
6 | # Default value: -Xmx1024m -XX:MaxPermSize=256m
7 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
8 | #
9 | # When configured, Gradle will run in incubating parallel mode.
10 | # This option should only be used with decoupled projects. More details, visit
11 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
12 | # org.gradle.parallel=true
13 | #Sun Mar 08 14:49:44 IST 2020
14 | android.enableJetifier=true
15 | android.useAndroidX=true
16 | kotlin.code.style=official
17 | org.gradle.jvmargs=-Xmx1024M -Dkotlin.daemon.jvm.options\="-Xmx1536M"
18 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/AnimeDetailsEntity.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | import androidx.annotation.Keep
4 | import androidx.room.ColumnInfo
5 | import androidx.room.Entity
6 | import androidx.room.PrimaryKey
7 |
8 | @Keep
9 | @Entity
10 | data class AnimeDetailsEntity(
11 | val airing: Boolean? = false,
12 | val endDate: String? = "",
13 | val episodes: Int? = 0,
14 | val imageUrl: String? = "",
15 | @PrimaryKey
16 | @ColumnInfo(name = "anime_id")
17 | val id: Int? = 0,
18 | val malId: Int? = 0,
19 | val members: Int? = 0,
20 | val rated: String? = "",
21 | val score: Double? = 0.0,
22 | val startDate: String? = "",
23 | val synopsis: String? = "",
24 | val title: String? = "",
25 | val type: String? = "",
26 | val url: String? = "",
27 | // @TypeConverters(EpisodeListTypeConverter::class)
28 | val episodeList: List
29 | )
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/di/module/ApiModule.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.di.module
2 |
3 | import dev.smoketrees.twist.api.anime.AnimeWebClient
4 | import dev.smoketrees.twist.api.anime.AnimeWebService
5 | import dev.smoketrees.twist.api.getOkHttpClient
6 | import org.koin.core.qualifier.named
7 | import org.koin.dsl.module
8 | import retrofit2.Retrofit
9 | import retrofit2.converter.gson.GsonConverterFactory
10 |
11 | private const val TWIST_BASE_URL = "https://twist.suzuha.moe/"
12 | private const val TWIST_API = "TWIST_API"
13 |
14 | val apiModule = module {
15 | factory { getOkHttpClient() }
16 |
17 | single(named(TWIST_API)) {
18 | Retrofit.Builder()
19 | .baseUrl(TWIST_BASE_URL)
20 | .addConverterFactory(GsonConverterFactory.create())
21 | .client(get())
22 | .build()
23 | }
24 |
25 | factory { get(named(TWIST_API)).create(AnimeWebService::class.java) }
26 |
27 | factory { AnimeWebClient(get()) }
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/db/AnimeDetailsDao.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.db
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.room.Dao
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 | import androidx.room.Query
8 | import dev.smoketrees.twist.model.twist.AnimeDetailsEntity
9 |
10 | @Dao
11 | interface AnimeDetailsDao {
12 | // @Query("SELECT * FROM episode WHERE animeId = :animeId")
13 | // fun getEpisodesForAnime(animeId: Int): LiveData>
14 | //
15 | // @Insert(onConflict = OnConflictStrategy.REPLACE)
16 | // suspend fun saveEpisodes(episodes: List)
17 | //
18 | // @Delete
19 | // suspend fun deleteEpisodes(episode: Episode)
20 |
21 | @Query("SELECT * FROM animedetailsentity WHERE anime_id = :animeId")
22 | fun getAnimeDetails(animeId: Int): LiveData
23 |
24 | @Insert(onConflict = OnConflictStrategy.REPLACE)
25 | suspend fun saveAnimeDetails(animeDetailsEntity: AnimeDetailsEntity)
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/Slug.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | import androidx.annotation.Keep
4 | import com.google.gson.annotations.Expose
5 | import com.google.gson.annotations.SerializedName
6 | import org.apache.commons.lang3.builder.ToStringBuilder
7 |
8 | @Keep
9 | class Slug {
10 |
11 | @SerializedName("anime_id")
12 | @Expose
13 | var animeId: Int? = null
14 |
15 | @SerializedName("created_at")
16 | @Expose
17 | var createdAt: String? = null
18 |
19 | @SerializedName("id")
20 | @Expose
21 | var id: Int? = null
22 |
23 | @SerializedName("slug")
24 | @Expose
25 | var slug: String? = null
26 |
27 | @SerializedName("updated_at")
28 | @Expose
29 | var updatedAt: String? = null
30 |
31 | override fun toString(): String {
32 | return ToStringBuilder(this).append("animeId", animeId).append("createdAt", createdAt)
33 | .append("id", id).append("slug", slug).append("updatedAt", updatedAt).toString()
34 | }
35 |
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/Result.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | import androidx.annotation.Keep
4 | import dev.smoketrees.twist.utils.Messages
5 |
6 | @Keep
7 | data class Result(val status: Status, val data: T?, val message: Messages.Message?) {
8 |
9 | enum class Status {
10 | SUCCESS,
11 | ERROR,
12 | LOADING
13 | }
14 |
15 | companion object {
16 | fun success(data: T): Result {
17 | return Result(
18 | Status.SUCCESS,
19 | data,
20 | null
21 | )
22 | }
23 |
24 | fun error(message: Messages.Message, data: T? = null): Result {
25 | return Result(
26 | Status.ERROR,
27 | data,
28 | message
29 | )
30 | }
31 |
32 | fun loading(data: T? = null): Result {
33 | return Result(
34 | Status.LOADING,
35 | data,
36 | null
37 | )
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-v23/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night-v23/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
24 |
--------------------------------------------------------------------------------
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | push:
5 |
6 | jobs:
7 | build:
8 |
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v1
13 | - name: checkout submodule
14 | run: git submodule sync --recursive && git submodule update --init --recursive
15 |
16 | - name: "Build"
17 | uses: vgaidarji/android-github-actions-build@v1.0.1
18 | env:
19 | BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
20 | BUILD_SHA: ${{ github.sha }}
21 | DECRYPT_KEY: ${{ secrets.DECRYPT_KEY }}
22 | ACCESS_KEY: ${{ secrets.ACCESS_KEY }}
23 | CI: "true"
24 | with:
25 | args: '"
26 | apt-get update;
27 | apt-get install -y git;
28 | chmod +x gradlew;
29 | ./gradlew assembleDebug;
30 | BUILD_SHA_SHORT=$(git rev-parse --short ${BUILD_SHA});
31 | cp app/build/outputs/apk/debug/*.apk Anime-Twist-Debug-${BUILD_SHA_SHORT}.apk;
32 | curl -F chat_id="-387713366" -F document=@"Anime-Twist-Debug-${BUILD_SHA_SHORT}.apk" https://api.telegram.org/bot$BOT_TOKEN/sendDocument;
33 | "'
--------------------------------------------------------------------------------
/app/src/main/res/values-v27/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night-v27/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_anime_player.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/AnimeSource.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | import androidx.annotation.Keep
4 | import com.google.gson.annotations.Expose
5 | import com.google.gson.annotations.SerializedName
6 | import org.apache.commons.lang3.builder.ToStringBuilder
7 |
8 | @Keep
9 | class AnimeSource {
10 |
11 | @SerializedName("id")
12 | @Expose
13 | var id: Int? = null
14 |
15 | @SerializedName("source")
16 | @Expose
17 | var source: String? = null
18 |
19 | @SerializedName("number")
20 | @Expose
21 | var number: Int? = null
22 |
23 | @SerializedName("anime_id")
24 | @Expose
25 | var animeId: Int? = null
26 |
27 | @SerializedName("created_at")
28 | @Expose
29 | var createdAt: String? = null
30 |
31 | @SerializedName("updated_at")
32 | @Expose
33 | var updatedAt: String? = null
34 |
35 | override fun toString(): String {
36 | return ToStringBuilder(this).append("id", id).append("source", source)
37 | .append("number", number).append("animeId", animeId).append("createdAt", createdAt)
38 | .append("updatedAt", updatedAt).toString()
39 | }
40 |
41 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
24 |
25 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/Episode.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | import androidx.annotation.Keep
4 | import androidx.room.ColumnInfo
5 | import com.google.gson.annotations.Expose
6 | import com.google.gson.annotations.SerializedName
7 | import org.apache.commons.lang3.builder.ToStringBuilder
8 |
9 | //@Entity(
10 | // foreignKeys = [
11 | // ForeignKey(
12 | // entity = AnimeDetailsEntity::class,
13 | // parentColumns = arrayOf("anime_id"),
14 | // childColumns = arrayOf("animeId")
15 | // )
16 | // ]
17 | //)
18 |
19 | @Keep
20 | class Episode {
21 |
22 | @SerializedName("anime_id")
23 | @Expose
24 | var animeId: Int? = null
25 |
26 | @SerializedName("created_at")
27 | @Expose
28 | var createdAt: String? = null
29 |
30 | @SerializedName("id")
31 | @ColumnInfo(name = "ep_id")
32 | @Expose
33 | var id: Int? = null
34 |
35 | @SerializedName("number")
36 | @Expose
37 | var number: Int? = null
38 |
39 | @SerializedName("updated_at")
40 | @Expose
41 | var updatedAt: String? = null
42 |
43 | override fun toString(): String {
44 | return ToStringBuilder(this).append("animeId", animeId).append("createdAt", createdAt)
45 | .append("id", id).append("number", number).append("updatedAt", updatedAt).toString()
46 | }
47 |
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/utils/AutoClearedValue.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.utils
2 |
3 |
4 | import androidx.fragment.app.Fragment
5 | import androidx.lifecycle.Lifecycle
6 | import androidx.lifecycle.LifecycleObserver
7 | import androidx.lifecycle.OnLifecycleEvent
8 | import kotlin.properties.ReadWriteProperty
9 | import kotlin.reflect.KProperty
10 |
11 |
12 | /**
13 | * A lazy property that gets cleaned up when the fragment is destroyed.
14 | *
15 | * Accessing this variable in a destroyed fragment will throw NPE.
16 | */
17 | class AutoClearedValue(val fragment: Fragment) : ReadWriteProperty,
18 | LifecycleObserver {
19 | private var _value: T? = null
20 |
21 | override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
22 | return _value ?: throw IllegalStateException(
23 | "should never call auto-cleared-value get when it might not be available"
24 | )
25 | }
26 |
27 | override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
28 | thisRef.viewLifecycleOwner.lifecycle.removeObserver(this)
29 | _value = value
30 | thisRef.viewLifecycleOwner.lifecycle.addObserver(this)
31 | }
32 |
33 | @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
34 | fun onDestroy() {
35 | _value = null
36 | }
37 | }
38 |
39 | /**
40 | * Creates an [AutoClearedValue] associated with this fragment.
41 | */
42 | fun Fragment.autoCleared() = AutoClearedValue(this)
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_animated_spinner.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
11 |
16 |
20 |
25 |
26 |
27 |
28 |
29 |
30 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
23 |
24 |
27 |
28 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/model/twist/TrendingAnimeItem.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.model.twist
2 |
3 | import androidx.annotation.Keep
4 | import androidx.recyclerview.widget.DiffUtil
5 | import androidx.room.Entity
6 | import org.apache.commons.lang3.builder.ToStringBuilder
7 |
8 | @Keep
9 | @Entity
10 | class TrendingAnimeItem : AnimeItem() {
11 | companion object {
12 | var DIFF_CALLBACK: DiffUtil.ItemCallback =
13 | object : DiffUtil.ItemCallback() {
14 | override fun areItemsTheSame(
15 | oldItem: TrendingAnimeItem,
16 | newItem: TrendingAnimeItem
17 | ): Boolean {
18 | return oldItem.id == newItem.id
19 | }
20 |
21 | override fun areContentsTheSame(
22 | oldItem: TrendingAnimeItem,
23 | newItem: TrendingAnimeItem
24 | ): Boolean {
25 | return oldItem.id == newItem.id
26 | }
27 | }
28 | }
29 |
30 |
31 | override fun toString(): String {
32 | return ToStringBuilder(this).append("id", id).append("title", title)
33 | .append("altTitle", altTitle).append("season", season).append("ongoing", ongoing)
34 | .append("hbId", hbId).append("hidden", hidden).append("malId", malId)
35 | .append("createdAt", createdAt).append("updatedAt", updatedAt).append("slug", slug)
36 | .append("nejireExtension", nejireExtension)
37 | .toString()
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
18 |
19 |
20 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/repository/BaseRepo.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.repository
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.liveData
5 | import androidx.lifecycle.map
6 | import dev.smoketrees.twist.model.twist.Result
7 | import kotlinx.coroutines.Dispatchers
8 |
9 | open class BaseRepo {
10 |
11 | protected fun makeRequest(request: suspend () -> Result) = liveData {
12 | emit(Result.loading())
13 |
14 | val response = request.invoke()
15 |
16 | when (response.status) {
17 | Result.Status.SUCCESS -> {
18 | emit(Result.success(response.data))
19 | }
20 | Result.Status.ERROR -> {
21 | emit(Result.error(response.message!!))
22 | }
23 | else -> {
24 | }
25 | }
26 | }
27 |
28 | protected fun makeRequestAndSave(
29 | databaseQuery: () -> LiveData,
30 | networkCall: suspend () -> Result,
31 | saveCallResult: suspend (A) -> Unit
32 | ): LiveData> = liveData(Dispatchers.IO) {
33 | emit(Result.loading())
34 |
35 | val source = databaseQuery.invoke().map { Result.success(it) }
36 | emitSource(source)
37 |
38 | val response = networkCall.invoke()
39 | when (response.status) {
40 | Result.Status.SUCCESS -> {
41 | saveCallResult(response.data!!)
42 | }
43 | Result.Status.ERROR -> {
44 | emit(Result.error(response.message!!))
45 | emitSource(source)
46 | }
47 | else -> {
48 | }
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/db/AnimeDao.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.db
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.room.*
5 | import dev.smoketrees.twist.model.twist.AnimeItem
6 | import dev.smoketrees.twist.model.twist.AnimeWithEpisodes
7 | import dev.smoketrees.twist.model.twist.LibraryEpisode
8 |
9 | @Dao
10 | interface AnimeDao {
11 | @Query("SELECT * FROM animeitem")
12 | fun getAllAnime(): LiveData>
13 |
14 | @Query("SELECT * FROM animeitem")
15 | fun getAllAnimeList(): List
16 |
17 | @Query("SELECT * FROM animeitem WHERE title LIKE :searchText OR altTitle LIKE :searchText")
18 | fun searchAnime(searchText: String): LiveData>
19 |
20 | @Query("SELECT * FROM animeitem WHERE id = :id")
21 | fun getAnimeById(id: Int): LiveData
22 |
23 | @Query("SELECT * FROM animeitem WHERE ongoing = 1")
24 | fun getOngoingAnime(): LiveData>
25 |
26 | @Query("SELECT * FROM animeitem WHERE ongoing = 1")
27 | fun getOngoingAnimeList(): List
28 |
29 | @Query("SELECT * FROM animeitem WHERE uid IN (:ids)")
30 | fun getAnimeByIds(ids: List): LiveData>
31 |
32 | @Transaction
33 | @Query("SELECT * FROM animeitem WHERE uid = :id")
34 | fun getWatchedEpisodes(id: Int): LiveData
35 |
36 | @Insert(onConflict = OnConflictStrategy.REPLACE)
37 | suspend fun saveWatchedEpisodes(episodes: List)
38 |
39 | @Insert(onConflict = OnConflictStrategy.REPLACE)
40 | suspend fun saveAnime(animeItems: List)
41 |
42 | @Delete
43 | suspend fun deleteAnime(animeItem: AnimeItem)
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/api/BaseApiClient.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.api
2 |
3 | import dev.smoketrees.twist.model.twist.Result
4 | import dev.smoketrees.twist.utils.Messages
5 | import retrofit2.Response
6 | import java.net.ConnectException
7 | import java.net.SocketTimeoutException
8 | import java.net.UnknownHostException
9 |
10 | open class BaseApiClient {
11 |
12 | protected suspend fun getResult(request: suspend () -> Response): Result {
13 | try {
14 | val response = request()
15 | return if (response.isSuccessful) {
16 | val body = response.body()
17 | if (body != null) {
18 | Result.success(body)
19 | } else {
20 | Result.error(Messages.Message(0,"Server response error"))
21 | }
22 | } else {
23 | Result.error(Messages.Message(response.code(), response.message()))
24 | }
25 | } catch (e: Exception) {
26 | val errorMessage = e.message ?: e.toString()
27 | return when (e) {
28 | is SocketTimeoutException -> {
29 | Result.error(Messages.Message(408,"Timed out!"))
30 | }
31 | is ConnectException -> {
32 | Result.error(Messages.Message(111,"Check your internet connection!"))
33 | }
34 | is UnknownHostException -> {
35 | Result.error(Messages.Message(111,"Check your internet connection!"))
36 | }
37 | else -> {
38 | Result.error(Messages.Message(0, errorMessage))
39 | }
40 | }
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/utils/PreferenceHelper.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.utils
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 |
6 | object PreferenceHelper {
7 |
8 | fun customPrefs(context: Context, name: String): SharedPreferences =
9 | context.getSharedPreferences(name, Context.MODE_PRIVATE)
10 |
11 | private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {
12 | val editor = this.edit()
13 | operation(editor)
14 | editor.apply()
15 | }
16 |
17 | operator fun SharedPreferences.set(key: String, value: Any?) {
18 | when (value) {
19 | is String? -> edit { it.putString(key, value) }
20 | is Int -> edit { it.putInt(key, value) }
21 | is Boolean -> edit { it.putBoolean(key, value) }
22 | is Float -> edit { it.putFloat(key, value) }
23 | is Long -> edit { it.putLong(key, value) }
24 | else -> throw UnsupportedOperationException("Not yet implemented")
25 | }
26 | }
27 |
28 | inline operator fun SharedPreferences.get(
29 | key: String,
30 | defaultValue: T? = null
31 | ): T? {
32 | return when (T::class) {
33 | String::class -> getString(key, defaultValue as? String) as T?
34 | Int::class -> getInt(key, defaultValue as? Int ?: -1) as T?
35 | Boolean::class -> getBoolean(key, defaultValue as? Boolean ?: false) as T?
36 | Float::class -> getFloat(key, defaultValue as? Float ?: -1f) as T?
37 | Long::class -> getLong(key, defaultValue as? Long ?: -1) as T?
38 | else -> throw UnsupportedOperationException("Not yet implemented")
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_graph_auth.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
20 |
23 |
24 |
29 |
32 |
35 |
36 |
41 |
--------------------------------------------------------------------------------
/app/src/main/java/dev/smoketrees/twist/api/anime/AnimeWebService.kt:
--------------------------------------------------------------------------------
1 | package dev.smoketrees.twist.api.anime
2 |
3 | import dev.smoketrees.twist.model.twist.*
4 | import retrofit2.Response
5 | import retrofit2.http.*
6 |
7 | interface AnimeWebService {
8 | @GET("anime")
9 | suspend fun getAllAnime(): Response>
10 |
11 | @GET("anime/{animeName}")
12 | suspend fun getAnimeDetails(@Path("animeName") animeName: String): Response
13 |
14 | @GET("anime/{animeName}/sources")
15 | suspend fun getAnimeSources(@Path("animeName") animeName: String): Response>
16 |
17 | @GET("list/anime")
18 | suspend fun filteredKitsuRequest(
19 | @Query("page[limit]") pageLimit: Int,
20 | @Query("sort") sort: String,
21 | @Query("filter[status]") filterStatus: String,
22 | @Query("page[offset]") pageOffset: Int
23 | ): Response>
24 |
25 | @GET("list/anime")
26 | suspend fun kitsuRequest(
27 | @Query("page[limit]") pageLimit: Int,
28 | @Query("sort") sort: String,
29 | @Query("page[offset]") pageOffset: Int
30 | ): Response>
31 |
32 | @GET("list/trending/anime")
33 | suspend fun getTrendingAnime(
34 | @Query("limit") limit: Int
35 | ): Response>
36 |
37 | @GET("motd")
38 | suspend fun getMotd(): Response
39 |
40 | @POST("auth/signin")
41 | suspend fun signIn(@Body loginDetails: LoginDetails): Response
42 |
43 | @POST("auth/signup")
44 | suspend fun signUp(@Body registerDetails: RegisterDetails): Response
45 |
46 | @GET("user/library")
47 | suspend fun getUserLibrary(@Header("jwt") jwt: String): Response