diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e3e65b..4580de3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Make sure that your pull request is based off of the latest `master` branch. ## PR titles / content -Please make sure to follow the default PR template (available in [.github/pull_request_template.md](https://github.com/kuylar/lighttube-android/blob/master/.github/pull_request_template.md)). +Please make sure to follow the default PR template (available in [.github/pull_request_template.md](https://github.com/lighttube-org/lighttube-android/blob/master/.github/pull_request_template.md)). Also, make sure that your PR titles describe what it does in a brief but understandable way. diff --git a/README.md b/README.md index 05c48c0..89333a4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # LightTube Android -> ⚠️ This app is in early development. Use at your own risk +> [!WARNING] +> This app is in early development. Use at your own risk A Material You design YouTube client, using [LightTube][1] to sync subscriptions & playlists between devices @@ -13,9 +14,9 @@ devices | Subscription Feeds | Complete | | Dislike Counts (using [ReturnYouTubeDislike][2]) | Complete | | [SponsorBlock][3] | Complete | +| Playlists | Complete | | Video Downloads & Offline Playback | Planned | | Background playback | Planned | -| Playlists | Planned | | More to possibly come soon | | ## Installing @@ -26,10 +27,10 @@ devices 3. Scroll down, and download the artifact (named LightTube, you might need to log into GitHub) 4. Unzip the file & install it on your device -[1]: https://github.com/kuylar/lighttube +[1]: https://github.com/lighttube-org/lighttube [2]: https://returnyoutubedislike.com/ [3]: https://sponsor.ajay.app/ -[4]: https://github.com/kuylar/lighttube-android/actions \ No newline at end of file +[4]: https://github.com/lighttube-org/lighttube-android/actions \ No newline at end of file diff --git a/app/src/main/java/dev/kuylar/lighttube/Utils.kt b/app/src/main/java/dev/kuylar/lighttube/Utils.kt index 1565fbf..7bad509 100644 --- a/app/src/main/java/dev/kuylar/lighttube/Utils.kt +++ b/app/src/main/java/dev/kuylar/lighttube/Utils.kt @@ -86,7 +86,7 @@ class Utils { } private fun getUserAgent(): String = - "LightTube-Android/${BuildConfig.VERSION_NAME} (https://github.com/kuylar/lighttube-android)" + "LightTube-Android/${BuildConfig.VERSION_NAME} (https://github.com/lighttube-org/lighttube-android)" fun getDislikeCount(videoId: String): Long { try { @@ -136,7 +136,7 @@ class Utils { var updateInfo: UpdateInfo? = null try { val req = Request.Builder().apply { - url("https://api.github.com/repos/kuylar/lighttube-android/releases/latest") + url("https://api.github.com/repos/lighttube-org/lighttube-android/releases/latest") header("User-Agent", getUserAgent()) }.build() diff --git a/app/src/main/java/dev/kuylar/lighttube/api/LightTubeApi.kt b/app/src/main/java/dev/kuylar/lighttube/api/LightTubeApi.kt index 6ef9911..7ff1db6 100644 --- a/app/src/main/java/dev/kuylar/lighttube/api/LightTubeApi.kt +++ b/app/src/main/java/dev/kuylar/lighttube/api/LightTubeApi.kt @@ -33,7 +33,7 @@ import java.io.IOException import java.net.URLEncoder -class LightTubeApi(context: Context) { +class LightTubeApi { private val tag = "LightTubeApi" private val client = OkHttpClient() private val gson = GsonBuilder().apply { @@ -44,16 +44,21 @@ class LightTubeApi(context: Context) { val host: String private val refreshToken: String? - init { + constructor(context: Context, refreshToken: String? = null) { val sp = context.getSharedPreferences("main", Context.MODE_PRIVATE) host = sp.getString("instanceHost", "")!! - refreshToken = sp.getString("refreshToken", null) + this.refreshToken = refreshToken ?: sp.getString("refreshToken", null) Log.i( tag, "Initialized the API for $host ${if (refreshToken != null) "with" else "without"} a refresh token" ) } + constructor(instance: String) { + host = instance + refreshToken = null + } + @Throws(LightTubeException::class, IOException::class) private fun get( token: TypeToken>, diff --git a/app/src/main/java/dev/kuylar/lighttube/api/UtilityApi.kt b/app/src/main/java/dev/kuylar/lighttube/api/UtilityApi.kt index 232dd1a..862c92e 100644 --- a/app/src/main/java/dev/kuylar/lighttube/api/UtilityApi.kt +++ b/app/src/main/java/dev/kuylar/lighttube/api/UtilityApi.kt @@ -1,9 +1,14 @@ package dev.kuylar.lighttube.api +import android.app.Activity import android.util.Log +import android.view.View import com.google.gson.Gson import com.google.gson.JsonObject import com.google.gson.reflect.TypeToken +import dev.kuylar.lighttube.R +import dev.kuylar.lighttube.api.models.InstanceInfo +import dev.kuylar.lighttube.databinding.ItemInstanceBinding import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request @@ -11,16 +16,17 @@ import java.util.Date class UtilityApi { companion object { + private val http = OkHttpClient() private var refreshToken: String = "" private var accessToken: String = "" private var tokenExpiryTimestamp: Long = 0 fun getInstances(): ArrayList { val request: Request = Request.Builder() - .url("https://raw.githubusercontent.com/kuylar/lighttube/master/public_instances.json") + .url("https://lighttube.kuylar.dev/instances") .build() - OkHttpClient().newCall(request).execute().use { response -> + http.newCall(request).execute().use { response -> return Gson().fromJson( response.body!!.string(), object : TypeToken>() {}.type @@ -58,7 +64,7 @@ class UtilityApi { .build() - OkHttpClient().newCall(request).execute().use { response -> + http.newCall(request).execute().use { response -> val res = Gson().fromJson( response.body!!.string(), JsonObject::class.java @@ -91,7 +97,7 @@ class UtilityApi { .post(body) .build() - OkHttpClient().newCall(request).execute().use { response -> + http.newCall(request).execute().use { response -> val res = Gson().fromJson( response.body!!.string(), JsonObject::class.java @@ -112,8 +118,57 @@ class UtilityApi { } } -class LightTubeInstance( +data class LightTubeInstance( val host: String, - val api: Boolean, - val accounts: Boolean -) \ No newline at end of file + val country: String, + val scheme: String, + val isCloudflare: Boolean, + val apiEnabled: Boolean, + val proxyEnabled: String, + val accountsEnabled: Boolean, +) { + fun fillBinding(binding: ItemInstanceBinding, activity: Activity) { + val context = binding.root.context + val instanceInfo: InstanceInfo + try { + instanceInfo = LightTubeApi("$scheme://$host").getInstanceInfo() + } catch (e: Exception) { + activity.runOnUiThread { + binding.loading.visibility = View.GONE + Log.e("UtilityApi.fillBinding", "Failed to get information about instance $host") + binding.instanceTitle.text = arrayOf(getFlag(), host).joinToString(" ") + binding.instanceDescription.text = context.getString( + R.string.setup_instance_load_fail, + context.getString(R.string.setup_instance_load_fail_this) + ) + } + return + } + + activity.runOnUiThread { + binding.loading.visibility = View.GONE + binding.instanceTitle.text = arrayOf(getFlag(), host).joinToString(" ") + binding.instanceDescription.text = if (instanceInfo.type != "lighttube") + context.getString(R.string.setup_instance_invalid, instanceInfo.type) + else if (apiEnabled) + context.getString( + R.string.template_instance_info, + instanceInfo.version, + if (accountsEnabled) context.getString(R.string.enabled) else context.getString( + R.string.disabled + ), + if (proxyEnabled == "all") context.getString(R.string.enabled) else context.getString( + R.string.disabled + ) + ) + else context.getString(R.string.setup_instance_api_disabled) + binding.instanceCloudflare.visibility = if (isCloudflare) View.VISIBLE else View.GONE + } + } + + private fun getFlag(): String { + val firstLetter = Character.codePointAt(country, 0) - 0x41 + 0x1F1E6 + val secondLetter = Character.codePointAt(country, 1) - 0x41 + 0x1F1E6 + return String(Character.toChars(firstLetter)) + String(Character.toChars(secondLetter)) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/kuylar/lighttube/api/models/InstanceInfo.kt b/app/src/main/java/dev/kuylar/lighttube/api/models/InstanceInfo.kt index 9995108..48b792e 100644 --- a/app/src/main/java/dev/kuylar/lighttube/api/models/InstanceInfo.kt +++ b/app/src/main/java/dev/kuylar/lighttube/api/models/InstanceInfo.kt @@ -4,8 +4,8 @@ class InstanceInfo ( val type: String, val version: String, val motd: String, - val allowsAPI: Boolean, + val allowsApi: Boolean, val allowsNewUsers: Boolean, - val allowsOauthAPI: Boolean, + val allowsOauthApi: Boolean, val allowsThirdPartyProxyUsage: Boolean ) \ No newline at end of file diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/LoginWebViewClient.kt b/app/src/main/java/dev/kuylar/lighttube/ui/LoginWebViewClient.kt index 3885b21..5c2d93a 100644 --- a/app/src/main/java/dev/kuylar/lighttube/ui/LoginWebViewClient.kt +++ b/app/src/main/java/dev/kuylar/lighttube/ui/LoginWebViewClient.kt @@ -6,7 +6,10 @@ import android.webkit.WebView import android.webkit.WebViewClient -class LoginWebViewClient(val onTokenReceived: (String) -> Unit, val showLoading: (Boolean) -> Unit) : WebViewClient() { +class LoginWebViewClient( + private val onTokenReceived: (String) -> Unit, + private val showLoading: (Boolean) -> Unit +) : WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { if (request == null) return false; return if (request.url != null && request.url.scheme == "lighttube") { diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/activity/LoginActivity.kt b/app/src/main/java/dev/kuylar/lighttube/ui/activity/LoginActivity.kt index e8f0121..3597a4f 100644 --- a/app/src/main/java/dev/kuylar/lighttube/ui/activity/LoginActivity.kt +++ b/app/src/main/java/dev/kuylar/lighttube/ui/activity/LoginActivity.kt @@ -1,17 +1,19 @@ package dev.kuylar.lighttube.ui.activity import android.app.ProgressDialog -import android.content.Intent import android.content.SharedPreferences import android.os.Bundle import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.edit import com.google.android.material.snackbar.Snackbar import dev.kuylar.lighttube.R import dev.kuylar.lighttube.api.LightTubeApi import dev.kuylar.lighttube.api.UtilityApi import dev.kuylar.lighttube.databinding.ActivityLoginBinding import dev.kuylar.lighttube.ui.LoginWebViewClient +import java.net.URLEncoder +import java.nio.charset.Charset import kotlin.concurrent.thread @@ -31,12 +33,14 @@ class LoginActivity : AppCompatActivity() { binding = ActivityLoginBinding.inflate(layoutInflater) setContentView(binding.root) + val isRegister = intent.extras?.getBoolean("register", false) ?: false + sp = getSharedPreferences("main", MODE_PRIVATE) val client = LoginWebViewClient(this::onTokenReceived, this::showProgressBarLoading) binding.loginWebView.webViewClient = client binding.loginWebView.settings.databaseEnabled = true - binding.loginWebView.loadUrl(getOauthUrl(scopes)) + binding.loginWebView.loadUrl(if (isRegister) getRegisterUrl(scopes) else getOauthUrl(scopes)) Snackbar.make(binding.root, R.string.login_hint, Snackbar.LENGTH_LONG).show() } @@ -60,6 +64,23 @@ class LoginActivity : AppCompatActivity() { }" } + private fun getRegisterUrl(scopes: List): String { + val sp = getSharedPreferences("main", MODE_PRIVATE) + val host = sp.getString("instanceHost", "") + val redirectUrl = + "/oauth2/authorize?response_type=code&client_id=LightTube Android&redirect_uri=lighttube://login&scope=${ + scopes.joinToString( + " " + ) + }" + return "$host/account/register?redirectUrl=${ + URLEncoder.encode( + redirectUrl, + Charset.defaultCharset().name() + ) + }" + } + private fun onTokenReceived(token: String) { showProgressBarLoading(true) val dialog = ProgressDialog.show( @@ -74,13 +95,13 @@ class LoginActivity : AppCompatActivity() { token, "lighttube://login" ) - sp.edit().apply { - putString("refreshToken", token) - apply() - } - val api = LightTubeApi(this) + val api = LightTubeApi(this, token) val user = api.getCurrentUser() val username = user.data!!.userID + sp.edit { + putString("refreshToken", token) + putString("username", username) + } runOnUiThread { dialog.dismiss() Toast.makeText( @@ -89,7 +110,7 @@ class LoginActivity : AppCompatActivity() { Toast.LENGTH_LONG ).show() showProgressBarLoading(false) - startActivity(Intent(this, MainActivity::class.java)) + setResult(RESULT_OK) finish() } } diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/activity/SetupActivity.kt b/app/src/main/java/dev/kuylar/lighttube/ui/activity/SetupActivity.kt index 38fa02b..b70519b 100644 --- a/app/src/main/java/dev/kuylar/lighttube/ui/activity/SetupActivity.kt +++ b/app/src/main/java/dev/kuylar/lighttube/ui/activity/SetupActivity.kt @@ -2,20 +2,26 @@ package dev.kuylar.lighttube.ui.activity import android.content.Intent import android.os.Bundle -import android.view.View -import android.widget.TextView +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment import com.google.android.material.elevation.SurfaceColors -import dev.kuylar.lighttube.R -import dev.kuylar.lighttube.api.UtilityApi import dev.kuylar.lighttube.databinding.ActivitySetupBinding -import kotlin.concurrent.thread +import dev.kuylar.lighttube.ui.fragment.setup.SetupCustomInstanceFragment +import dev.kuylar.lighttube.ui.fragment.setup.SetupFinishFragment +import dev.kuylar.lighttube.ui.fragment.setup.SetupInstanceSelectFragment +import dev.kuylar.lighttube.ui.fragment.setup.SetupLoginFragment +import dev.kuylar.lighttube.ui.fragment.setup.SetupWelcomeFragment class SetupActivity : AppCompatActivity() { private lateinit var binding: ActivitySetupBinding + private var publicInstancesFragment: Fragment? = null + private var customInstanceFragment: Fragment? = null + + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val sp = getSharedPreferences("main", MODE_PRIVATE) + getSharedPreferences("main", MODE_PRIVATE) // set status bar & bottom nav bar colors val color = SurfaceColors.SURFACE_2.getColor(this) @@ -24,57 +30,57 @@ class SetupActivity : AppCompatActivity() { binding = ActivitySetupBinding.inflate(layoutInflater) setContentView(binding.root) + } - thread { - val instances = UtilityApi.getInstances() - runOnUiThread { - while (binding.instanceList.childCount > 0) { - binding.instanceList.removeViewAt(0) - } - - for (instance in instances) { - if (!instance.api) continue - - val v = layoutInflater.inflate(R.layout.item_instance, null) - v.findViewById(R.id.instanceTitle).text = instance.host - v.findViewById(R.id.instanceDescription).text = getString( - R.string.template_instance_info, - if (instance.api) getString(R.string.enabled) else getString(R.string.disabled), - if (instance.accounts) getString(R.string.enabled) else getString(R.string.disabled) - ) - v.setOnClickListener { - sp.edit().apply { - putString("instanceHost", instance.host) - apply() - } - if (!instance.accounts) finishSetup() - else { - binding.setupScreenInstance.visibility = View.GONE - binding.setupScreenLogin.visibility = View.VISIBLE - } - } - binding.instanceList.addView(v) - } - } - } + fun finishSetup() { + startActivity(Intent(this, MainActivity::class.java)) + finish() + } - binding.setupButtonWelcomeNext.setOnClickListener { - binding.setupScreenWelcome.visibility = View.GONE - binding.setupScreenInstance.visibility = View.VISIBLE + fun goToStage(stage: Int) { + if (!validStages.contains(stage)) { + Toast.makeText(this, "invalid setup stage: $stage", Toast.LENGTH_LONG).show() + return } + val fragment = when (stage) { + STAGE_WELCOME -> SetupWelcomeFragment() + STAGE_INSTANCES_PUBLIC -> { + if (publicInstancesFragment == null) + publicInstancesFragment = SetupInstanceSelectFragment() + publicInstancesFragment!! + } - binding.setupButtonLoginSkip.setOnClickListener { - finishSetup() - } + STAGE_INSTANCES_CUSTOM -> { + if (customInstanceFragment == null) + customInstanceFragment = SetupCustomInstanceFragment() + customInstanceFragment!! + } - binding.setupButtonLoginLogin.setOnClickListener { - startActivity(Intent(this, LoginActivity::class.java)) - finish() + STAGE_LOGIN -> SetupLoginFragment() + STAGE_FINISH -> SetupFinishFragment() + else -> SetupWelcomeFragment() } + supportFragmentManager.beginTransaction().apply { + replace(binding.setupFragmentContainer.id, fragment) + }.commit() } - private fun finishSetup() { - startActivity(Intent(this, MainActivity::class.java)) - finish() + companion object { + const val STAGE_WELCOME = 0 + const val STAGE_INSTANCES_PUBLIC = 1 + const val STAGE_INSTANCES_CUSTOM = 2 + const val STAGE_LOGIN = 3 + const val STAGE_FINISH = 4 + const val REQUEST_CODE_LOGIN = 0 + const val RESULT_CODE_LOGIN_SUCCESS = 0 + const val RESULT_CODE_LOGIN_FAIL = 1 + + val validStages = arrayOf( + STAGE_WELCOME, + STAGE_INSTANCES_PUBLIC, + STAGE_INSTANCES_CUSTOM, + STAGE_LOGIN, + STAGE_FINISH, + ) } } \ No newline at end of file diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/adapter/InstanceListAdapter.kt b/app/src/main/java/dev/kuylar/lighttube/ui/adapter/InstanceListAdapter.kt new file mode 100644 index 0000000..e0315fd --- /dev/null +++ b/app/src/main/java/dev/kuylar/lighttube/ui/adapter/InstanceListAdapter.kt @@ -0,0 +1,35 @@ +package dev.kuylar.lighttube.ui.adapter + +import android.app.Activity +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import dev.kuylar.lighttube.api.LightTubeInstance +import dev.kuylar.lighttube.databinding.ItemInstanceBinding +import kotlin.concurrent.thread + +class InstanceListAdapter( + private val activity: Activity, + private val items: List, + private val onClick: (LightTubeInstance) -> Unit +) : RecyclerView.Adapter() { + class InstanceViewHolder(private val binding: ItemInstanceBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(instance: LightTubeInstance, onClick: (LightTubeInstance) -> Unit) { + thread { + instance.fillBinding(binding, (binding.root.context as Activity)) + } + binding.root.setOnClickListener { + onClick.invoke(instance) + } + } + } + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + InstanceViewHolder(ItemInstanceBinding.inflate(activity.layoutInflater, parent, false)) + + override fun onBindViewHolder(holder: InstanceViewHolder, position: Int) { + holder.bind(items[position], onClick) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/fragment/setup/SetupCustomInstanceFragment.kt b/app/src/main/java/dev/kuylar/lighttube/ui/fragment/setup/SetupCustomInstanceFragment.kt new file mode 100644 index 0000000..cb9aabb --- /dev/null +++ b/app/src/main/java/dev/kuylar/lighttube/ui/fragment/setup/SetupCustomInstanceFragment.kt @@ -0,0 +1,108 @@ +package dev.kuylar.lighttube.ui.fragment.setup + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.edit +import androidx.fragment.app.Fragment +import dev.kuylar.lighttube.R +import dev.kuylar.lighttube.api.LightTubeApi +import dev.kuylar.lighttube.api.models.InstanceInfo +import dev.kuylar.lighttube.databinding.FragmentSetupCustomInstanceBinding +import dev.kuylar.lighttube.ui.activity.SetupActivity +import kotlin.concurrent.thread + +class SetupCustomInstanceFragment : Fragment() { + private lateinit var binding: FragmentSetupCustomInstanceBinding + private var loadingUrl = "" + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentSetupCustomInstanceBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.publicInstances.setOnClickListener { + (activity as SetupActivity).goToStage(SetupActivity.STAGE_INSTANCES_PUBLIC) + } + binding.fieldInstance.editText!!.setOnKeyListener { _, _, _ -> + updateInstanceInfo(binding.fieldInstance.editText!!.editableText.toString()) + false + } + } + + private fun updateInstanceInfo(url: String) { + if (url == loadingUrl) return + loadingUrl = url + clearCard() + thread { + try { + val instanceInfo = LightTubeApi(url).getInstanceInfo() + activity?.runOnUiThread { + if (loadingUrl != url) return@runOnUiThread + fillData(instanceInfo, url) + } + } catch (e: Exception) { + activity?.runOnUiThread { + if (loadingUrl != url) return@runOnUiThread + fillError(e, url) + } + } + } + } + + private fun fillData(instanceInfo: InstanceInfo, url: String) { + binding.instanceCard.root.setOnClickListener { + next(url, instanceInfo.allowsOauthApi) + } + binding.instanceCard.loading.visibility = View.GONE + binding.instanceCard.instanceTitle.text = url + binding.instanceCard.instanceDescription.text = if (instanceInfo.type != "lighttube") + getString(R.string.setup_instance_invalid, instanceInfo.type) + else if (instanceInfo.allowsApi) + getString( + R.string.template_instance_info, + instanceInfo.version, + if (instanceInfo.allowsOauthApi) getString(R.string.enabled) else getString(R.string.disabled), + if (instanceInfo.allowsThirdPartyProxyUsage) getString(R.string.enabled) else getString( + R.string.disabled) + ) + else getString(R.string.setup_instance_api_disabled) + binding.instanceCard.instanceCloudflare.visibility = View.VISIBLE + binding.instanceCard.instanceCloudflare.setText(R.string.setup_instance_custom_select) + } + + @SuppressLint("SetTextI18n") + private fun fillError(e: Exception, url: String) { + clearCard() + binding.instanceCard.loading.visibility = View.GONE + binding.instanceCard.instanceTitle.text = getString(R.string.setup_instance_load_fail, url) + binding.instanceCard.instanceDescription.text = "${e.javaClass.name}: ${e.message}" + } + + private fun clearCard() { + binding.instanceCard.root.setOnClickListener { } + binding.instanceCard.loading.visibility = View.VISIBLE + binding.instanceCard.instanceTitle.text = "" + binding.instanceCard.instanceDescription.text = "" + binding.instanceCard.instanceCloudflare.visibility = View.GONE + } + + private fun next(url: String, accountsEnabled: Boolean) { + val sp = requireContext().getSharedPreferences("main", AppCompatActivity.MODE_PRIVATE) + sp.edit { + putString("instanceHost", url) + putBoolean("customInstance", true) + } + (activity as SetupActivity).goToStage(if (accountsEnabled) SetupActivity.STAGE_LOGIN else SetupActivity.STAGE_FINISH) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/fragment/setup/SetupFinishFragment.kt b/app/src/main/java/dev/kuylar/lighttube/ui/fragment/setup/SetupFinishFragment.kt new file mode 100644 index 0000000..e03e0b4 --- /dev/null +++ b/app/src/main/java/dev/kuylar/lighttube/ui/fragment/setup/SetupFinishFragment.kt @@ -0,0 +1,43 @@ +package dev.kuylar.lighttube.ui.fragment.setup + +import android.os.Bundle +import android.text.Html +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import dev.kuylar.lighttube.R +import dev.kuylar.lighttube.databinding.FragmentSetupFinishBinding +import dev.kuylar.lighttube.ui.activity.SetupActivity + +class SetupFinishFragment : Fragment() { + private lateinit var binding: FragmentSetupFinishBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentSetupFinishBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val sp = requireContext().getSharedPreferences("main", AppCompatActivity.MODE_PRIVATE) + + binding.host.text = sp.getString("instanceHost", "") + binding.account.text = if (sp.contains("username")) + Html.fromHtml(getString(R.string.setup_complete_account_loggedin, sp.getString("username", "")), Html.FROM_HTML_MODE_LEGACY) + else + getString(R.string.setup_complete_account_not_loggedin) + + binding.finish.setOnClickListener { + (activity as SetupActivity).finishSetup() + } + binding.back.setOnClickListener { + (activity as SetupActivity).goToStage(SetupActivity.STAGE_LOGIN) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/fragment/setup/SetupInstanceSelectFragment.kt b/app/src/main/java/dev/kuylar/lighttube/ui/fragment/setup/SetupInstanceSelectFragment.kt new file mode 100644 index 0000000..b72a148 --- /dev/null +++ b/app/src/main/java/dev/kuylar/lighttube/ui/fragment/setup/SetupInstanceSelectFragment.kt @@ -0,0 +1,58 @@ +package dev.kuylar.lighttube.ui.fragment.setup + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.edit +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import dev.kuylar.lighttube.api.UtilityApi +import dev.kuylar.lighttube.databinding.FragmentSetupInstanceSelectBinding +import dev.kuylar.lighttube.ui.activity.SetupActivity +import dev.kuylar.lighttube.ui.adapter.InstanceListAdapter +import kotlin.concurrent.thread + +class SetupInstanceSelectFragment : Fragment() { + private lateinit var binding: FragmentSetupInstanceSelectBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentSetupInstanceSelectBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.custom.setOnClickListener { + (activity as SetupActivity).goToStage(SetupActivity.STAGE_INSTANCES_CUSTOM) + } + binding.recyclerInstances.layoutManager = + LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + + thread { + val instances = UtilityApi.getInstances() + activity?.runOnUiThread { + binding.loading.visibility = View.GONE + binding.recyclerInstances.adapter = + InstanceListAdapter(requireActivity(), instances.shuffled()) { + next(it.scheme, it.host, it.accountsEnabled) + } + } + } + } + + fun next(scheme: String, host: String, accountsEnabled: Boolean) { + val sp = requireContext().getSharedPreferences("main", AppCompatActivity.MODE_PRIVATE) + sp.edit { + putString("instanceHost", "$scheme://$host") + putBoolean("customInstance", false) + } + (activity as SetupActivity).goToStage(if (accountsEnabled) SetupActivity.STAGE_LOGIN else SetupActivity.STAGE_FINISH) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/fragment/setup/SetupLoginFragment.kt b/app/src/main/java/dev/kuylar/lighttube/ui/fragment/setup/SetupLoginFragment.kt new file mode 100644 index 0000000..9f14f9a --- /dev/null +++ b/app/src/main/java/dev/kuylar/lighttube/ui/fragment/setup/SetupLoginFragment.kt @@ -0,0 +1,62 @@ +package dev.kuylar.lighttube.ui.fragment.setup + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import dev.kuylar.lighttube.R +import dev.kuylar.lighttube.databinding.FragmentSetupLoginBinding +import dev.kuylar.lighttube.ui.activity.LoginActivity +import dev.kuylar.lighttube.ui.activity.SetupActivity + +class SetupLoginFragment : Fragment() { + private lateinit var binding: FragmentSetupLoginBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentSetupLoginBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val sp = requireContext().getSharedPreferences("main", AppCompatActivity.MODE_PRIVATE) + binding.buttonBack.setOnClickListener { + (activity as SetupActivity).goToStage( + if (sp.getBoolean("customInstance", false)) + SetupActivity.STAGE_INSTANCES_CUSTOM + else + SetupActivity.STAGE_INSTANCES_PUBLIC + ) + } + binding.buttonSkip.setOnClickListener { + (activity as SetupActivity).goToStage(SetupActivity.STAGE_FINISH) + } + val intentLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + (activity as SetupActivity).goToStage(SetupActivity.STAGE_FINISH) + } + } + binding.body.text = getString(R.string.setup_login_body, sp.getString("instanceHost", "")) + binding.buttonRegister.setOnClickListener { + val i = Intent(context, LoginActivity::class.java) + i.putExtra("register", true) + intentLauncher.launch(i) + } + binding.buttonLogin.setOnClickListener { + val i = Intent(context, LoginActivity::class.java) + i.putExtra("register", false) + intentLauncher.launch(i) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/kuylar/lighttube/ui/fragment/setup/SetupWelcomeFragment.kt b/app/src/main/java/dev/kuylar/lighttube/ui/fragment/setup/SetupWelcomeFragment.kt new file mode 100644 index 0000000..de55da8 --- /dev/null +++ b/app/src/main/java/dev/kuylar/lighttube/ui/fragment/setup/SetupWelcomeFragment.kt @@ -0,0 +1,30 @@ +package dev.kuylar.lighttube.ui.fragment.setup + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import dev.kuylar.lighttube.databinding.FragmentSetupWelcomeBinding +import dev.kuylar.lighttube.ui.activity.SetupActivity + +class SetupWelcomeFragment : Fragment() { + private lateinit var binding: FragmentSetupWelcomeBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentSetupWelcomeBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.nextButton.setOnClickListener { + (activity as SetupActivity).goToStage(SetupActivity.STAGE_INSTANCES_PUBLIC) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_setup.xml b/app/src/main/res/layout/activity_setup.xml index d1b8b42..26f435a 100644 --- a/app/src/main/res/layout/activity_setup.xml +++ b/app/src/main/res/layout/activity_setup.xml @@ -1,141 +1,12 @@ - - - - - - - - -