Compare commits

6 Commits

Author SHA1 Message Date
Acid 07884c2728 added fdroid badge 2026-06-25 02:37:07 -04:00
Acid cd24d122c3 separated style to css files 2026-06-25 02:25:38 -04:00
Acid 347eaefbf5 release on F-droid 2026-05-08 11:39:26 -04:00
Acid e373c7f337 modified: index.html 2026-04-18 02:44:14 -04:00
Acid ad7f1c7481 add screenshots, icon, and update page
- add phoneScreenshots/s1-s3.jpeg and icon.png
- add screenshots section to index.html
- fix target API: 34 → 35

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 02:14:46 -04:00
Acid 9dd8990303 pages initial 2026-03-24 17:07:29 -04:00
41 changed files with 1246 additions and 1420 deletions
-58
View File
@@ -1,58 +0,0 @@
# stims
An Android app that keeps your screen awake while specific apps are in the foreground.
## What it does
Stims lets you select apps that should prevent your screen from turning off. When a "stimmed" app is active in the foreground, the screen stays on at full brightness. When you switch to a different app, the screen resumes normal sleep behavior.
---
## Permissions
Stims requires **Usage Access** permission to detect which app is in the foreground so it can keep the phone awake.
On first launch the app will redirect you to the system settings screen. To enable it:
1. Go to **Settings → Apps → Special app access → Usage access**
2. Find **Stims** in the list and toggle it on
3. Return to the app
Without this permission the background service cannot detect foreground apps and the screen will not be kept awake.
---
# Building from source
**Requirements**
- Android Studio or the Android SDK command-line tools
- JDK 8+
**Steps**
```bash
git clone https://github.com/acidburnmonkey/stims.git
cd stims
./gradlew assembleDebug
```
The APK will be output to:
```
app/build/outputs/apk/debug/app-debug.apk
```
For a release build:
```bash
./gradlew assembleRelease
# output: app/build/outputs/apk/release/app-release-unsigned.apk
```
---
## Compatibility
- **Minimum:** Android 7.0 (Nougat, API 24)
- **Target:** Android 14 (API 34)
-71
View File
@@ -1,71 +0,0 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'acidburn.stims'
compileSdk 34
defaultConfig {
applicationId "acidburn.stims"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.8'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'androidx.activity:activity-compose:1.8.2'
implementation platform('androidx.compose:compose-bom:2023.10.01')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.material:material-icons-extended'
// Added for XML theme support
implementation 'com.google.android.material:material:1.11.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2023.10.01')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
}
-40
View File
@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Required to query all installed apps on Android 11+ -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Stims">
<service
android:name=".StimsService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="specialUse">
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Monitoring apps to keep screen on" />
</service>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -1,297 +0,0 @@
package acidburn.stims
import android.app.AppOpsManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.os.Process
import android.provider.Settings
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Search
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.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.drawable.toBitmap
import acidburn.stims.ui.theme.StimsTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
data class AppInfo(
val name: String,
val packageName: String,
val icon: Drawable? = null
)
class MainActivity : ComponentActivity() {
private lateinit var prefs: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prefs = getSharedPreferences("stims_prefs", Context.MODE_PRIVATE)
setContent {
StimsTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AppListScreen(prefs)
}
}
}
}
override fun onResume() {
super.onResume()
if (!hasUsageStatsPermission(this)) {
Toast.makeText(this, "Please enable Usage Stats permission", Toast.LENGTH_LONG).show()
startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
}
}
private fun hasUsageStatsPermission(context: Context): Boolean {
val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
val mode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
appOps.unsafeCheckOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, Process.myUid(), context.packageName)
} else {
@Suppress("DEPRECATION")
appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, Process.myUid(), context.packageName)
}
return mode == AppOpsManager.MODE_ALLOWED
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppListScreen(prefs: SharedPreferences) {
val context = LocalContext.current
val packageManager = context.packageManager
var allApps by remember { mutableStateOf<List<AppInfo>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
val stimmedPackages = remember {
mutableStateListOf<String>().apply {
addAll(prefs.getStringSet("stimmed_apps", emptySet()) ?: emptySet())
}
}
val tempSelected = remember { mutableStateListOf<String>() }
LaunchedEffect(stimmedPackages.toList()) {
prefs.edit().putStringSet("stimmed_apps", stimmedPackages.toSet()).apply()
val intent = Intent(context, StimsService::class.java).apply {
putStringArrayListExtra("selected_packages", ArrayList(stimmedPackages))
}
if (stimmedPackages.isEmpty()) {
context.stopService(intent)
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
}
LaunchedEffect(Unit) {
allApps = withContext(Dispatchers.IO) {
val intent = Intent(Intent.ACTION_MAIN, null).apply {
addCategory(Intent.CATEGORY_LAUNCHER)
}
val resolveInfos = packageManager.queryIntentActivities(intent, 0)
resolveInfos.map { resolveInfo ->
AppInfo(
name = resolveInfo.loadLabel(packageManager).toString(),
packageName = resolveInfo.activityInfo.packageName,
icon = resolveInfo.loadIcon(packageManager)
)
}.distinctBy { it.packageName }
.sortedBy { it.name }
}
isLoading = false
}
var searchQuery by remember { mutableStateOf("") }
val stimList = remember(stimmedPackages.toList(), allApps) {
allApps.filter { it.packageName in stimmedPackages }
}
val availableApps = remember(searchQuery, stimmedPackages.toList(), allApps) {
allApps.filter { it.packageName !in stimmedPackages &&
(searchQuery.isBlank() || it.name.contains(searchQuery, ignoreCase = true) || it.packageName.contains(searchQuery, ignoreCase = true))
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Stims Manager", fontWeight = FontWeight.Bold) }
)
}
) { padding ->
if (isLoading) {
Box(modifier = Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
Column(modifier = Modifier.padding(padding)) {
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
placeholder = { Text("Search apps to stim...") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
singleLine = true
)
LazyColumn(modifier = Modifier.weight(1f)) {
if (stimList.isNotEmpty()) {
item {
SectionHeader("STAY AWAKE (STIMMED)")
}
items(stimList) { app ->
StimmedAppItem(app = app) {
stimmedPackages.remove(app.packageName)
}
}
}
item {
SectionHeader("ALL APPS")
}
items(availableApps) { app ->
SelectableAppItem(
app = app,
isSelected = app.packageName in tempSelected,
onToggle = {
if (app.packageName in tempSelected) tempSelected.remove(app.packageName)
else tempSelected.add(app.packageName)
}
)
}
}
Button(
onClick = {
stimmedPackages.addAll(tempSelected)
tempSelected.clear()
Toast.makeText(context, "Apps Stimmed!", Toast.LENGTH_SHORT).show()
},
enabled = tempSelected.isNotEmpty(),
modifier = Modifier.fillMaxWidth().padding(16.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary),
shape = MaterialTheme.shapes.medium
) {
Text("STIM SELECTED APPS", fontSize = 16.sp, fontWeight = FontWeight.Bold)
}
}
}
}
}
@Composable
fun SectionHeader(title: String) {
Text(
text = title,
style = MaterialTheme.typography.labelLarge.copy(
fontWeight = FontWeight.ExtraBold,
color = MaterialTheme.colorScheme.secondary,
letterSpacing = 1.2.sp
),
modifier = Modifier.padding(start = 16.dp, top = 20.dp, bottom = 8.dp)
)
}
@Composable
fun StimmedAppItem(app: AppInfo, onRemove: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 6.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFE8F5E9)
),
elevation = CardDefaults.cardElevation(4.dp)
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
AppIcon(app.icon)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(text = app.name, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold)
Text(text = "STIMMED", style = MaterialTheme.typography.labelSmall, color = Color(0xFF2E7D32))
}
IconButton(onClick = onRemove) {
Icon(Icons.Default.Delete, contentDescription = "Remove", tint = Color.Gray)
}
}
}
}
@Composable
fun SelectableAppItem(app: AppInfo, isSelected: Boolean, onToggle: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onToggle() }
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
AppIcon(app.icon)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(text = app.name, style = MaterialTheme.typography.bodyLarge)
Text(text = app.packageName, style = MaterialTheme.typography.bodySmall, color = Color.Gray)
}
Checkbox(
checked = isSelected,
onCheckedChange = { onToggle() }
)
}
}
@Composable
fun AppIcon(icon: Drawable?) {
if (icon != null) {
Image(
bitmap = icon.toBitmap().asImageBitmap(),
contentDescription = null,
modifier = Modifier.size(44.dp)
)
} else {
Surface(
modifier = Modifier.size(44.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.small
) {}
}
}
@@ -1,137 +0,0 @@
package acidburn.stims
import android.app.*
import android.app.usage.UsageEvents
import android.app.usage.UsageStatsManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.PowerManager
import androidx.core.app.NotificationCompat
class StimsService : Service() {
private var wakeLock: PowerManager.WakeLock? = null
private var selectedPackages = mutableSetOf<String>()
private val handler = Handler(Looper.getMainLooper())
private val checkInterval = 10000L // Check every 10 seconds
private val monitorRunnable = object : Runnable {
override fun run() {
checkForegroundApp()
handler.postDelayed(this, checkInterval)
}
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val action = intent?.action
val packages = intent?.getStringArrayListExtra("selected_packages")
if (packages != null) {
selectedPackages = packages.toMutableSet()
updateNotification()
}
if (action == ACTION_STOP) {
stopSelf()
return START_NOT_STICKY
}
startForegroundService()
handler.removeCallbacks(monitorRunnable)
handler.post(monitorRunnable)
return START_STICKY
}
private fun startForegroundService() {
createNotificationChannel()
updateNotification()
handler.post(monitorRunnable)
}
private fun updateNotification() {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Stims Daemon Running")
.setContentText("Monitoring ${selectedPackages.size} apps")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setOngoing(true)
.build()
startForeground(NOTIFICATION_ID, notification)
}
private fun checkForegroundApp() {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
if (!powerManager.isInteractive) {
releaseWakeLock()
return
}
val usageStatsManager = getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
val time = System.currentTimeMillis()
val events = usageStatsManager.queryEvents(time - 15000, time)
val event = UsageEvents.Event()
var currentPackage: String? = null
while (events.hasNextEvent()) {
events.getNextEvent(event)
if (event.eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
currentPackage = event.packageName
}
}
if (currentPackage != null && selectedPackages.contains(currentPackage)) {
acquireWakeLock()
} else if (currentPackage != null) {
releaseWakeLock()
}
}
private fun acquireWakeLock() {
if (wakeLock == null || !wakeLock!!.isHeld) {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
@Suppress("DEPRECATION")
wakeLock = powerManager.newWakeLock(
PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
"Stims::WakeLock"
)
wakeLock?.acquire(10 * 60 * 1000L) // 10 min timeout
}
}
private fun releaseWakeLock() {
if (wakeLock?.isHeld == true) {
wakeLock?.release()
}
wakeLock = null
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
if (manager.getNotificationChannel(CHANNEL_ID) == null) {
val serviceChannel = NotificationChannel(
CHANNEL_ID, "Stims Service", NotificationManager.IMPORTANCE_LOW
)
manager.createNotificationChannel(serviceChannel)
}
}
}
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacks(monitorRunnable)
releaseWakeLock()
}
companion object {
const val CHANNEL_ID = "StimsServiceChannel"
const val NOTIFICATION_ID = 1
const val ACTION_STOP = "acidburn.stims.STOP"
}
}
@@ -1,31 +0,0 @@
package acidburn.stims.ui.theme
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.platform.LocalContext
@Composable
fun StimsTheme(content: @Composable () -> Unit) {
val darkTheme = isSystemInDarkTheme()
val context = LocalContext.current
val colorScheme = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
}
darkTheme -> darkColorScheme()
else -> lightColorScheme()
}
MaterialTheme(
colorScheme = colorScheme,
content = content
)
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/circular_icon" />
<foreground android:drawable="@android:color/transparent" />
</adaptive-icon>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/circular_icon" />
<foreground android:drawable="@android:color/transparent" />
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

-3
View File
@@ -1,3 +0,0 @@
<resources>
<string name="app_name">Stims</string>
</resources>
-6
View File
@@ -1,6 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Stims" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your theme here. -->
</style>
</resources>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 889 KiB

-389
View File
@@ -1,389 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="221.7683"
height="373.49692"
viewBox="0 0 221.7683 373.49692"
role="img"
aria-label="Cyberpunk caffeine auto-injector icon"
version="1.1"
id="svg24"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs13">
<linearGradient
id="bodyGrad"
x1="0"
y1="0"
x2="1"
y2="1">
<stop
offset="0%"
stop-color="#24163a"
id="stop1" />
<stop
offset="50%"
stop-color="#3b2c65"
id="stop2" />
<stop
offset="100%"
stop-color="#0b0f1f"
id="stop3" />
</linearGradient>
<linearGradient
id="glassGrad"
x1="183.4281"
y1="75.396645"
x2="441.97247"
y2="333.94101"
gradientTransform="scale(0.79948761,1.2508011)"
gradientUnits="userSpaceOnUse">
<stop
offset="0%"
stop-color="#ff9a00"
id="stop4" />
<stop
offset="45%"
stop-color="#ff5e00"
id="stop5" />
<stop
offset="100%"
stop-color="#ffbf3c"
id="stop6" />
</linearGradient>
<linearGradient
id="needleGrad"
x1="260.10269"
y1="342.12448"
x2="350.93219"
y2="342.12448"
gradientTransform="scale(1.5138252,0.66057826)"
gradientUnits="userSpaceOnUse">
<stop
offset="0%"
stop-color="#9cfbff"
id="stop7" />
<stop
offset="100%"
stop-color="#00d1ff"
id="stop8" />
</linearGradient>
<linearGradient
id="ringGrad"
x1="0"
y1="0"
x2="0"
y2="1">
<stop
offset="0%"
stop-color="#59f3ff"
id="stop9" />
<stop
offset="100%"
stop-color="#00a3d9"
id="stop10" />
</linearGradient>
<filter
id="glowCyan"
x="-0.75"
y="-0.75"
width="2.5"
height="2.5">
<feGaussianBlur
stdDeviation="5"
result="blur"
id="feGaussianBlur10" />
<feMerge
id="feMerge11">
<feMergeNode
in="blur"
id="feMergeNode10" />
<feMergeNode
in="SourceGraphic"
id="feMergeNode11" />
</feMerge>
</filter>
<filter
id="glowOrange"
x="-0.28296598"
y="-0.84744828"
width="1.565932"
height="2.6948966">
<feGaussianBlur
stdDeviation="7"
result="blur"
id="feGaussianBlur11" />
<feMerge
id="feMerge13">
<feMergeNode
in="blur"
id="feMergeNode12" />
<feMergeNode
in="SourceGraphic"
id="feMergeNode13" />
</feMerge>
</filter>
<style
id="style13">
.outline { stroke:#0ff7ff; stroke-opacity:.55; stroke-width:3; }
.metal { fill:url(#bodyGrad); stroke:#7d6efc; stroke-width:3; }
.accent { fill:#6a49ff; }
.cyan { fill:#40f5ff; }
.text { font-family: Arial, Helvetica, sans-serif; font-weight:700; letter-spacing:2px; }
</style>
<linearGradient
xlink:href="#bodyGrad"
id="linearGradient24"
x1="136.72952"
y1="127.53463"
x2="231.78215"
y2="222.58723"
gradientTransform="scale(0.68383178,1.462348)"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#bodyGrad"
id="linearGradient25"
x1="134.45683"
y1="173.42926"
x2="334.03391"
y2="373.00632"
gradientTransform="scale(1.067257,0.93698147)"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#ringGrad"
id="linearGradient26"
x1="58.319386"
y1="210.38649"
x2="58.319386"
y2="314.7475"
gradientTransform="scale(1.6289609,0.61388827)"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#ringGrad"
id="linearGradient27"
x1="144.26373"
y1="210.38649"
x2="144.26373"
y2="314.7475"
gradientTransform="scale(1.6289609,0.61388827)"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#bodyGrad"
id="linearGradient28"
x1="579.89478"
y1="113.10458"
x2="656.08533"
y2="189.29514"
gradientTransform="scale(0.59062442,1.6931233)"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#bodyGrad"
id="linearGradient29"
x1="512.72302"
y1="157.30843"
x2="582.06396"
y2="226.64938"
gradientTransform="scale(0.74991759,1.3334799)"
gradientUnits="userSpaceOnUse" />
</defs>
<g
transform="matrix(-0.30621193,0.74049952,-0.49004152,-0.46271545,312.88776,84.680243)"
id="g24">
<!-- rear cap -->
<rect
x="95"
y="188"
rx="26"
ry="26"
width="62"
height="136"
class="metal"
id="rect13"
style="fill:url(#linearGradient24)" />
<circle
cx="124"
cy="205"
r="9"
class="cyan"
filter="url(#glowCyan)"
id="circle13" />
<circle
cx="124"
cy="307"
r="9"
class="cyan"
filter="url(#glowCyan)"
id="circle14" />
<!-- body -->
<rect
x="145"
y="164"
rx="34"
ry="34"
width="210"
height="184"
class="metal"
id="rect14"
style="fill:url(#linearGradient25)" />
<rect
x="165"
y="182"
rx="20"
ry="20"
width="170"
height="148"
fill="#11192c"
stroke="#3ef4ff"
stroke-width="2"
id="rect15" />
<!-- glowing chamber -->
<rect
x="184"
y="196"
rx="16"
ry="16"
width="132"
height="120"
fill="url(#glassGrad)"
filter="url(#glowOrange)"
opacity="0.96"
id="rect16"
style="fill:url(#glassGrad)" />
<rect
x="193"
y="196"
rx="10"
ry="10"
width="18"
height="120"
fill="#fff4c2"
opacity="0.18"
id="rect17" />
<text
x="250"
y="263"
text-anchor="middle"
class="text"
font-size="28px"
fill="#fff6d5"
filter="url(#glowOrange)"
id="text17">CAF</text>
<!-- cyan tubing -->
<path
d="m 214,174 c -26,-34 -54,-34 -68,-1"
fill="none"
stroke="url(#ringGrad)"
stroke-width="9"
stroke-linecap="round"
filter="url(#glowCyan)"
id="path17"
style="stroke:url(#linearGradient26)" />
<path
d="m 286,174 c 26,-34 54,-34 68,-1"
fill="none"
stroke="url(#ringGrad)"
stroke-width="9"
stroke-linecap="round"
filter="url(#glowCyan)"
id="path18"
style="stroke:url(#linearGradient27)" />
<!-- side nodes -->
<circle
cx="170"
cy="256"
r="12"
class="accent"
id="circle18" />
<circle
cx="341"
cy="256"
r="12"
class="accent"
id="circle19" />
<!-- display -->
<rect
x="205"
y="320"
rx="10"
ry="10"
width="90"
height="24"
fill="#101827"
stroke="#3ef4ff"
stroke-width="2"
id="rect19" />
<text
x="250"
y="338"
text-anchor="middle"
class="text"
font-size="14px"
fill="#53fbff"
id="text19">250</text>
<!-- front collar -->
<rect
x="344"
y="193"
rx="20"
ry="20"
width="42"
height="126"
class="metal"
id="rect20"
style="fill:url(#linearGradient28)" />
<circle
cx="365"
cy="207"
r="8"
class="cyan"
filter="url(#glowCyan)"
id="circle20" />
<circle
cx="365"
cy="305"
r="8"
class="cyan"
filter="url(#glowCyan)"
id="circle21" />
<!-- injector tip -->
<path
d="m 386,232 49,-20 v 88 l -49,-20 z"
fill="url(#bodyGrad)"
stroke="#7d6efc"
stroke-width="3"
id="path21"
style="fill:url(#linearGradient29)" />
<path
d="m 435,244 55,12 -55,12 z"
fill="url(#needleGrad)"
filter="url(#glowCyan)"
id="path22"
style="fill:url(#needleGrad)" />
<path
d="m 490,252 16,4 -16,4 z"
fill="#d8fdff"
id="path23" />
<!-- little rings -->
<rect
x="156"
y="176"
rx="6"
ry="6"
width="12"
height="160"
fill="#1af2ff"
opacity="0.35"
id="rect23" />
<rect
x="328"
y="176"
rx="6"
ry="6"
width="12"
height="160"
fill="#1af2ff"
opacity="0.2"
id="rect24" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.8 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

-18
View File
@@ -1,18 +0,0 @@
// Top-level build file
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
@@ -1,7 +0,0 @@
Stims lets you select apps that should prevent your screen from turning off. When a stimmed app is active in the foreground,
the screen stays on at full brightness. When you switch to a different app, the screen resumes normal sleep behavior.
Select apps to stim from the list, tap "Stim Selected Apps", and a background service will monitor the foreground
app and hold a wake lock whenever one of your chosen apps is active.
Requires Usage Access permission to detect which app is in the foreground.
@@ -1 +0,0 @@
Keep your screen awake while specific apps are in the foreground.
-3
View File
@@ -1,3 +0,0 @@
android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
Binary file not shown.
-6
View File
@@ -1,6 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
-249
View File
@@ -1,249 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
-92
View File
@@ -1,92 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+565
View File
@@ -0,0 +1,565 @@
:root {
--bg: #070707;
--surface: #0e0e0e;
--border: #1c1c1c;
--border-mid: #282828;
--amber: #e89a0a;
--amber-bright: #ffb020;
--text: #f5f0e8;
--text-dim: #c8c2b8;
--text-muted: #9a9490;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
background-color: var(--bg);
color: var(--text);
font-family: 'Azeret Mono', monospace;
font-weight: 400;
line-height: 1.6;
min-height: 100vh;
overflow-x: hidden;
}
/* Grain overlay */
body::after {
content: '';
position: fixed;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E");
opacity: 0.035;
pointer-events: none;
z-index: 999;
}
/* ─── HERO ─── */
.hero {
position: relative;
min-height: 100svh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem 6rem;
overflow: hidden;
}
/* Ambient breathing orb */
.orb {
position: absolute;
width: 700px;
height: 700px;
border-radius: 50%;
background: radial-gradient(circle,
rgba(232, 154, 10, 0.22) 0%,
rgba(232, 154, 10, 0.07) 35%,
transparent 65%);
pointer-events: none;
animation: breathe 7s ease-in-out infinite;
}
@keyframes breathe {
0%,
100% {
transform: scale(1);
opacity: 0.75;
}
50% {
transform: scale(1.12);
opacity: 1;
}
}
/* Secondary haze ring */
.orb-ring {
position: absolute;
width: 900px;
height: 900px;
border-radius: 50%;
background: radial-gradient(circle,
transparent 45%,
rgba(232, 154, 10, 0.03) 60%,
transparent 70%);
pointer-events: none;
animation: breathe 7s ease-in-out infinite reverse;
}
.hero-content {
position: relative;
z-index: 1;
text-align: center;
max-width: 820px;
}
.eyebrow {
font-size: 0.62rem;
font-weight: 500;
letter-spacing: 0.35em;
color: var(--amber);
text-transform: uppercase;
margin-bottom: 1.25rem;
opacity: 0;
animation: fadeUp 0.6s 0.1s ease forwards;
}
.app-name {
font-family: 'Big Shoulders Display', sans-serif;
font-weight: 900;
font-size: clamp(6.5rem, 22vw, 15rem);
line-height: 0.85;
letter-spacing: -0.01em;
text-transform: uppercase;
color: var(--text);
opacity: 0;
animation: fadeUp 0.7s 0.2s ease forwards;
}
.app-name .accent {
color: var(--amber);
}
.tagline {
margin-top: 2.25rem;
font-size: clamp(0.78rem, 1.4vw, 0.92rem);
font-weight: 300;
font-style: italic;
color: var(--text);
line-height: 2;
opacity: 0;
animation: fadeUp 0.7s 0.35s ease forwards;
}
/* ─── BUTTONS ─── */
.download-row {
display: flex;
gap: 0.75rem;
justify-content: center;
align-items: center;
flex-wrap: wrap;
margin-top: 3rem;
opacity: 0;
animation: fadeUp 0.7s 0.5s ease forwards;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.55rem;
height: 40px;
padding: 0 1.4rem;
box-sizing: border-box;
font-family: 'Azeret Mono', monospace;
font-size: 0.7rem;
font-weight: 500;
letter-spacing: 0.06em;
text-decoration: none;
border-radius: 3px;
transition: all 0.18s ease;
white-space: nowrap;
cursor: pointer;
}
.btn-amber {
background: var(--amber);
color: #000;
border: 1px solid var(--amber);
}
.btn-amber:hover {
background: var(--amber-bright);
border-color: var(--amber-bright);
box-shadow: 0 0 28px rgba(232, 154, 10, 0.35);
}
.btn-outline {
background: #1e1a10;
color: var(--text);
border: 1px solid #5a4e30;
}
.btn-outline:hover {
background: #2a2214;
border-color: var(--amber);
color: var(--amber-bright);
}
.btn-ghost {
background: #181818;
color: var(--text-dim);
border: 1px solid #3a3530;
}
.btn-ghost:hover {
background: #202020;
color: var(--text);
border-color: #5a5550;
}
.btn.disabled {
opacity: 0.38;
pointer-events: none;
}
.badge {
font-size: 0.5rem;
padding: 0.12rem 0.35rem;
background: rgba(255, 255, 255, 0.07);
color: var(--text-muted);
border-radius: 2px;
letter-spacing: 0.1em;
}
.icon {
width: 15px;
height: 15px;
fill: currentColor;
flex-shrink: 0;
}
/* F-Droid official badge — self-contained graphic, sized to match buttons */
.fdroid-badge {
display: inline-flex;
align-items: center;
text-decoration: none;
transition: opacity 0.18s ease;
}
.fdroid-badge:hover {
opacity: 0.85;
}
.fdroid-badge img {
display: block;
height: 60px;
width: auto;
}
/* ─── DIVIDER ─── */
hr {
border: none;
border-top: 1px solid var(--border);
}
/* ─── COMPATIBILITY STRIP ─── */
.compat-strip {
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
padding: 1rem 2rem;
display: flex;
justify-content: center;
gap: 3rem;
flex-wrap: wrap;
}
.compat-item {
font-size: 0.62rem;
letter-spacing: 0.1em;
color: var(--text-muted);
text-transform: uppercase;
}
.compat-item span {
color: var(--text-dim);
}
/* ─── SECTION WRAPPER ─── */
.section {
padding: 5rem 2rem;
max-width: 920px;
margin: 0 auto;
}
.section-label {
font-size: 0.58rem;
letter-spacing: 0.4em;
color: var(--amber);
text-transform: uppercase;
margin-bottom: 3rem;
}
/* ─── FEATURES GRID ─── */
.features-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
border: 1px solid var(--border);
background: var(--border);
gap: 1px;
}
.feature-card {
background: var(--bg);
padding: 2.5rem 1.75rem;
transition: background 0.2s;
}
.feature-card:hover {
background: var(--surface);
}
.feature-glyph {
font-size: 1.6rem;
line-height: 1;
color: var(--amber);
margin-bottom: 1.4rem;
display: block;
opacity: 0.85;
}
.feature-title {
font-family: 'Big Shoulders Display', sans-serif;
font-weight: 700;
font-size: 1.3rem;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--text);
margin-bottom: 0.65rem;
}
.feature-desc {
font-size: 0.75rem;
font-weight: 300;
color: var(--text-dim);
line-height: 1.85;
}
/* ─── STEPS ─── */
.steps-list {
display: grid;
gap: 0;
margin-top: 2rem;
}
.step {
display: grid;
grid-template-columns: 5rem 1fr;
gap: 1.5rem;
padding: 2.25rem 0;
border-bottom: 1px solid var(--border);
align-items: start;
}
.step:first-child {
border-top: 1px solid var(--border);
}
.step-num {
font-family: 'Big Shoulders Display', sans-serif;
font-weight: 900;
font-size: 4rem;
line-height: 1;
color: var(--border-mid);
padding-top: 0.1rem;
}
.step-title {
font-size: 0.78rem;
font-weight: 500;
color: var(--text);
margin-bottom: 0.5rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.step-desc {
font-size: 0.74rem;
font-weight: 300;
color: var(--text-dim);
line-height: 1.85;
}
.step-code {
display: inline-block;
background: var(--surface);
border: 1px solid var(--border);
padding: 0.1rem 0.45rem;
border-radius: 2px;
font-size: 0.68rem;
color: var(--amber);
margin-top: 0.5rem;
}
/* ─── PERMISSION CARD ─── */
.perm-card {
border: 1px solid var(--border);
padding: 2rem;
margin-top: 2rem;
display: grid;
grid-template-columns: 2.5rem 1fr;
gap: 1.5rem;
align-items: start;
}
.perm-dot {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
border: 1px solid var(--border-mid);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9rem;
color: var(--amber);
flex-shrink: 0;
}
.perm-name {
font-size: 0.68rem;
font-weight: 500;
color: var(--amber);
letter-spacing: 0.15em;
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.perm-desc {
font-size: 0.75rem;
font-weight: 300;
color: var(--text-dim);
line-height: 1.85;
}
/* ─── SCREENSHOTS ─── */
.screenshots-track {
display: flex;
gap: 1.25rem;
overflow-x: auto;
padding-bottom: 1rem;
scrollbar-width: thin;
scrollbar-color: var(--border-mid) transparent;
}
.screenshots-track::-webkit-scrollbar {
height: 4px;
}
.screenshots-track::-webkit-scrollbar-track {
background: transparent;
}
.screenshots-track::-webkit-scrollbar-thumb {
background: var(--border-mid);
border-radius: 2px;
}
.screenshot {
flex-shrink: 0;
width: 220px;
border: 1px solid var(--border);
border-radius: 4px;
overflow: hidden;
transition: border-color 0.18s;
}
.screenshot:hover {
border-color: var(--border-mid);
}
.screenshot img {
width: 100%;
height: auto;
display: block;
}
/* ─── FOOTER ─── */
footer {
border-top: 1px solid var(--border);
padding: 2rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
max-width: 100%;
}
.footer-left {
font-size: 0.62rem;
color: var(--text-muted);
letter-spacing: 0.05em;
}
.footer-links {
display: flex;
gap: 2rem;
}
.footer-links a {
font-size: 0.62rem;
color: var(--text-muted);
text-decoration: none;
letter-spacing: 0.05em;
transition: color 0.18s;
}
.footer-links a:hover {
color: var(--amber);
}
/* ─── ANIMATIONS ─── */
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ─── RESPONSIVE ─── */
@media (max-width: 700px) {
.features-grid {
grid-template-columns: 1fr;
}
.download-row {
flex-direction: column;
align-items: stretch;
}
.btn,
.fdroid-badge {
justify-content: center;
}
.step {
grid-template-columns: 3.5rem 1fr;
gap: 1rem;
}
.step-num {
font-size: 3rem;
}
.perm-card {
grid-template-columns: 1fr;
}
footer {
flex-direction: column;
text-align: center;
}
.footer-links {
justify-content: center;
}
.compat-strip {
gap: 1.5rem;
}
}

Before

Width:  |  Height:  |  Size: 889 KiB

After

Width:  |  Height:  |  Size: 889 KiB

+234
View File
@@ -0,0 +1,234 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stims — Keep Your Screen Awake</title>
<meta
name="description"
content="Stims keeps your Android screen awake while your chosen apps are open. Simple, lightweight, no bloat."
/>
<meta property="og:title" content="Stims — Keep Your Screen Awake" />
<meta
property="og:description"
content="Pick the apps. Forget the rest. Your screen stays on when you need it to."
/>
<meta property="og:type" content="website" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Big+Shoulders+Display:wght@400;700;900&family=Azeret+Mono:ital,wght@0,300;0,400;0,500;1,300&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="home.css" />
</head>
<body>
<!-- ═══ HERO ═══ -->
<section class="hero">
<div class="orb"></div>
<div class="orb-ring"></div>
<div class="hero-content">
<p class="eyebrow">Android Utility</p>
<h1 class="app-name">STIM<span class="accent">S</span></h1>
<p class="tagline">
Your screen stays on when you need it to.<br />
Pick the apps. Forget the rest.
</p>
<div class="download-row">
<!-- Google Play — uncomment href when available -->
<a href="#" class="btn btn-amber disabled" aria-disabled="true">
<!-- Play icon -->
<svg class="icon" viewBox="0 0 24 24">
<path
d="M5.31 3.07C4.55 2.64 3.79 3.13 3.79 4.01v15.98c0 .88.76 1.37 1.52.94l14.1-7.99a1.07 1.07 0 0 0 0-1.88L5.31 3.07z"
/>
</svg>
Google Play
<span class="badge">SOON</span>
</a>
<!-- F-Droid -->
<a
href="https://f-droid.org/en/packages/acidburn.stims/"
class="fdroid-badge"
target="_blank"
rel="noopener noreferrer"
>
<img
src="https://f-droid.org/badge/get-it-on.svg"
alt="Get it on F-Droid"
/>
</a>
<!-- GitHub -->
<a
href="https://github.com/acidburnmonkey/stims"
class="btn btn-ghost"
target="_blank"
rel="noopener noreferrer"
>
<svg class="icon" viewBox="0 0 24 24">
<path
d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34-.46-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.87 1.52 2.34 1.07 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.92 0-1.11.38-2 1.03-2.71-.1-.25-.45-1.29.1-2.64 0 0 .84-.27 2.75 1.02.79-.22 1.65-.33 2.5-.33.85 0 1.71.11 2.5.33 1.91-1.29 2.75-1.02 2.75-1.02.55 1.35.2 2.39.1 2.64.65.71 1.03 1.6 1.03 2.71 0 3.82-2.34 4.66-4.57 4.91.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2z"
/>
</svg>
GitHub
</a>
</div>
</div>
</section>
<!-- ═══ COMPAT STRIP ═══ -->
<div class="compat-strip">
<span class="compat-item">Minimum <span>Android 7.0 (API 24)</span></span>
<span class="compat-item">Target <span>Android 15 (API 35)</span></span>
<span class="compat-item">Size <span>Lightweight</span></span>
<span class="compat-item">License <span>GPL v3</span></span>
</div>
<!-- ═══ SCREENSHOTS ═══ -->
<div class="section">
<p class="section-label">Screenshots</p>
<div class="screenshots-track">
<div class="screenshot">
<img
src="phoneScreenshots/s1.jpeg"
alt="Stims screenshot 1"
loading="lazy"
/>
</div>
<div class="screenshot">
<img
src="phoneScreenshots/s2.jpeg"
alt="Stims screenshot 2"
loading="lazy"
/>
</div>
<div class="screenshot">
<img
src="phoneScreenshots/s3.jpeg"
alt="Stims screenshot 3"
loading="lazy"
/>
</div>
</div>
</div>
<hr />
<!-- ═══ FEATURES ═══ -->
<div class="section">
<p class="section-label">What it does</p>
<div class="features-grid">
<div class="feature-card">
<span class="feature-glyph"></span>
<h3 class="feature-title">Per-app control</h3>
<p class="feature-desc">
Choose exactly which apps keep the screen awake. Every other app
behaves as normal. Your battery, your rules.
</p>
</div>
<div class="feature-card">
<span class="feature-glyph"></span>
<h3 class="feature-title">Silent service</h3>
<p class="feature-desc">
A lightweight background service watches the foreground app and
manages the wake lock automatically — no interaction needed.
</p>
</div>
<div class="feature-card">
<span class="feature-glyph"></span>
<h3 class="feature-title">Zero bloat</h3>
<p class="feature-desc">
No accounts. No analytics. No network access. No trackers. One
purpose, done well.
</p>
</div>
</div>
</div>
<hr />
<!-- ═══ HOW IT WORKS ═══ -->
<div class="section">
<p class="section-label">Setup</p>
<div class="steps-list">
<div class="step">
<div class="step-num">01</div>
<div>
<p class="step-title">Grant Usage Access</p>
<p class="step-desc">
On first launch, Stims will open the system permission screen.
Navigate to
<span class="step-code"
>Settings → Apps → Special app access → Usage access</span
>, find Stims, and toggle it on.
</p>
</div>
</div>
<div class="step">
<div class="step-num">02</div>
<div>
<p class="step-title">Pick your apps</p>
<p class="step-desc">
Back in Stims, tap any app in the list to mark it. Your selection
is saved immediately — no confirm button needed.
</p>
</div>
</div>
<div class="step">
<div class="step-num">03</div>
<div>
<p class="step-title">Done</p>
<p class="step-desc">
Switch to a marked app and the screen stays on at full brightness.
Switch away and normal sleep behavior returns. That's the whole
thing.
</p>
</div>
</div>
</div>
</div>
<hr />
<!-- ═══ PERMISSIONS ═══ -->
<div class="section">
<p class="section-label">Permissions</p>
<div class="perm-card">
<div class="perm-dot"></div>
<div>
<p class="perm-name">Usage Access</p>
<p class="perm-desc">
Used solely to detect which app is currently in the foreground so
the wake lock can be acquired or released accordingly. This data is
processed entirely on-device and is never transmitted anywhere.
Stims has no internet permission and cannot contact any external
server.
</p>
</div>
</div>
</div>
<!-- ═══ FOOTER ═══ -->
<footer>
<p class="footer-left">
Stims &nbsp;·&nbsp; Android 7.0+ &nbsp;·&nbsp; Open Source
</p>
<nav class="footer-links">
<a
href="https://github.com/acidburnmonkey/stims"
target="_blank"
rel="noopener noreferrer"
>GitHub</a
>
<a href="privacy.html">Privacy Policy</a>
</nav>
</footer>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

+292
View File
@@ -0,0 +1,292 @@
:root {
--bg: #070707;
--surface: #0e0e0e;
--border: #1c1c1c;
--border-mid: #282828;
--amber: #e89a0a;
--text: #ede8dc;
--text-dim: #8a857c;
--text-muted: #474340;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
background-color: var(--bg);
color: var(--text);
font-family: 'Azeret Mono', monospace;
font-weight: 300;
line-height: 1.75;
min-height: 100vh;
}
body::after {
content: '';
position: fixed;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E");
opacity: 0.035;
pointer-events: none;
z-index: 999;
}
/* ─── HEADER ─── */
header {
border-bottom: 1px solid var(--border);
padding: 1.25rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.header-logo {
font-family: 'Big Shoulders Display', sans-serif;
font-weight: 900;
font-size: 1.5rem;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--text);
text-decoration: none;
}
.header-logo span {
color: var(--amber);
}
.header-back {
font-size: 0.62rem;
color: var(--text-muted);
text-decoration: none;
letter-spacing: 0.1em;
text-transform: uppercase;
transition: color 0.18s;
display: flex;
align-items: center;
gap: 0.4rem;
}
.header-back:hover {
color: var(--amber);
}
/* ─── HERO ─── */
.doc-hero {
padding: 5rem 2rem 3rem;
max-width: 720px;
margin: 0 auto;
border-bottom: 1px solid var(--border);
}
.doc-eyebrow {
font-size: 0.6rem;
letter-spacing: 0.4em;
color: var(--amber);
text-transform: uppercase;
margin-bottom: 1.5rem;
}
.doc-title {
font-family: 'Big Shoulders Display', sans-serif;
font-weight: 900;
font-size: clamp(3rem, 8vw, 5.5rem);
line-height: 0.9;
text-transform: uppercase;
color: var(--text);
margin-bottom: 2rem;
}
.doc-meta {
font-size: 0.65rem;
color: var(--text-muted);
letter-spacing: 0.08em;
}
/* ─── SUMMARY CARD ─── */
.summary-card {
max-width: 720px;
margin: 3rem auto 0;
padding: 0 2rem;
}
.summary-box {
border: 1px solid var(--amber);
border-left: 3px solid var(--amber);
padding: 1.5rem 1.75rem;
background: rgba(232, 154, 10, 0.04);
}
.summary-box p {
font-size: 0.78rem;
font-weight: 400;
color: var(--text);
line-height: 1.85;
}
.summary-box strong {
color: var(--amber);
font-weight: 500;
}
/* ─── DOC BODY ─── */
.doc-body {
max-width: 720px;
margin: 0 auto;
padding: 3rem 2rem 6rem;
}
.doc-section {
margin-bottom: 3.5rem;
}
.doc-section-num {
font-size: 0.58rem;
letter-spacing: 0.35em;
color: var(--text-muted);
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.doc-section-title {
font-family: 'Big Shoulders Display', sans-serif;
font-weight: 700;
font-size: 1.6rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text);
margin-bottom: 1.25rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border);
}
.doc-section p {
font-size: 0.78rem;
font-weight: 300;
color: var(--text-dim);
line-height: 1.9;
margin-bottom: 1rem;
}
.doc-section p:last-child {
margin-bottom: 0;
}
.doc-section ul {
list-style: none;
padding: 0;
margin: 0.75rem 0;
}
.doc-section ul li {
font-size: 0.78rem;
font-weight: 300;
color: var(--text-dim);
line-height: 1.9;
padding-left: 1.5rem;
position: relative;
margin-bottom: 0.4rem;
}
.doc-section ul li::before {
content: '—';
position: absolute;
left: 0;
color: var(--border-mid);
}
.highlight {
color: var(--text);
font-weight: 400;
}
.mono-tag {
display: inline-block;
background: var(--surface);
border: 1px solid var(--border);
padding: 0.08rem 0.4rem;
border-radius: 2px;
font-size: 0.7rem;
color: var(--amber);
}
/* Contact block */
.contact-block {
border: 1px solid var(--border);
padding: 1.5rem;
margin-top: 1.5rem;
}
.contact-label {
font-size: 0.58rem;
letter-spacing: 0.25em;
color: var(--text-muted);
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.contact-block a {
color: var(--amber);
text-decoration: none;
font-size: 0.78rem;
transition: color 0.18s;
}
.contact-block a:hover {
color: #ffb020;
text-decoration: underline;
}
/* ─── FOOTER ─── */
footer {
border-top: 1px solid var(--border);
padding: 1.75rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.footer-left {
font-size: 0.62rem;
color: var(--text-muted);
letter-spacing: 0.05em;
}
.footer-links {
display: flex;
gap: 2rem;
}
.footer-links a {
font-size: 0.62rem;
color: var(--text-muted);
text-decoration: none;
letter-spacing: 0.05em;
transition: color 0.18s;
}
.footer-links a:hover {
color: var(--amber);
}
@media (max-width: 600px) {
footer {
flex-direction: column;
text-align: center;
}
.footer-links {
justify-content: center;
}
}
+155
View File
@@ -0,0 +1,155 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Privacy Policy — Stims</title>
<meta name="description"
content="Privacy policy for Stims, an Android app that keeps your screen awake for selected apps." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Big+Shoulders+Display:wght@700;900&family=Azeret+Mono:ital,wght@0,300;0,400;0,500;1,300&display=swap"
rel="stylesheet" />
<link rel="stylesheet" href="privacy.css" />
</head>
<body>
<!-- ═══ HEADER ═══ -->
<header>
<a href="index.html" class="header-logo">STIM<span>S</span></a>
<a href="index.html" class="header-back"> ← Back to home </a>
</header>
<!-- ═══ DOC HERO ═══ -->
<div class="doc-hero">
<p class="doc-eyebrow">Legal</p>
<h1 class="doc-title">Privacy<br />Policy</h1>
<p class="doc-meta">
Effective date: 2026-03-24 &nbsp;·&nbsp; Applies to: Stims for Android
</p>
</div>
<!-- ═══ SUMMARY ═══ -->
<div class="summary-card">
<div class="summary-box">
<p>
<strong>Short version:</strong> Stims collects no personal data, sends
nothing to any server, and contains no third-party SDKs, analytics, or
advertising. Everything the app does stays entirely on your device.
</p>
</div>
</div>
<!-- ═══ POLICY BODY ═══ -->
<div class="doc-body">
<div class="doc-section">
<p class="doc-section-num">§ 01</p>
<h2 class="doc-section-title">Information We Collect</h2>
<p>
<span class="highlight">We collect no personal information.</span>
Stims does not ask for, store, or transmit any data that identifies
you or your device to us or to any third party.
</p>
</div>
<div class="doc-section">
<p class="doc-section-num">§ 02</p>
<h2 class="doc-section-title">Permissions Used</h2>
<p>Stims requires one special permission to function:</p>
<ul>
<li>
<span class="highlight">Usage Access</span> — allows the app to
detect which app is currently in the foreground. This is used
exclusively to determine whether to acquire or release a screen wake
lock. The data is read in-process, never written to persistent
storage beyond the current session, and never transmitted.
</li>
</ul>
<p>
Your list of selected apps is stored in
<span class="mono-tag">SharedPreferences</span> — local device storage
only, never synced or uploaded.
</p>
<p>
Stims has <span class="highlight">no internet permission</span> in its
manifest. It is architecturally incapable of making network requests.
</p>
</div>
<div class="doc-section">
<p class="doc-section-num">§ 03</p>
<h2 class="doc-section-title">Data Sharing</h2>
<p>
We share no data with anyone because we collect no data. There are no
third-party SDKs, no analytics libraries, no advertising networks, and
no crash reporting services included in the app.
</p>
</div>
<div class="doc-section">
<p class="doc-section-num">§ 04</p>
<h2 class="doc-section-title">Data Storage &amp; Retention</h2>
<p>
The only persistent data Stims stores is your list of selected apps,
saved to local <span class="mono-tag">SharedPreferences</span> on your
device. This data:
</p>
<ul>
<li>Never leaves your device</li>
<li>Is deleted when you uninstall the app</li>
<li>
Can be cleared at any time via Settings → Apps → Stims → Clear data
</li>
</ul>
</div>
<div class="doc-section">
<p class="doc-section-num">§ 05</p>
<h2 class="doc-section-title">Children's Privacy</h2>
<p>
Stims does not knowingly collect information from anyone, including
children under the age of 13. Because no data is collected at all, the
app has no age-specific data practices.
</p>
</div>
<div class="doc-section">
<p class="doc-section-num">§ 06</p>
<h2 class="doc-section-title">Changes to This Policy</h2>
<p>
If the privacy practices of Stims change — for example if a future
version adds a feature requiring network access — this policy will be
updated and the effective date revised. The current policy always
reflects the latest published version of the app.
</p>
</div>
<div class="doc-section">
<p class="doc-section-num">§ 07</p>
<h2 class="doc-section-title">Contact</h2>
<p>
Questions about this policy or the app can be directed to the project
maintainer via GitHub:
</p>
<div class="contact-block">
<p class="contact-label">GitHub</p>
<a href="https://github.com/acidburnmonkey/stims" target="_blank"
rel="noopener noreferrer">github.com/acidburnmonkey/stims</a>
</div>
</div>
</div>
<!-- ═══ FOOTER ═══ -->
<footer>
<p class="footer-left">Stims &nbsp;·&nbsp; Privacy Policy</p>
<nav class="footer-links">
<a href="index.html">Home</a>
<a href="https://github.com/acidburnmonkey/stims" target="_blank" rel="noopener noreferrer">GitHub</a>
</nav>
</footer>
</body>
</html>
-2
View File
@@ -1,2 +0,0 @@
rootProject.name = "stims"
include ':app'