commit c77691edda90b466a889862ab398c03aadd0fff2 Author: yourfriendoss Date: Sat Oct 18 20:02:25 2025 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5cbb64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.aab +*.apk +output-metadata.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2ef0141 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 sad.ovh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..b1411cd --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "ovh.sad.bleh" + compileSdk { + version = release(36) + } + + defaultConfig { + applicationId = "ovh.sad.bleh" + minSdk = 34 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.test.manifest) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm new file mode 100644 index 0000000..d437e55 Binary files /dev/null and b/app/release/baselineProfiles/0/app-release.dm differ diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm new file mode 100644 index 0000000..525809b Binary files /dev/null and b/app/release/baselineProfiles/1/app-release.dm differ diff --git a/app/src/androidTest/java/ovh/sad/bleh/ExampleInstrumentedTest.kt b/app/src/androidTest/java/ovh/sad/bleh/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..1152101 --- /dev/null +++ b/app/src/androidTest/java/ovh/sad/bleh/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package ovh.sad.bleh + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("ovh.sad.bleh", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4238e32 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..ff0e563 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/ovh/sad/bleh/Caching.kt b/app/src/main/java/ovh/sad/bleh/Caching.kt new file mode 100644 index 0000000..b8cbcee --- /dev/null +++ b/app/src/main/java/ovh/sad/bleh/Caching.kt @@ -0,0 +1,81 @@ +package ovh.sad.bleh + +import android.content.Context +import android.content.Context.MODE_PRIVATE +import android.os.Handler +import android.util.Log + +class Caching { + companion object { + // The logger function, defaults to Android Log + var defaultLogger: (String) -> Unit = { msg -> Log.d("Caching", msg) } + private var logger: (String) -> Unit = defaultLogger + + // Set a custom logger + fun setLog(customLog: (String) -> Unit) { + logger = customLog + } + + // Helper to call logger + private fun log(message: String) { + logger(message) + } + + fun getBranch(context: Context): String? { + val sharedPreferences = context.getSharedPreferences("UserPreferences", MODE_PRIVATE) + return if (sharedPreferences.getString("blehBranch", null) != null) { + sharedPreferences.getString("blehBranch", ""); + } else { + "stable"; + } + } + + fun getCachedJsFile(context: Context): java.io.File { + return java.io.File(context.cacheDir, "bleh.user.js") + } + + fun getCachedVersionFile(context: Context): java.io.File { + return java.io.File(context.cacheDir, "bleh.user.js.version") + } + + fun getBlehJs(context: Context): String? { + val cachedFile = getCachedJsFile(context) + val text = if (cachedFile.exists()) cachedFile.readText() else return null + + return text; + } + + fun checkBlehJsUpdate(handler: Handler, context: Context) { + log("Checking bleh updates..") + val branch = getBranch(context) + val buildUrl = + "https://raw.githubusercontent.com/katelyynn/bleh/refs/heads/${if(branch == "stable") "uwu" else branch}/fm/src/build/build.json?${System.currentTimeMillis()}" + val jsUrl = + "https://raw.githubusercontent.com/katelyynn/bleh/refs/heads/${if(branch == "stable") "uwu" else branch}/fm/bleh.user.js?${System.currentTimeMillis()}" + + val cachedFile = getCachedJsFile(context) + val versionFile = getCachedVersionFile(context) + + try { + val buildResult = Utils.fetchHtml(handler, buildUrl) + + val latestVersion = buildResult.html?.let { + Regex("\"build\"\\s*:\\s*\"([^\"]+)\"").find(it)?.groupValues?.get(1) + } + "-" + branch + + var cachedVersion = if (versionFile.exists()) versionFile.readText() else null + log("Cached version: $cachedVersion, latest version: $latestVersion"); + + if (latestVersion != null && latestVersion != cachedVersion) { + val jsResult = Utils.fetchHtml(handler, jsUrl) + if (jsResult.error == null) { + cachedFile.writeText(jsResult.html.orEmpty()) + versionFile.writeText(latestVersion) + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ovh/sad/bleh/InterceptingWebViewClient.kt b/app/src/main/java/ovh/sad/bleh/InterceptingWebViewClient.kt new file mode 100644 index 0000000..677d71e --- /dev/null +++ b/app/src/main/java/ovh/sad/bleh/InterceptingWebViewClient.kt @@ -0,0 +1,153 @@ +package ovh.sad.bleh + +import android.content.Context +import android.graphics.Bitmap +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Toast +import java.io.ByteArrayInputStream +import java.nio.charset.StandardCharsets + +data class FetchResult( + val html: String?, + val error: String? = null, + val statusCode: Int? = null +) + +open class InterceptingWebViewClient(private val context: Context) : WebViewClient() { + open fun log(message: String) { + Log.d("IWVC", message) + } + + private val blockedDomains = arrayOf( + "demdex.net", + "ssa.last.fm", + "googletagmanager.com", + "everestjs.net", + "newrelic.com", + "at.cbsi.com", + "tiqcdn.com", + "siteintercept.qualtrics.com", + "secure-us.imrworldwide.com", + "scorecardresearch.com", + "cookielaw.org", + "cdn.privacy.paramount.com", + "twochihuahuas.com", + "doubleclick.net", + "liadm.com", + "amazon-adsystem.com", + "confiant-integrations.net", + "criteo.com", + "adsrvr.org", + "contextual.media.net", + "adsafeprotected.com", + "merequartz.com", + "googlesyndication.com", + "strangeclocks.com" + ) + + val mainHandler = Handler(Looper.getMainLooper()) + // onPageStarted here exists for one big reason + // + // When logging in for the first time, the client sends a POST request, which we can't really get the response of (cause it's a POST rqeuest, duh!) + // so I set this flag (interceptNextPost), which intercepts the next page load after a POST (which is always the page POST'ed) + private var interceptNextPost = false; + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + if(interceptNextPost) { + val injectedJs = Caching.getBlehJs(context) + if(injectedJs == null) { + mainHandler.post { + Toast.makeText(context, "Something is fucked! injectedJs returned NULL", Toast.LENGTH_SHORT).show() + } + return super.onPageStarted(view, url, favicon) + } + + mainHandler.post { + Toast.makeText(context, "This page load may be slow.", Toast.LENGTH_SHORT).show() + + view?.evaluateJavascript("window.unsafeWindow = window;$injectedJs") { e -> + } + } + + interceptNextPost = false; + } + super.onPageStarted(view, url, favicon) + } + + override fun shouldInterceptRequest( + view: WebView?, + request: WebResourceRequest? + ): WebResourceResponse? { + val req = request ?: return null + val url = req.url.toString() + try { + val host = req.url.host ?: "no host wtf" + + if (blockedDomains.any { host.contains(it, ignoreCase = true) }) { + log("Blocked $host.") + return WebResourceResponse( + "application/javascript", + StandardCharsets.UTF_8.name(), + ByteArrayInputStream.nullInputStream() + ) + } else { + log("Letting past: $host") + } + + if(req.method == "POST" && req.isForMainFrame && (req.url.host == "www.last.fm" || req.url.host == "last.fm")) { + interceptNextPost = true; + } + + if (req.method == "GET" && req.isForMainFrame && (req.url.host == "www.last.fm" || req.url.host == "last.fm")) { + log("Intercepting mainframe..") + + var fetched = Utils.fetchHtml(mainHandler, url, 3, true); + val code = fetched.statusCode + if ((code != 200 && code != 404) || fetched.html == null) { + mainHandler.post { + Toast.makeText(context, "Could not modify HTML. Status code: " + code + ", error: " + fetched.error, Toast.LENGTH_LONG).show() + } + return super.shouldInterceptRequest(view, request) + } + + val injectedJs = Caching.getBlehJs(context); + if(injectedJs == null) { + mainHandler.post { + Toast.makeText(context, "Something is fucked! injectedJs returned NULL", Toast.LENGTH_SHORT).show() + } + return super.shouldInterceptRequest(view, request) + } + val injectedTag = "" + val modifiedHtml = try { + val pattern = Regex("(?i)]*>") + + if (pattern.containsMatchIn(fetched.html)) { + fetched.html.replaceFirst(pattern, "$0$injectedTag") + } else { + injectedTag + fetched.html + } + } catch (_: Exception) { + injectedTag + fetched.html + } + + val bytes = modifiedHtml.toByteArray(StandardCharsets.UTF_8) + return WebResourceResponse( + "text/html", + StandardCharsets.UTF_8.name(), + ByteArrayInputStream(bytes) + ) + } + } catch (e: Exception) { + e.printStackTrace() + } + + return super.shouldInterceptRequest(view, request) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ovh/sad/bleh/MainActivity.kt b/app/src/main/java/ovh/sad/bleh/MainActivity.kt new file mode 100644 index 0000000..973be50 --- /dev/null +++ b/app/src/main/java/ovh/sad/bleh/MainActivity.kt @@ -0,0 +1,61 @@ +package ovh.sad.bleh + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.MotionEvent +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInteropFilter +import ovh.sad.bleh.ui.theme.BlehTheme + +class MainActivity : ComponentActivity() { + + @OptIn(ExperimentalComposeUiApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + var tapCount = 0 + var lastTapTime = 0L + val multiTapThresholdMs = 200L + val requiredTaps = 30 + + setContent { + BlehTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + LastFmWebView( + modifier = Modifier + .padding(innerPadding) + .pointerInteropFilter { event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + val now = System.currentTimeMillis() + if (now - lastTapTime < multiTapThresholdMs) { + tapCount++ + } else { + tapCount = 1 + } + lastTapTime = now + + Log.d("Secret", "Tap #$tapCount") + + if (tapCount >= requiredTaps) { + startActivity(Intent(this@MainActivity, SecretActivity::class.java)) + tapCount = 0 + } + } + } + + false + } + ) + } + } + } + } +} diff --git a/app/src/main/java/ovh/sad/bleh/SecretActivity.kt b/app/src/main/java/ovh/sad/bleh/SecretActivity.kt new file mode 100644 index 0000000..f40a5d8 --- /dev/null +++ b/app/src/main/java/ovh/sad/bleh/SecretActivity.kt @@ -0,0 +1,124 @@ +package ovh.sad.bleh + +import android.content.Context.MODE_PRIVATE +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import ovh.sad.bleh.ui.theme.BlehTheme +import androidx.core.content.edit +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class SecretActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + BlehTheme { + SecretScreen(this) + } + } + } +} + +@Composable +fun SecretScreen(activity: SecretActivity) { + val sharedPreferences = activity.getSharedPreferences("UserPreferences", MODE_PRIVATE) + val tempBranch = sharedPreferences.getString("blehBranch", null); + + var selectedOption by remember { mutableStateOf(if(tempBranch != null) "custom" else "stable") } + var customBranch by remember { mutableStateOf(tempBranch ?: "") } + var errorText by remember { mutableStateOf("") } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text("Select branch", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary) + + RadioButtonRow("Latest stable", MaterialTheme.colorScheme.secondary, "stable", selectedOption) { selectedOption = it } + RadioButtonRow("Custom branch", MaterialTheme.colorScheme.secondary, "custom", selectedOption) { selectedOption = it } + + if (selectedOption == "custom") { + OutlinedTextField( + value = customBranch, + onValueChange = { customBranch = it }, + label = { Text("Input a Custom Branch here", color = MaterialTheme.colorScheme.secondary) }, + singleLine = true, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text) + ) + } + + Button(onClick = { + if (selectedOption == "custom" && customBranch.isBlank()) { + errorText = "Custom branch cannot be empty!" + } else if (selectedOption == "custom" && !isValidBranch(customBranch)) { + errorText = "Custom branch is incorrect!" + } else { + errorText = "" + + val previousState = sharedPreferences.getString("blehBranch", "") + ""; + + sharedPreferences.edit(commit = true) { + if (selectedOption == "custom") { + putString("blehBranch", customBranch) + } else { + if (previousState.isNotEmpty()) { + remove("blehBranch"); + } + } + }; + + if (previousState != sharedPreferences.getString("blehBranch", "")) { + CoroutineScope(Dispatchers.IO).launch { + Caching.checkBlehJsUpdate( + Handler(Looper.getMainLooper()), + activity + ); + } + } + activity.finish(); + } + + }) { + Text("Submit") + } + + if (errorText.isNotEmpty()) { + Text(errorText, color = MaterialTheme.colorScheme.error) + } + } + } +} + +@Composable +fun RadioButtonRow(text: String, textColor: Color, value: String, selected: String, onSelected: (String) -> Unit) { + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = selected == value, + onClick = { onSelected(value) } + ) + Text(text, color = textColor) + } +} + +// Simple branch validation +fun isValidBranch(branch: String) = branch.matches(Regex("^[a-zA-Z0-9_-]+$")) \ No newline at end of file diff --git a/app/src/main/java/ovh/sad/bleh/Utils.kt b/app/src/main/java/ovh/sad/bleh/Utils.kt new file mode 100644 index 0000000..b166b23 --- /dev/null +++ b/app/src/main/java/ovh/sad/bleh/Utils.kt @@ -0,0 +1,131 @@ +package ovh.sad.bleh + +import android.os.Handler +import android.util.Log +import android.webkit.CookieManager +import java.io.BufferedInputStream +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.net.HttpURLConnection +import java.net.URL + + +open class Utils { + + companion object { + // The logger function, defaults to Android Log + var defaultLogger: (String) -> Unit = { msg -> Log.d("Utils", msg) } + private var logger: (String) -> Unit = defaultLogger + + // Set a custom logger + fun setLog(customLog: (String) -> Unit) { + logger = customLog + } + + // Helper to call logger + private fun log(message: String) { + logger(message) + } + + fun fetchHtml( + handler: Handler, + urlString: String, + retries: Int = 3, + cookies: Boolean = false + ): FetchResult { + var conn: HttpURLConnection? = null + var input: InputStream? = null + + val retryableCodes = setOf(429, 500, 502, 503, 504, 406) + val cookieHeader = CookieManager.getInstance().getCookie(urlString) ?: "" + + return try { + val url = URL(urlString) + log("URL: $urlString (retries left: $retries)") + + conn = (url.openConnection() as HttpURLConnection).apply { + requestMethod = "GET" + connectTimeout = 25_000 + readTimeout = 25_000 + instanceFollowRedirects = true + setRequestProperty( + "User-Agent", + "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36" + ) + setRequestProperty( + "Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" + ) + setRequestProperty("Accept-Language", "en-US,en;q=0.9") + setRequestProperty("Connection", "keep-alive") + setRequestProperty("DNT", "1") + setRequestProperty("Upgrade-Insecure-Requests", "1") + setRequestProperty("X-UA-Device-Type", "mobile") + setRequestProperty("X-UA-Country-Code", "LV") + if (cookies && cookieHeader.isNotEmpty()) { + setRequestProperty("Cookie", cookieHeader) + } + } + + val code = conn.responseCode + + if (cookies) { + conn.headerFields["Set-Cookie"]?.forEach { header -> + handler.post { + CookieManager.getInstance().setCookie(urlString, header) { + CookieManager.getInstance().flush() + } + } + } + } + + if (code in retryableCodes) { + log("Got $code, retrying... ($retries left)") + if (retries > 0) { + Thread.sleep(500) + return fetchHtml(handler, urlString, retries - 1, cookies) + } else { + log("$code persisted after retries.") + return FetchResult(html = null, error = "HTTP $code", statusCode = code) + } + } + + input = if (code in 200..299) { + BufferedInputStream(conn.inputStream) + } else { + BufferedInputStream(conn.errorStream ?: ByteArrayInputStream(ByteArray(0))) + } + + val html = readStreamToString(input) + + if (code !in 200..299) { + log("Code $code") + return FetchResult(html = html, error = "HTTP $code", statusCode = code) + } + + log("Downloaded $urlString.") + FetchResult(html = html, statusCode = code) + + } catch (e: Exception) { + log("Exception: ${e.message}") + FetchResult(html = null, error = e.message) + } finally { + try { + input?.close() + } catch (_: Exception) {} + conn?.disconnect() + } + } + + private fun readStreamToString(input: InputStream): String { + val baos = ByteArrayOutputStream() + val buffer = ByteArray(4096) + var read: Int + while (input.read(buffer).also { read = it } != -1) { + baos.write(buffer, 0, read) + } + return baos.toString() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ovh/sad/bleh/WebViewScreen.kt b/app/src/main/java/ovh/sad/bleh/WebViewScreen.kt new file mode 100644 index 0000000..204acd7 --- /dev/null +++ b/app/src/main/java/ovh/sad/bleh/WebViewScreen.kt @@ -0,0 +1,194 @@ +package ovh.sad.bleh + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.webkit.CookieManager as AndroidCookieManager +import android.webkit.WebChromeClient +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Toast +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.BufferedInputStream +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.net.HttpCookie +import java.net.HttpURLConnection +import java.net.URL +import java.nio.charset.StandardCharsets + + +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun LastFmWebView(modifier: Modifier = Modifier) { + val context = LocalContext.current + var isLoading by remember { mutableStateOf(true) } + var currentLog by remember { mutableStateOf("Loading.. ") } + + val rotation by rememberInfiniteTransition().animateFloat( + initialValue = 0f, + targetValue = 360f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 1500, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ) + ) + + Box(modifier = modifier.fillMaxSize()) { + AndroidView( + factory = { ctx -> + WebView(ctx).apply { + layoutParams = android.view.ViewGroup.LayoutParams( + android.view.ViewGroup.LayoutParams.MATCH_PARENT, + android.view.ViewGroup.LayoutParams.MATCH_PARENT + ) + + AndroidCookieManager.getInstance().setAcceptCookie(true) + AndroidCookieManager.getInstance().setAcceptThirdPartyCookies(this, true) + + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + fitsSystemWindows = true + + ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets -> + val sys = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.updatePadding( + left = sys.left, + top = sys.top, + right = sys.right, + bottom = sys.bottom + ) + insets + } + + ViewCompat.requestApplyInsets(this) + + var iwvc = object : InterceptingWebViewClient(context) { + override fun log(message: String) { + currentLog = "Loading.. (iwvc)\n$message"; + super.log(message) + } + } + + webViewClient = iwvc; + + webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + if (newProgress < 100) isLoading = true + if(newProgress == 100) { + isLoading = false; + } + } + } + + Caching.setLog { e -> + Caching.defaultLogger(e); + currentLog = "Loading.. (caching)\n$e"; + } + + Utils.setLog { e -> + Utils.defaultLogger(e); + currentLog = "Loading.. (utils)\n$e"; + } + + WebView.setWebContentsDebuggingEnabled(true) + + CoroutineScope(Dispatchers.IO).launch { + Caching.checkBlehJsUpdate(iwvc.mainHandler, ctx); + ctx.mainExecutor.execute { + loadUrl("https://www.last.fm/login") + } + } + } + }, + update = { } + ) + + if (isLoading) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.hsl(0f, 0f, 0.13f)), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .size(96.dp) + .rotate(rotation) + .clip(CircleShape) + ) { + Image( + painter = painterResource(id = R.drawable.ic_launcher_background), + contentDescription = null, + modifier = Modifier.matchParentSize() + ) + Image( + painter = painterResource(id = R.drawable.ic_launcher_foreground), + contentDescription = "Loading", + modifier = Modifier.matchParentSize() + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + Text( + modifier = Modifier.widthIn(max = 200.dp), + softWrap = true, + text = currentLog, + textAlign = TextAlign.Center, + color = Color.White + ) + } + } + } + } +} diff --git a/app/src/main/java/ovh/sad/bleh/ui/theme/Color.kt b/app/src/main/java/ovh/sad/bleh/ui/theme/Color.kt new file mode 100644 index 0000000..9ac2658 --- /dev/null +++ b/app/src/main/java/ovh/sad/bleh/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package ovh.sad.bleh.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650A4) +val PurpleGrey40 = Color(0xFF625B71) +val Pink40 = Color(0xFF7D5260) diff --git a/app/src/main/java/ovh/sad/bleh/ui/theme/Theme.kt b/app/src/main/java/ovh/sad/bleh/ui/theme/Theme.kt new file mode 100644 index 0000000..ee6fd7a --- /dev/null +++ b/app/src/main/java/ovh/sad/bleh/ui/theme/Theme.kt @@ -0,0 +1,95 @@ +package ovh.sad.bleh.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext + +// Light theme colors +val LightColors = lightColorScheme( + primary = Purple40, + onPrimary = Color.White, + primaryContainer = Purple80, + onPrimaryContainer = Color.Black, + secondary = PurpleGrey40, + onSecondary = Color.White, + secondaryContainer = PurpleGrey80, + onSecondaryContainer = Color.Black, + tertiary = Pink40, + onTertiary = Color.White, + tertiaryContainer = Pink80, + onTertiaryContainer = Color.Black, + background = Color(0xFFF5F5F5), + onBackground = Color(0xFF1C1B1F), + surface = Color.White, + onSurface = Color(0xFF1C1B1F), + error = Color(0xFFB00020), + onError = Color.White, + surfaceVariant = Color(0xFFE7E0EC), + onSurfaceVariant = Color(0xFF49454F), + outline = Color(0xFF79747E), + inverseOnSurface = Color.White, + inverseSurface = Color(0xFF313033), + inversePrimary = Purple80, + scrim = Color.Black +) + +// Dark theme colors +val DarkColors = darkColorScheme( + primary = Purple80, + onPrimary = Color.Black, + primaryContainer = Purple40, + onPrimaryContainer = Color.White, + secondary = PurpleGrey80, + onSecondary = Color.Black, + secondaryContainer = PurpleGrey40, + onSecondaryContainer = Color.White, + tertiary = Pink80, + onTertiary = Color.Black, + tertiaryContainer = Pink40, + onTertiaryContainer = Color.White, + background = Color(0xFF1C1B1F), + onBackground = Color(0xFFE6E1E5), + surface = Color(0xFF1C1B1F), + onSurface = Color(0xFFE6E1E5), + error = Color(0xFFCF6679), + onError = Color.Black, + surfaceVariant = Color(0xFF49454F), + onSurfaceVariant = Color(0xFFCAC4D0), + outline = Color(0xFF938F99), + inverseOnSurface = Color(0xFF1C1B1F), + inverseSurface = Color(0xFFE6E1E5), + inversePrimary = Purple40, + scrim = Color.Black +) + +@Composable +fun BlehTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColors + else -> LightColors + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/ovh/sad/bleh/ui/theme/Type.kt b/app/src/main/java/ovh/sad/bleh/ui/theme/Type.kt new file mode 100644 index 0000000..9af3b7d --- /dev/null +++ b/app/src/main/java/ovh/sad/bleh/ui/theme/Type.kt @@ -0,0 +1,54 @@ +package ovh.sad.bleh.ui.theme + + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val Typography = Typography( + displayLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = (-0.25).sp + ), + displayMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 45.sp, + lineHeight = 52.sp + ), + displaySmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 36.sp, + lineHeight = 44.sp + ), + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + lineHeight = 28.sp + ), + titleMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 24.sp + ), + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp + ), + labelLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp + ) +) \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..27c77fe --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..7ddce00 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..bbd3e02 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..bbd3e02 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..e53f766 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..033a46d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..95eeda0 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..81a7196 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..c6d1639 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..eea6069 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..caefcc4 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..399c27a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..ba2e16e Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b73050c Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..483e71d --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + bleh + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..7d22f78 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +