From 9b90e6a4a1f3fc60a37c61d94d28a3d9dd0e76dc Mon Sep 17 00:00:00 2001 From: Mathis Pruvot Date: Thu, 28 May 2026 11:50:19 +0000 Subject: [PATCH] fix: include native module sources in git tracking Fix .gitignore to only exclude root android/ and ios/ (Expo prebuild output) while keeping modules/ native code. Add Android WallpaperService Kotlin sources, GLSL shaders, manifest, and Gradle config. --- .gitignore | 4 +- .../android/build.gradle.kts | 27 ++ .../android/src/main/AndroidManifest.xml | 24 + .../lively/wallpaper/LiveWallpaperService.kt | 189 ++++++++ .../com/lively/wallpaper/ShaderRenderer.kt | 438 ++++++++++++++++++ .../wallpaper/WallpaperAndroidModule.kt | 59 +++ .../android/src/main/res/xml/wallpaper.xml | 5 + 7 files changed, 744 insertions(+), 2 deletions(-) create mode 100644 modules/wallpaper-android/android/build.gradle.kts create mode 100644 modules/wallpaper-android/android/src/main/AndroidManifest.xml create mode 100644 modules/wallpaper-android/android/src/main/java/com/lively/wallpaper/LiveWallpaperService.kt create mode 100644 modules/wallpaper-android/android/src/main/java/com/lively/wallpaper/ShaderRenderer.kt create mode 100644 modules/wallpaper-android/android/src/main/java/com/lively/wallpaper/WallpaperAndroidModule.kt create mode 100644 modules/wallpaper-android/android/src/main/res/xml/wallpaper.xml diff --git a/.gitignore b/.gitignore index d57ac62..4f29717 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ node_modules/ .expo/ -android/ -ios/ +/android/ +/ios/ dist/ .env *.jks diff --git a/modules/wallpaper-android/android/build.gradle.kts b/modules/wallpaper-android/android/build.gradle.kts new file mode 100644 index 0000000..802db39 --- /dev/null +++ b/modules/wallpaper-android/android/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.lively.wallpaper" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } +} + +dependencies { + implementation(project(":expo-modules-core")) + implementation("org.jetbrains.kotlin:kotlin-stdlib") +} diff --git a/modules/wallpaper-android/android/src/main/AndroidManifest.xml b/modules/wallpaper-android/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..15c1a11 --- /dev/null +++ b/modules/wallpaper-android/android/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/modules/wallpaper-android/android/src/main/java/com/lively/wallpaper/LiveWallpaperService.kt b/modules/wallpaper-android/android/src/main/java/com/lively/wallpaper/LiveWallpaperService.kt new file mode 100644 index 0000000..e2a4775 --- /dev/null +++ b/modules/wallpaper-android/android/src/main/java/com/lively/wallpaper/LiveWallpaperService.kt @@ -0,0 +1,189 @@ +package com.lively.wallpaper + +import android.content.Context +import android.content.SharedPreferences +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.opengl.GLSurfaceView +import android.os.Handler +import android.os.Looper +import android.service.wallpaper.WallpaperService +import android.view.SurfaceHolder +import org.json.JSONObject +import javax.microedition.khronos.egl.EGLConfig +import javax.microedition.khronos.opengles.GL10 + +/** + * Live Wallpaper service that renders animated shaders via OpenGL ES 2.0. + * + * Reads wallpaper config from SharedPreferences (written by WallpaperAndroidModule). + * Config format: { shaderId, imagePath, depthMapPath?, uniforms: { intensity, speed, direction } } + */ +class LiveWallpaperService : WallpaperService() { + + override fun onCreateEngine(): Engine = LiveEngine() + + inner class LiveEngine : WallpaperService.Engine(), SensorEventListener { + + private var renderer: ShaderRenderer? = null + private var glSurface: WallpaperGLSurfaceView? = null + + // Config + private var shaderId = "ken-burns" + private var imagePath = "" + private var depthMapPath: String? = null + private var intensity = 0.5f + private var speed = 0.3f + private var direction = 0f + private var targetFps = 24 + + // State + private var startTime = 0L + private var gyroX = 0f + private var gyroY = 0f + private var isVisible = false + + // Sensors + private var sensorManager: SensorManager? = null + private var gyroscope: Sensor? = null + + // Render loop + private val handler = Handler(Looper.getMainLooper()) + private val renderRunnable = object : Runnable { + override fun run() { + if (isVisible) { + glSurface?.requestRender() + handler.postDelayed(this, (1000L / targetFps)) + } + } + } + + override fun onCreate(surfaceHolder: SurfaceHolder) { + super.onCreate(surfaceHolder) + setTouchEventsEnabled(false) + + loadConfig() + + sensorManager = getSystemService(Context.SENSOR_SERVICE) as? SensorManager + gyroscope = sensorManager?.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + } + + override fun onSurfaceCreated(holder: SurfaceHolder) { + super.onSurfaceCreated(holder) + + glSurface = WallpaperGLSurfaceView(this@LiveWallpaperService).also { view -> + view.setEGLContextClientVersion(2) + view.setRenderer(WallpaperRenderer()) + view.renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY + } + + startTime = System.currentTimeMillis() + } + + override fun onVisibilityChanged(visible: Boolean) { + super.onVisibilityChanged(visible) + isVisible = visible + + if (visible) { + handler.post(renderRunnable) + gyroscope?.let { + sensorManager?.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) + } + } else { + handler.removeCallbacks(renderRunnable) + sensorManager?.unregisterListener(this) + } + } + + override fun onSurfaceDestroyed(holder: SurfaceHolder) { + super.onSurfaceDestroyed(holder) + isVisible = false + handler.removeCallbacks(renderRunnable) + sensorManager?.unregisterListener(this) + renderer?.release() + renderer = null + } + + override fun onSensorChanged(event: SensorEvent?) { + event?.let { + if (it.sensor.type == Sensor.TYPE_GYROSCOPE) { + // Accumulate with damping + gyroX += it.values[0] * 0.05f + gyroY += it.values[1] * 0.05f + // Damping toward zero + gyroX *= 0.95f + gyroY *= 0.95f + } + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} + + private fun loadConfig() { + val prefs = getSharedPreferences("lively_wallpaper", Context.MODE_PRIVATE) + val configStr = prefs.getString("config", null) ?: return + + try { + val json = JSONObject(configStr) + shaderId = json.optString("animation", "ken-burns") + imagePath = json.optString("sourceUri", "") + depthMapPath = json.optString("depthMapUri", null) + targetFps = json.optInt("fps", 24) + + val uniforms = json.optJSONObject("uniforms") + if (uniforms != null) { + intensity = uniforms.optDouble("intensity", 0.5).toFloat() + speed = uniforms.optDouble("speed", 0.3).toFloat() + direction = uniforms.optDouble("direction", 0.0).toFloat() + } + } catch (e: Exception) { + // Fallback to defaults on parse error + } + } + + inner class WallpaperRenderer : GLSurfaceView.Renderer { + override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) { + renderer = ShaderRenderer().also { r -> + r.initialize(shaderId) + if (imagePath.isNotEmpty()) { + r.loadTexture(imagePath) + } + depthMapPath?.let { path -> + if (path.isNotEmpty()) r.loadDepthTexture(path) + } + } + } + + override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) { + android.opengl.GLES20.glViewport(0, 0, width, height) + } + + override fun onDrawFrame(gl: GL10?) { + val elapsed = (System.currentTimeMillis() - startTime) / 1000f + val holder = surfaceHolder + val width = holder.surfaceFrame.width() + val height = holder.surfaceFrame.height() + + renderer?.draw( + width = width, + height = height, + time = elapsed, + intensity = intensity, + speed = speed, + direction = direction, + gyroX = gyroX, + gyroY = gyroY + ) + } + } + + /** + * Custom GLSurfaceView that uses the WallpaperService's SurfaceHolder. + */ + inner class WallpaperGLSurfaceView(context: Context) : GLSurfaceView(context) { + override fun getHolder(): SurfaceHolder = this@LiveEngine.surfaceHolder + } + } +} diff --git a/modules/wallpaper-android/android/src/main/java/com/lively/wallpaper/ShaderRenderer.kt b/modules/wallpaper-android/android/src/main/java/com/lively/wallpaper/ShaderRenderer.kt new file mode 100644 index 0000000..859d2ec --- /dev/null +++ b/modules/wallpaper-android/android/src/main/java/com/lively/wallpaper/ShaderRenderer.kt @@ -0,0 +1,438 @@ +package com.lively.wallpaper + +import android.opengl.GLES20 +import android.opengl.GLUtils +import android.graphics.BitmapFactory +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.FloatBuffer + +/** + * OpenGL ES 2.0 renderer that applies fragment shaders to a texture. + * Each animation type maps to a GLSL fragment shader equivalent to the SkSL version. + */ +class ShaderRenderer { + + private var programId = 0 + private var textureId = 0 + private var depthTextureId = 0 + private var vertexBuffer: FloatBuffer? = null + + // Uniform locations + private var uResolution = -1 + private var uTime = -1 + private var uIntensity = -1 + private var uSpeed = -1 + private var uDirection = -1 + private var uImage = -1 + private var uDepthMap = -1 + private var uGyroX = -1 + private var uGyroY = -1 + private var uParticleType = -1 + + private val vertexShaderCode = """ + attribute vec4 aPosition; + attribute vec2 aTexCoord; + varying vec2 vTexCoord; + void main() { + gl_Position = aPosition; + vTexCoord = aTexCoord; + } + """.trimIndent() + + // Fullscreen quad vertices (position + texcoord) + private val quadVertices = floatArrayOf( + // X, Y, U, V + -1.0f, -1.0f, 0.0f, 1.0f, + 1.0f, -1.0f, 1.0f, 1.0f, + -1.0f, 1.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 0.0f, + ) + + fun initialize(shaderId: String) { + val bb = ByteBuffer.allocateDirect(quadVertices.size * 4) + bb.order(ByteOrder.nativeOrder()) + vertexBuffer = bb.asFloatBuffer().apply { + put(quadVertices) + position(0) + } + + val fragmentShaderCode = getFragmentShader(shaderId) + programId = createProgram(vertexShaderCode, fragmentShaderCode) + + GLES20.glUseProgram(programId) + + uResolution = GLES20.glGetUniformLocation(programId, "uResolution") + uTime = GLES20.glGetUniformLocation(programId, "uTime") + uIntensity = GLES20.glGetUniformLocation(programId, "uIntensity") + uSpeed = GLES20.glGetUniformLocation(programId, "uSpeed") + uDirection = GLES20.glGetUniformLocation(programId, "uDirection") + uImage = GLES20.glGetUniformLocation(programId, "uImage") + uDepthMap = GLES20.glGetUniformLocation(programId, "uDepthMap") + uGyroX = GLES20.glGetUniformLocation(programId, "uGyroX") + uGyroY = GLES20.glGetUniformLocation(programId, "uGyroY") + uParticleType = GLES20.glGetUniformLocation(programId, "uParticleType") + } + + fun loadTexture(imagePath: String): Boolean { + val bitmap = BitmapFactory.decodeFile(imagePath) ?: return false + + val textures = IntArray(1) + GLES20.glGenTextures(1, textures, 0) + textureId = textures[0] + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE) + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0) + bitmap.recycle() + + return true + } + + fun loadDepthTexture(depthMapPath: String): Boolean { + val bitmap = BitmapFactory.decodeFile(depthMapPath) ?: return false + + val textures = IntArray(1) + GLES20.glGenTextures(1, textures, 0) + depthTextureId = textures[0] + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, depthTextureId) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE) + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE) + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0) + bitmap.recycle() + + return true + } + + fun draw( + width: Int, + height: Int, + time: Float, + intensity: Float, + speed: Float, + direction: Float, + gyroX: Float = 0f, + gyroY: Float = 0f, + particleType: Float = 0f + ) { + GLES20.glUseProgram(programId) + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) + + // Bind image texture to unit 0 + GLES20.glActiveTexture(GLES20.GL_TEXTURE0) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId) + if (uImage >= 0) GLES20.glUniform1i(uImage, 0) + + // Bind depth map to unit 1 (if available) + if (depthTextureId != 0) { + GLES20.glActiveTexture(GLES20.GL_TEXTURE1) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, depthTextureId) + if (uDepthMap >= 0) GLES20.glUniform1i(uDepthMap, 1) + } + + // Set uniforms + if (uResolution >= 0) GLES20.glUniform2f(uResolution, width.toFloat(), height.toFloat()) + if (uTime >= 0) GLES20.glUniform1f(uTime, time) + if (uIntensity >= 0) GLES20.glUniform1f(uIntensity, intensity) + if (uSpeed >= 0) GLES20.glUniform1f(uSpeed, speed) + if (uDirection >= 0) GLES20.glUniform1f(uDirection, direction) + if (uGyroX >= 0) GLES20.glUniform1f(uGyroX, gyroX) + if (uGyroY >= 0) GLES20.glUniform1f(uGyroY, gyroY) + if (uParticleType >= 0) GLES20.glUniform1f(uParticleType, particleType) + + // Draw fullscreen quad + val posHandle = GLES20.glGetAttribLocation(programId, "aPosition") + val texHandle = GLES20.glGetAttribLocation(programId, "aTexCoord") + + vertexBuffer?.let { buf -> + buf.position(0) + GLES20.glEnableVertexAttribArray(posHandle) + GLES20.glVertexAttribPointer(posHandle, 2, GLES20.GL_FLOAT, false, 16, buf) + + buf.position(2) + GLES20.glEnableVertexAttribArray(texHandle) + GLES20.glVertexAttribPointer(texHandle, 2, GLES20.GL_FLOAT, false, 16, buf) + } + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) + + GLES20.glDisableVertexAttribArray(posHandle) + GLES20.glDisableVertexAttribArray(texHandle) + } + + fun release() { + if (programId != 0) { + GLES20.glDeleteProgram(programId) + programId = 0 + } + if (textureId != 0) { + GLES20.glDeleteTextures(1, intArrayOf(textureId), 0) + textureId = 0 + } + if (depthTextureId != 0) { + GLES20.glDeleteTextures(1, intArrayOf(depthTextureId), 0) + depthTextureId = 0 + } + } + + private fun createProgram(vertexSource: String, fragmentSource: String): Int { + val vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, vertexSource) + val fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource) + + val program = GLES20.glCreateProgram() + GLES20.glAttachShader(program, vertexShader) + GLES20.glAttachShader(program, fragmentShader) + GLES20.glLinkProgram(program) + + val linkStatus = IntArray(1) + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0) + if (linkStatus[0] == 0) { + val log = GLES20.glGetProgramInfoLog(program) + GLES20.glDeleteProgram(program) + throw RuntimeException("Program link failed: $log") + } + + GLES20.glDeleteShader(vertexShader) + GLES20.glDeleteShader(fragmentShader) + + return program + } + + private fun compileShader(type: Int, source: String): Int { + val shader = GLES20.glCreateShader(type) + GLES20.glShaderSource(shader, source) + GLES20.glCompileShader(shader) + + val compileStatus = IntArray(1) + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0) + if (compileStatus[0] == 0) { + val log = GLES20.glGetShaderInfoLog(shader) + GLES20.glDeleteShader(shader) + throw RuntimeException("Shader compile failed: $log") + } + + return shader + } + + companion object { + /** + * Maps animation IDs to GLSL fragment shaders. + * These are GLSL ES 2.0 equivalents of the SkSL shaders in src/shaders/. + */ + fun getFragmentShader(shaderId: String): String = when (shaderId) { + "ken-burns" -> """ + precision mediump float; + varying vec2 vTexCoord; + uniform sampler2D uImage; + uniform vec2 uResolution; + uniform float uTime; + uniform float uIntensity; + uniform float uSpeed; + uniform float uDirection; + + void main() { + float t = uTime * uSpeed * 0.1; + float zoom = 1.0 + uIntensity * 0.15 * (0.5 + 0.5 * sin(t)); + float panX = cos(uDirection) * uIntensity * 0.05 * sin(t * 0.7); + float panY = sin(uDirection) * uIntensity * 0.05 * cos(t * 0.7); + vec2 center = vec2(0.5); + vec2 uv = (vTexCoord - center) / zoom + center; + uv += vec2(panX, panY); + uv = clamp(uv, 0.0, 1.0); + gl_FragColor = texture2D(uImage, uv); + } + """.trimIndent() + + "color-shift" -> """ + precision mediump float; + varying vec2 vTexCoord; + uniform sampler2D uImage; + uniform float uTime; + uniform float uIntensity; + uniform float uSpeed; + + vec3 rgb2hsl(vec3 c) { + float mx = max(c.r, max(c.g, c.b)); + float mn = min(c.r, min(c.g, c.b)); + float l = (mx + mn) * 0.5; + float s = 0.0, h = 0.0; + if (mx != mn) { + float d = mx - mn; + s = l > 0.5 ? d / (2.0 - mx - mn) : d / (mx + mn); + if (mx == c.r) h = (c.g - c.b) / d + (c.g < c.b ? 6.0 : 0.0); + else if (mx == c.g) h = (c.b - c.r) / d + 2.0; + else h = (c.r - c.g) / d + 4.0; + h /= 6.0; + } + return vec3(h, s, l); + } + + float hue2rgb(float p, float q, float t) { + float tt = t; + if (tt < 0.0) tt += 1.0; + if (tt > 1.0) tt -= 1.0; + if (tt < 1.0/6.0) return p + (q - p) * 6.0 * tt; + if (tt < 1.0/2.0) return q; + if (tt < 2.0/3.0) return p + (q - p) * (2.0/3.0 - tt) * 6.0; + return p; + } + + vec3 hsl2rgb(vec3 hsl) { + if (hsl.y == 0.0) return vec3(hsl.z); + float q = hsl.z < 0.5 ? hsl.z * (1.0 + hsl.y) : hsl.z + hsl.y - hsl.z * hsl.y; + float p = 2.0 * hsl.z - q; + return vec3(hue2rgb(p,q,hsl.x+1.0/3.0), hue2rgb(p,q,hsl.x), hue2rgb(p,q,hsl.x-1.0/3.0)); + } + + void main() { + vec4 color = texture2D(uImage, vTexCoord); + vec3 hsl = rgb2hsl(color.rgb); + float t = uTime * uSpeed * 0.15; + hsl.x = fract(hsl.x + uIntensity * 0.1 * sin(t)); + hsl.y = clamp(hsl.y + uIntensity * 0.15 * sin(t * 1.3), 0.0, 1.0); + hsl.z = clamp(hsl.z + uIntensity * 0.03 * sin(t * 0.8), 0.0, 1.0); + gl_FragColor = vec4(hsl2rgb(hsl), color.a); + } + """.trimIndent() + + "particles" -> """ + precision mediump float; + varying vec2 vTexCoord; + uniform sampler2D uImage; + uniform vec2 uResolution; + uniform float uTime; + uniform float uIntensity; + uniform float uSpeed; + uniform float uParticleType; + + float hash(vec2 p) { + vec3 p3 = fract(vec3(p.xyx) * 0.1031); + p3 += dot(p3, p3.yzx + 33.33); + return fract((p3.x + p3.y) * p3.z); + } + + float snow(vec2 uv, float layer) { + float t = uTime * uSpeed * 0.4 + layer * 12.345; + vec2 grid = floor(uv * (8.0 + layer * 6.0)); + vec2 f = fract(uv * (8.0 + layer * 6.0)); + float rnd = hash(grid); + vec2 center = vec2(0.5 + 0.3*sin(t+rnd*6.28), fract(rnd+t*(0.1+rnd*0.08))); + float d = length(f - center); + float size = 0.04 + rnd * 0.06; + return smoothstep(size, size*0.3, d) * (0.4+0.6*rnd); + } + + float bokeh(vec2 uv, float layer) { + float t = uTime * uSpeed * 0.2 + layer * 5.67; + vec2 grid = floor(uv * (3.0 + layer * 2.0)); + vec2 f = fract(uv * (3.0 + layer * 2.0)); + float rnd = hash(grid); + vec2 center = vec2(0.5+0.2*sin(t*0.5+rnd*6.28), 0.5+0.2*cos(t*0.3+rnd*6.28)); + float d = length(f - center); + float size = 0.1 + rnd * 0.15; + float ring = smoothstep(size,size-0.02,d) - smoothstep(size-0.03,size-0.06,d); + float fill = smoothstep(size,size*0.2,d) * 0.3; + return (ring+fill) * (0.3+0.7*rnd) * step(0.3, rnd); + } + + void main() { + vec4 color = texture2D(uImage, vTexCoord); + float particles = 0.0; + for (float i = 0.0; i < 3.0; i += 1.0) { + if (uParticleType < 0.5) particles += snow(vTexCoord, i); + else particles += bokeh(vTexCoord, i); + } + particles *= uIntensity; + gl_FragColor = vec4(clamp(color.rgb + vec3(particles), 0.0, 1.0), color.a); + } + """.trimIndent() + + "vignette-pulse" -> """ + precision mediump float; + varying vec2 vTexCoord; + uniform sampler2D uImage; + uniform float uTime; + uniform float uIntensity; + uniform float uSpeed; + + void main() { + vec4 color = texture2D(uImage, vTexCoord); + vec2 center = vTexCoord - 0.5; + float dist = length(center); + float t = uTime * uSpeed * 0.3; + float strength = uIntensity * (0.6 + 0.4 * sin(t)); + float vignette = 1.0 - smoothstep(0.2, 0.85, dist * (1.0 + strength)); + vec3 tinted = color.rgb * vignette; + float tint = (1.0 - vignette) * 0.15 * uIntensity; + tinted.b += tint * 0.3; + gl_FragColor = vec4(clamp(tinted, 0.0, 1.0), color.a); + } + """.trimIndent() + + "glitch" -> """ + precision mediump float; + varying vec2 vTexCoord; + uniform sampler2D uImage; + uniform vec2 uResolution; + uniform float uTime; + uniform float uIntensity; + uniform float uSpeed; + + float hash(float n) { return fract(sin(n) * 43758.5453); } + + void main() { + float t = uTime * uSpeed * 0.5; + float trigger = step(0.92, sin(t*2.0)*sin(t*7.3)); + float amount = uIntensity * trigger; + float shift = uIntensity * 0.003 + amount * 0.015; + vec2 uvR = vec2(vTexCoord.x + shift, vTexCoord.y); + vec2 uvB = vec2(vTexCoord.x - shift, vTexCoord.y); + uvR = clamp(uvR, 0.0, 1.0); + uvB = clamp(uvB, 0.0, 1.0); + float r = texture2D(uImage, uvR).r; + float g = texture2D(uImage, vTexCoord).g; + float b = texture2D(uImage, uvB).b; + gl_FragColor = vec4(r, g, b, 1.0); + } + """.trimIndent() + + "depth-parallax" -> """ + precision mediump float; + varying vec2 vTexCoord; + uniform sampler2D uImage; + uniform sampler2D uDepthMap; + uniform vec2 uResolution; + uniform float uTime; + uniform float uIntensity; + uniform float uSpeed; + uniform float uGyroX; + uniform float uGyroY; + + void main() { + float depth = texture2D(uDepthMap, vTexCoord).r; + float t = uTime * uSpeed * 0.2; + vec2 offset = vec2( + uGyroX * 0.8 + sin(t) * 0.3, + uGyroY * 0.8 + cos(t * 0.7) * 0.3 + ); + vec2 displacement = offset * depth * uIntensity * 0.04; + vec2 uv = clamp(vTexCoord + displacement, 0.0, 1.0); + gl_FragColor = texture2D(uImage, uv); + } + """.trimIndent() + + else -> """ + precision mediump float; + varying vec2 vTexCoord; + uniform sampler2D uImage; + void main() { gl_FragColor = texture2D(uImage, vTexCoord); } + """.trimIndent() + } + } +} diff --git a/modules/wallpaper-android/android/src/main/java/com/lively/wallpaper/WallpaperAndroidModule.kt b/modules/wallpaper-android/android/src/main/java/com/lively/wallpaper/WallpaperAndroidModule.kt new file mode 100644 index 0000000..4abce00 --- /dev/null +++ b/modules/wallpaper-android/android/src/main/java/com/lively/wallpaper/WallpaperAndroidModule.kt @@ -0,0 +1,59 @@ +package com.lively.wallpaper + +import android.app.WallpaperManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition +import org.json.JSONObject + +class WallpaperAndroidModule : Module() { + + override fun definition() = ModuleDefinition { + Name("WallpaperAndroid") + + /** + * Save wallpaper configuration to SharedPreferences. + * The LiveWallpaperService reads this on start. + */ + AsyncFunction("saveConfig") { configJson: String -> + val context = appContext.reactContext ?: throw Exception("Context unavailable") + val prefs = context.getSharedPreferences("lively_wallpaper", Context.MODE_PRIVATE) + prefs.edit().putString("config", configJson).apply() + } + + /** + * Launch the system live wallpaper picker with our service pre-selected. + */ + AsyncFunction("setLiveWallpaper") { + val context = appContext.reactContext ?: throw Exception("Context unavailable") + val intent = Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER).apply { + putExtra( + WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT, + ComponentName(context.packageName, LiveWallpaperService::class.java.name) + ) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + } + + /** + * Check if live wallpapers are supported on this device. + */ + Function("isLiveWallpaperSupported") { + val context = appContext.reactContext ?: return@Function false + val pm = context.packageManager + pm.hasSystemFeature("android.software.live_wallpaper") + } + + /** + * Get the currently active wallpaper config (if it's ours). + */ + Function("getCurrentConfig") { + val context = appContext.reactContext ?: return@Function null + val prefs = context.getSharedPreferences("lively_wallpaper", Context.MODE_PRIVATE) + prefs.getString("config", null) + } + } +} diff --git a/modules/wallpaper-android/android/src/main/res/xml/wallpaper.xml b/modules/wallpaper-android/android/src/main/res/xml/wallpaper.xml new file mode 100644 index 0000000..da4cb50 --- /dev/null +++ b/modules/wallpaper-android/android/src/main/res/xml/wallpaper.xml @@ -0,0 +1,5 @@ + +