35
.github/workflows/build-release-apk.yml
vendido
Archivo normal
@@ -0,0 +1,35 @@
|
|||||||
|
name: Build & Publish APK Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up JDK
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Make Gradle executable
|
||||||
|
run: chmod +x ./gradlew
|
||||||
|
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: ./gradlew build
|
||||||
|
|
||||||
|
- name: Build Debug APK
|
||||||
|
run: ./gradlew assembleDebug
|
||||||
|
|
||||||
|
- name: Upload APK to Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: app/build/outputs/apk/debug/*.apk
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.TOKEN }}
|
||||||
15
.gitignore
vendido
Archivo normal
@@ -0,0 +1,15 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
22
LICENSE
Archivo normal
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Manuel Alejandro
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
168
README.md
Archivo normal
@@ -0,0 +1,168 @@
|
|||||||
|
# URLFinder
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="app/src/main/res/drawable/ic_urlfinder_logo.xml" width="120" alt="URLFinder logo"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<strong>A free, open-source Android app that detects URLs and QR codes in real time using your device camera.</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/manalejandro/URLFinder/releases"><img src="https://img.shields.io/github/v/release/manalejandro/URLFinder?style=flat-square" alt="Latest release"/></a>
|
||||||
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="MIT License"/></a>
|
||||||
|
<a href="https://developer.android.com"><img src="https://img.shields.io/badge/platform-Android-green?style=flat-square" alt="Android"/></a>
|
||||||
|
<img src="https://img.shields.io/badge/min%20SDK-24-orange?style=flat-square" alt="Min SDK 24"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 📷 **Real-time camera scanning** — live camera preview powered by CameraX
|
||||||
|
- 🔤 **Text URL detection** — recognizes URLs in any visible text using ML Kit Text Recognition
|
||||||
|
- 📲 **QR code scanning** — decodes QR codes that contain URLs with ML Kit Barcode Scanning
|
||||||
|
- 🔗 **Clickable URLs** — tap any detected URL to open it in your browser (with a safety warning)
|
||||||
|
- ⚡ **Flash / torch toggle** — turn on the camera torch for scanning in low-light conditions
|
||||||
|
- 🗂️ **Local history** — all detected URLs are saved locally and can be browsed and cleared from Settings
|
||||||
|
- ⚠️ **Safety warning dialog** — a confirmation dialog reminds you that URLs may be dangerous before opening
|
||||||
|
- 🆓 **Free & open source** — MIT licensed, no ads, no tracking, no analytics
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
> _Coming soon_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Android version** | 7.0 (API 24) or higher |
|
||||||
|
| **Permissions** | `CAMERA`, `INTERNET` |
|
||||||
|
| **Hardware** | Rear camera with autofocus recommended |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Download APK
|
||||||
|
|
||||||
|
Download the latest release APK from the [Releases page](https://github.com/manalejandro/URLFinder/releases) and install it on your device.
|
||||||
|
|
||||||
|
> You may need to enable **Install from unknown sources** in your device settings.
|
||||||
|
|
||||||
|
### Build from source
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/manalejandro/URLFinder.git
|
||||||
|
cd URLFinder
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Open in Android Studio**
|
||||||
|
- File → Open → select the `URLFinder` folder
|
||||||
|
- Let Gradle sync finish
|
||||||
|
|
||||||
|
3. **Run or build**
|
||||||
|
```bash
|
||||||
|
./gradlew assembleDebug
|
||||||
|
# APK → app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
MainActivity
|
||||||
|
└── CameraScreen (Jetpack Compose)
|
||||||
|
├── CameraViewModel
|
||||||
|
│ ├── CameraX (Preview + ImageAnalysis)
|
||||||
|
│ ├── ML Kit Text Recognition → extracts URLs from plain text
|
||||||
|
│ └── ML Kit Barcode Scanner → decodes QR codes
|
||||||
|
├── UrlResultsPanel (LazyColumn of detected URLs)
|
||||||
|
└── SettingsScreen
|
||||||
|
├── History tab (UrlHistoryRepository — SharedPreferences)
|
||||||
|
└── About tab (open-source info + GitHub link)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key design decisions:**
|
||||||
|
- Frame analysis is **throttled to 800 ms** to reduce CPU/battery usage.
|
||||||
|
- `ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST` drops frames that arrive while the previous one is still being processed.
|
||||||
|
- Both text and barcode analyzers run **concurrently** on the same frame, results are merged.
|
||||||
|
- URLs are **deduplicated** per session; the history avoids duplicate entries too.
|
||||||
|
- The history is stored as JSON in `SharedPreferences` (no external DB dependency).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech stack
|
||||||
|
|
||||||
|
| Library | Version | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| Kotlin | 2.0.21 | Programming language |
|
||||||
|
| Jetpack Compose (BOM) | 2024.09.00 | UI toolkit |
|
||||||
|
| Material 3 | — | Design system |
|
||||||
|
| CameraX | 1.4.2 | Camera access and analysis |
|
||||||
|
| ML Kit Text Recognition | 16.0.1 | URL detection in text |
|
||||||
|
| ML Kit Barcode Scanning | 17.3.0 | QR code decoding |
|
||||||
|
| Accompanist Permissions | 0.37.0 | Runtime camera permission |
|
||||||
|
| Lifecycle ViewModel Compose | 2.10.0 | State management |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
| Permission | Reason |
|
||||||
|
|---|---|
|
||||||
|
| `CAMERA` | Required to capture and analyze camera frames for URLs and QR codes |
|
||||||
|
| `INTERNET` | Required to open detected URLs in the browser |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Feel free to:
|
||||||
|
|
||||||
|
- Open an [issue](https://github.com/manalejandro/URLFinder/issues) to report a bug or suggest a feature
|
||||||
|
- Fork the repo and submit a pull request
|
||||||
|
|
||||||
|
Please follow the existing code style (Kotlin + Compose conventions) and add documentation to public APIs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
```
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Manuel Alejandro
|
||||||
|
|
||||||
|
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.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
Made with ❤️ using Kotlin, Jetpack Compose, CameraX and ML Kit<br/>
|
||||||
|
<a href="https://github.com/manalejandro/URLFinder">https://github.com/manalejandro/URLFinder</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
1
app/.gitignore
vendido
Archivo normal
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
70
app/build.gradle.kts
Archivo normal
@@ -0,0 +1,70 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.manalejandro.urlfinder"
|
||||||
|
compileSdk {
|
||||||
|
version = release(36) {
|
||||||
|
minorApiLevel = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.manalejandro.urlfinder"
|
||||||
|
minSdk = 24
|
||||||
|
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
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||||
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
|
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)
|
||||||
|
// CameraX
|
||||||
|
implementation(libs.androidx.camera.core)
|
||||||
|
implementation(libs.androidx.camera.camera2)
|
||||||
|
implementation(libs.androidx.camera.lifecycle)
|
||||||
|
implementation(libs.androidx.camera.view)
|
||||||
|
// ML Kit
|
||||||
|
implementation(libs.mlkit.text.recognition)
|
||||||
|
implementation(libs.mlkit.barcode.scanning)
|
||||||
|
// Permissions
|
||||||
|
implementation(libs.accompanist.permissions)
|
||||||
|
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)
|
||||||
|
}
|
||||||
21
app/proguard-rules.pro
vendido
Archivo normal
@@ -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
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.manalejandro.urlfinder
|
||||||
|
|
||||||
|
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("com.manalejandro.urlfinder", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/src/main/AndroidManifest.xml
Archivo normal
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-feature android:name="android.hardware.camera" android:required="true" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.URLFinder">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/Theme.URLFinder">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
38
app/src/main/assets/ic_urlfinder_logo.svg
Archivo normal
@@ -0,0 +1,38 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#1565C0;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#0D47A1;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.3"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="48" cy="48" r="44" fill="url(#bgGrad)" filter="url(#shadow)"/>
|
||||||
|
|
||||||
|
<!-- Accent ring -->
|
||||||
|
<circle cx="48" cy="48" r="42" fill="none" stroke="#42A5F5" stroke-width="1.5" stroke-opacity="0.4"/>
|
||||||
|
|
||||||
|
<!-- Magnifying glass circle -->
|
||||||
|
<circle cx="42" cy="44" r="18" fill="none" stroke="white" stroke-width="4.5" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Magnifying glass handle -->
|
||||||
|
<line x1="56" y1="58" x2="70" y2="72" stroke="white" stroke-width="4.5" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<!-- Chain link left pill -->
|
||||||
|
<rect x="30" y="40" width="16" height="8" rx="4" fill="white"/>
|
||||||
|
|
||||||
|
<!-- Chain link right pill -->
|
||||||
|
<rect x="38" y="40" width="16" height="8" rx="4" fill="white"/>
|
||||||
|
|
||||||
|
<!-- Middle gap (mask the overlap to look like interlinked) -->
|
||||||
|
<rect x="38" y="41.5" width="8" height="5" rx="0" fill="#1565C0"/>
|
||||||
|
|
||||||
|
<!-- Top-right highlight dot -->
|
||||||
|
<circle cx="72" cy="20" r="5" fill="#64B5F6"/>
|
||||||
|
<circle cx="72" cy="20" r="3" fill="#E3F2FD"/>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
Después Anchura: | Altura: | Tamaño: 1.4 KiB |
BIN
app/src/main/ic_launcher-playstore.png
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 57 KiB |
340
app/src/main/java/com/manalejandro/urlfinder/CameraScreen.kt
Archivo normal
@@ -0,0 +1,340 @@
|
|||||||
|
package com.manalejandro.urlfinder
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import android.Manifest
|
||||||
|
import androidx.camera.view.PreviewView
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
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.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
import com.google.accompanist.permissions.isGranted
|
||||||
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
|
import com.google.accompanist.permissions.shouldShowRationale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root composable for the URLFinder app.
|
||||||
|
* Handles camera permission flow and top-level navigation between [CameraContent]
|
||||||
|
* and [SettingsScreen].
|
||||||
|
*
|
||||||
|
* @param repository History repository shared across screens.
|
||||||
|
* @param viewModel Camera ViewModel, injected by Compose.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
|
@Composable
|
||||||
|
fun CameraScreen(
|
||||||
|
repository: UrlHistoryRepository,
|
||||||
|
viewModel: CameraViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
// Wire repository into the ViewModel once
|
||||||
|
viewModel.historyRepository = repository
|
||||||
|
|
||||||
|
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
|
||||||
|
var showSettings by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// Intercept the system back button/gesture when Settings is open
|
||||||
|
// so it navigates back to the camera instead of closing the app.
|
||||||
|
BackHandler(enabled = showSettings) {
|
||||||
|
showSettings = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSettings) {
|
||||||
|
SettingsScreen(
|
||||||
|
repository = repository,
|
||||||
|
onBack = { showSettings = false }
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
cameraPermissionState.status.isGranted -> {
|
||||||
|
CameraContent(
|
||||||
|
viewModel = viewModel,
|
||||||
|
onOpenSettings = { showSettings = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cameraPermissionState.status.shouldShowRationale -> {
|
||||||
|
PermissionRationaleContent(
|
||||||
|
message = "URLFinder needs access to your camera to scan URLs and QR codes.",
|
||||||
|
onRequestPermission = { cameraPermissionState.launchPermissionRequest() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
PermissionRationaleContent(
|
||||||
|
message = "Camera permission is required to scan URLs and QR codes. Please grant it.",
|
||||||
|
onRequestPermission = { cameraPermissionState.launchPermissionRequest() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-screen camera UI with live [PreviewView], flash toggle, settings button
|
||||||
|
* and the URL results overlay panel.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun CameraContent(
|
||||||
|
viewModel: CameraViewModel,
|
||||||
|
onOpenSettings: () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
val detectedUrls by viewModel.detectedUrls.collectAsState()
|
||||||
|
val isFlashOn by viewModel.isFlashOn.collectAsState()
|
||||||
|
var urlToOpen by remember { mutableStateOf<String?>(null) }
|
||||||
|
val uriHandler = androidx.compose.ui.platform.LocalUriHandler.current
|
||||||
|
|
||||||
|
val previewView = remember { PreviewView(context) }
|
||||||
|
|
||||||
|
// Start (or restart) the camera whenever the lifecycle resumes.
|
||||||
|
// This handles the return from Settings without crashing.
|
||||||
|
DisposableEffect(lifecycleOwner) {
|
||||||
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
|
viewModel.startCamera(context, lifecycleOwner, previewView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleOwner.lifecycle.addObserver(observer)
|
||||||
|
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety warning modal
|
||||||
|
urlToOpen?.let { url ->
|
||||||
|
SafetyWarningDialog(
|
||||||
|
url = url,
|
||||||
|
onConfirm = {
|
||||||
|
runCatching { uriHandler.openUri(url) }
|
||||||
|
urlToOpen = null
|
||||||
|
},
|
||||||
|
onDismiss = { urlToOpen = null }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
// --- Camera preview layer ---
|
||||||
|
AndroidView(
|
||||||
|
factory = { previewView },
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Scanning overlay ---
|
||||||
|
ScanningOverlay()
|
||||||
|
|
||||||
|
// --- Top action bar (flash + settings) — respects status bar height ---
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.statusBarsPadding()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
// Flash toggle button
|
||||||
|
IconButton(
|
||||||
|
onClick = { viewModel.toggleFlash() },
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = if (isFlashOn) Color(0xFFFFD600) else Color.Black.copy(alpha = 0.55f),
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(
|
||||||
|
id = if (isFlashOn) R.drawable.ic_flash_on else R.drawable.ic_flash_off
|
||||||
|
),
|
||||||
|
contentDescription = if (isFlashOn) "Flash on" else "Flash off",
|
||||||
|
tint = if (isFlashOn) Color.Black else Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
// Settings button
|
||||||
|
IconButton(
|
||||||
|
onClick = onOpenSettings,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = Color.Black.copy(alpha = 0.55f),
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Settings,
|
||||||
|
contentDescription = "Settings",
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- URL results panel anchored to the bottom ---
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = detectedUrls.isNotEmpty(),
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter),
|
||||||
|
enter = slideInVertically { it } + fadeIn(),
|
||||||
|
exit = fadeOut()
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.93f),
|
||||||
|
tonalElevation = 8.dp,
|
||||||
|
shadowElevation = 8.dp,
|
||||||
|
shape = MaterialTheme.shapes.large
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(top = 8.dp)) {
|
||||||
|
// Header
|
||||||
|
Text(
|
||||||
|
text = "Detected URLs (${detectedUrls.size})",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// URL list — items open the safety warning dialog
|
||||||
|
UrlResultsPanel(
|
||||||
|
urls = detectedUrls,
|
||||||
|
onUrlClick = { urlToOpen = it.url }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// Clear button — full width at the bottom of the panel
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.clearResults() },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_link),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Text("Clear results")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semi-transparent scanning overlay with corner guides and a hint label.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun ScanningOverlay() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_scan_frame),
|
||||||
|
contentDescription = "Scan area",
|
||||||
|
tint = Color.White.copy(alpha = 0.8f),
|
||||||
|
modifier = Modifier.size(240.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Point the camera at a URL or QR code",
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 24.dp, vertical = 8.dp)
|
||||||
|
.background(
|
||||||
|
Color.Black.copy(alpha = 0.45f),
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displayed when camera permission has not been granted yet.
|
||||||
|
*
|
||||||
|
* @param message Human-readable explanation of why the permission is needed.
|
||||||
|
* @param onRequestPermission Lambda called when the user taps the grant button.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun PermissionRationaleContent(message: String, onRequestPermission: () -> Unit) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.padding(32.dp)
|
||||||
|
) {
|
||||||
|
// Logo shown in full color (no tint)
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_urlfinder_logo),
|
||||||
|
contentDescription = "URLFinder logo",
|
||||||
|
tint = Color.Unspecified,
|
||||||
|
modifier = Modifier.size(96.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "URLFinder",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(bottom = 24.dp)
|
||||||
|
)
|
||||||
|
Button(onClick = onRequestPermission) {
|
||||||
|
Text("Grant Camera Permission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
237
app/src/main/java/com/manalejandro/urlfinder/CameraViewModel.kt
Archivo normal
@@ -0,0 +1,237 @@
|
|||||||
|
package com.manalejandro.urlfinder
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.Patterns
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.camera.core.Camera
|
||||||
|
import androidx.camera.core.CameraSelector
|
||||||
|
import androidx.camera.core.ExperimentalGetImage
|
||||||
|
import androidx.camera.core.ImageAnalysis
|
||||||
|
import androidx.camera.core.ImageProxy
|
||||||
|
import androidx.camera.core.Preview
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.camera.view.PreviewView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||||
|
import com.google.mlkit.vision.barcode.common.Barcode
|
||||||
|
import com.google.mlkit.vision.common.InputImage
|
||||||
|
import com.google.mlkit.vision.text.TextRecognition
|
||||||
|
import com.google.mlkit.vision.text.latin.TextRecognizerOptions
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
|
||||||
|
/** Source type that originated the detected URL */
|
||||||
|
enum class UrlSource { TEXT, QR_CODE }
|
||||||
|
|
||||||
|
/** A URL found by the scanner with its source type */
|
||||||
|
data class DetectedUrl(
|
||||||
|
val url: String,
|
||||||
|
val source: UrlSource
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel responsible for managing the camera lifecycle and URL detection pipeline.
|
||||||
|
* Uses CameraX for camera access, ML Kit Text Recognition for detecting URLs in
|
||||||
|
* plain text, and ML Kit Barcode Scanning for QR codes.
|
||||||
|
*/
|
||||||
|
class CameraViewModel : ViewModel() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "CameraViewModel"
|
||||||
|
/** Minimum interval between frame analyses in milliseconds (throttle) */
|
||||||
|
private const val ANALYSIS_INTERVAL_MS = 800L
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _detectedUrls = MutableStateFlow<List<DetectedUrl>>(emptyList())
|
||||||
|
/** Live stream of all URLs detected in the current camera session */
|
||||||
|
val detectedUrls: StateFlow<List<DetectedUrl>> = _detectedUrls.asStateFlow()
|
||||||
|
|
||||||
|
private val _isScanning = MutableStateFlow(false)
|
||||||
|
/** Whether the camera analyzer is actively processing frames */
|
||||||
|
val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
|
||||||
|
|
||||||
|
private val _isFlashOn = MutableStateFlow(false)
|
||||||
|
/** Whether the camera torch/flash is currently enabled */
|
||||||
|
val isFlashOn: StateFlow<Boolean> = _isFlashOn.asStateFlow()
|
||||||
|
|
||||||
|
/** Exposed repository — injected from outside (e.g. from Application context) */
|
||||||
|
var historyRepository: UrlHistoryRepository? = null
|
||||||
|
|
||||||
|
private val analysisExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
private val textRecognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
|
||||||
|
private val barcodeScanner = BarcodeScanning.getClient()
|
||||||
|
private val lastAnalysisTimestamp = AtomicLong(0L)
|
||||||
|
private val isProcessing = AtomicBoolean(false)
|
||||||
|
private var camera: Camera? = null
|
||||||
|
|
||||||
|
/** URL regex backed by Android's built-in Patterns.WEB_URL */
|
||||||
|
private val urlRegex = Patterns.WEB_URL.toRegex()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds CameraX use cases (Preview + ImageAnalysis) to the given [lifecycleOwner].
|
||||||
|
*
|
||||||
|
* @param context Application or Activity context used to get the camera provider.
|
||||||
|
* @param lifecycleOwner The lifecycle owner that controls camera lifetime.
|
||||||
|
* @param previewView The [PreviewView] that will render the camera feed.
|
||||||
|
*/
|
||||||
|
fun startCamera(
|
||||||
|
context: Context,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
previewView: PreviewView
|
||||||
|
) {
|
||||||
|
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||||
|
cameraProviderFuture.addListener({
|
||||||
|
val cameraProvider = cameraProviderFuture.get()
|
||||||
|
|
||||||
|
val preview = Preview.Builder().build().also {
|
||||||
|
it.surfaceProvider = previewView.surfaceProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageAnalysis = ImageAnalysis.Builder()
|
||||||
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||||
|
.build()
|
||||||
|
.also { analysis ->
|
||||||
|
analysis.setAnalyzer(analysisExecutor) { imageProxy ->
|
||||||
|
analyzeFrame(imageProxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
cameraProvider.unbindAll()
|
||||||
|
camera = cameraProvider.bindToLifecycle(
|
||||||
|
lifecycleOwner,
|
||||||
|
CameraSelector.DEFAULT_BACK_CAMERA,
|
||||||
|
preview,
|
||||||
|
imageAnalysis
|
||||||
|
)
|
||||||
|
// Restore flash state if it was on before rebind
|
||||||
|
camera?.cameraControl?.enableTorch(_isFlashOn.value)
|
||||||
|
_isScanning.value = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Camera binding failed", e)
|
||||||
|
}
|
||||||
|
}, ContextCompat.getMainExecutor(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the camera torch on or off.
|
||||||
|
*/
|
||||||
|
fun toggleFlash() {
|
||||||
|
val newState = !_isFlashOn.value
|
||||||
|
_isFlashOn.value = newState
|
||||||
|
camera?.cameraControl?.enableTorch(newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyzes a single camera [frame][ImageProxy] for URLs (text) and QR codes.
|
||||||
|
* Throttles analysis to [ANALYSIS_INTERVAL_MS] ms to reduce CPU usage.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalGetImage::class)
|
||||||
|
private fun analyzeFrame(imageProxy: ImageProxy) {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val elapsed = now - lastAnalysisTimestamp.get()
|
||||||
|
|
||||||
|
// Throttle: skip frames that arrive too quickly
|
||||||
|
if (elapsed < ANALYSIS_INTERVAL_MS || !isProcessing.compareAndSet(false, true)) {
|
||||||
|
imageProxy.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastAnalysisTimestamp.set(now)
|
||||||
|
|
||||||
|
val mediaImage = imageProxy.image
|
||||||
|
if (mediaImage == null) {
|
||||||
|
imageProxy.close()
|
||||||
|
isProcessing.set(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
||||||
|
val foundUrls = mutableListOf<DetectedUrl>()
|
||||||
|
var pendingTasks = 2
|
||||||
|
|
||||||
|
fun checkDone() {
|
||||||
|
pendingTasks--
|
||||||
|
if (pendingTasks == 0) {
|
||||||
|
mergeResults(foundUrls)
|
||||||
|
imageProxy.close()
|
||||||
|
isProcessing.set(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Text recognition (URLs in plain text) ---
|
||||||
|
textRecognizer.process(inputImage)
|
||||||
|
.addOnSuccessListener { visionText ->
|
||||||
|
val urls = urlRegex.findAll(visionText.text)
|
||||||
|
.map { it.value.normalizeUrl() }
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.map { DetectedUrl(it, UrlSource.TEXT) }
|
||||||
|
.toList()
|
||||||
|
synchronized(foundUrls) { foundUrls.addAll(urls) }
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e -> Log.e(TAG, "Text recognition failed", e) }
|
||||||
|
.addOnCompleteListener { checkDone() }
|
||||||
|
|
||||||
|
// --- Barcode / QR code scanning ---
|
||||||
|
barcodeScanner.process(inputImage)
|
||||||
|
.addOnSuccessListener { barcodes ->
|
||||||
|
val urls = barcodes
|
||||||
|
.filter { it.valueType == Barcode.TYPE_URL || it.format == Barcode.FORMAT_QR_CODE }
|
||||||
|
.mapNotNull { it.url?.url ?: it.rawValue }
|
||||||
|
.filter { it.looksLikeUrl() }
|
||||||
|
.map { DetectedUrl(it.normalizeUrl(), UrlSource.QR_CODE) }
|
||||||
|
synchronized(foundUrls) { foundUrls.addAll(urls) }
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e -> Log.e(TAG, "Barcode scanning failed", e) }
|
||||||
|
.addOnCompleteListener { checkDone() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges newly detected URLs with the existing accumulated list,
|
||||||
|
* avoiding exact-URL duplicates. Also saves to history.
|
||||||
|
*/
|
||||||
|
private fun mergeResults(newUrls: List<DetectedUrl>) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
if (newUrls.isEmpty()) return@launch
|
||||||
|
val existing = _detectedUrls.value.map { it.url }.toSet()
|
||||||
|
val unique = newUrls.filter { it.url !in existing }
|
||||||
|
if (unique.isNotEmpty()) {
|
||||||
|
_detectedUrls.value = _detectedUrls.value + unique
|
||||||
|
unique.forEach { historyRepository?.add(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clears the accumulated URL list so the user can start a fresh scan */
|
||||||
|
fun clearResults() {
|
||||||
|
_detectedUrls.value = emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
analysisExecutor.shutdown()
|
||||||
|
textRecognizer.close()
|
||||||
|
barcodeScanner.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Extension helpers ---
|
||||||
|
|
||||||
|
/** Ensures a URL string starts with a proper HTTP/HTTPS scheme */
|
||||||
|
private fun String.normalizeUrl(): String =
|
||||||
|
if (startsWith("http://") || startsWith("https://")) this
|
||||||
|
else "https://$this"
|
||||||
|
|
||||||
|
/** Returns true if the string looks like a URL */
|
||||||
|
private fun String.looksLikeUrl(): Boolean =
|
||||||
|
Patterns.WEB_URL.matcher(this).matches() ||
|
||||||
|
startsWith("http://") ||
|
||||||
|
startsWith("https://")
|
||||||
32
app/src/main/java/com/manalejandro/urlfinder/MainActivity.kt
Archivo normal
@@ -0,0 +1,32 @@
|
|||||||
|
package com.manalejandro.urlfinder
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.manalejandro.urlfinder.ui.theme.URLFinderTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single-activity entry point for URLFinder.
|
||||||
|
* Creates the [UrlHistoryRepository] and hosts [CameraScreen].
|
||||||
|
*/
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
setContent {
|
||||||
|
URLFinderTheme {
|
||||||
|
Surface(modifier = Modifier.fillMaxSize()) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val repository = remember { UrlHistoryRepository(context) }
|
||||||
|
CameraScreen(repository = repository)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
451
app/src/main/java/com/manalejandro/urlfinder/SettingsScreen.kt
Archivo normal
@@ -0,0 +1,451 @@
|
|||||||
|
package com.manalejandro.urlfinder
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.TabRow
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
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.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings screen with two tabs: URL History and About.
|
||||||
|
*
|
||||||
|
* @param repository History repository to read/clear entries.
|
||||||
|
* @param onBack Callback invoked when the user taps the back button.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(
|
||||||
|
repository: UrlHistoryRepository,
|
||||||
|
onBack: () -> Unit
|
||||||
|
) {
|
||||||
|
var selectedTab by remember { mutableIntStateOf(0) }
|
||||||
|
val tabs = listOf("History", "About")
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Settings") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Back"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
TabRow(selectedTabIndex = selectedTab) {
|
||||||
|
tabs.forEachIndexed { index, title ->
|
||||||
|
Tab(
|
||||||
|
selected = selectedTab == index,
|
||||||
|
onClick = { selectedTab = index },
|
||||||
|
text = { Text(title) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (selectedTab) {
|
||||||
|
0 -> HistoryTab(repository = repository)
|
||||||
|
1 -> AboutTab()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// History Tab
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HistoryTab(repository: UrlHistoryRepository) {
|
||||||
|
val history by repository.history.collectAsState()
|
||||||
|
var urlToOpen by remember { mutableStateOf<String?>(null) }
|
||||||
|
var showClearConfirm by remember { mutableStateOf(false) }
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
|
// Dangerous-URL warning dialog
|
||||||
|
urlToOpen?.let { url ->
|
||||||
|
SafetyWarningDialog(
|
||||||
|
url = url,
|
||||||
|
onConfirm = {
|
||||||
|
runCatching { uriHandler.openUri(url) }
|
||||||
|
urlToOpen = null
|
||||||
|
},
|
||||||
|
onDismiss = { urlToOpen = null }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all confirmation dialog
|
||||||
|
if (showClearConfirm) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showClearConfirm = false },
|
||||||
|
title = { Text("Clear history") },
|
||||||
|
text = { Text("Are you sure you want to delete all saved URLs? This cannot be undone.") },
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
repository.clear()
|
||||||
|
showClearConfirm = false
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
) { Text("Delete all") }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showClearConfirm = false }) { Text("Cancel") }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (history.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_link),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Text(
|
||||||
|
text = "No history yet",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Scanned URLs will appear here",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
items(history, key = { it.id }) { entry ->
|
||||||
|
HistoryItem(
|
||||||
|
entry = entry,
|
||||||
|
onClick = { urlToOpen = entry.url }
|
||||||
|
)
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all button — full width at the bottom
|
||||||
|
Button(
|
||||||
|
onClick = { showClearConfirm = true },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Text("Clear all history")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HistoryItem(entry: HistoryEntry, onClick: () -> Unit) {
|
||||||
|
val dateFormat = remember { SimpleDateFormat("MMM d, yyyy · HH:mm", Locale.getDefault()) }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 10.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(
|
||||||
|
id = if (entry.source == UrlSource.QR_CODE) R.drawable.ic_qr_code else R.drawable.ic_link
|
||||||
|
),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (entry.source == UrlSource.QR_CODE)
|
||||||
|
MaterialTheme.colorScheme.secondary
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = entry.url,
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = dateFormat.format(Date(entry.timestamp)),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_open_in_browser),
|
||||||
|
contentDescription = "Open in browser",
|
||||||
|
tint = MaterialTheme.colorScheme.outline,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// About Tab
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AboutTab() {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
// Multicolor logo (not tinted)
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_urlfinder_logo),
|
||||||
|
contentDescription = "URLFinder logo",
|
||||||
|
tint = androidx.compose.ui.graphics.Color.Unspecified,
|
||||||
|
modifier = Modifier.size(96.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "URLFinder",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Version 1.0",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
HorizontalDivider()
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "URLFinder is a free and open-source Android app that uses your " +
|
||||||
|
"device camera to detect URLs in text and QR codes in real time, " +
|
||||||
|
"letting you open them directly in your browser.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_open_source),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Free & Open Source Software",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = "URLFinder is released under the MIT License. " +
|
||||||
|
"You are free to use, modify and distribute it.",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
runCatching {
|
||||||
|
uriHandler.openUri("https://github.com/manalejandro/URLFinder")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_github),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Text("View on GitHub")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
HorizontalDivider()
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "Built with ❤️ using Kotlin, Jetpack Compose, CameraX & ML Kit",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Safety Warning Dialog
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal dialog warning the user that the URL may be dangerous before opening it.
|
||||||
|
*
|
||||||
|
* @param url The URL that is about to be opened.
|
||||||
|
* @param onConfirm Called when the user accepts the risk and wants to proceed.
|
||||||
|
* @param onDismiss Called when the user cancels.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SafetyWarningDialog(
|
||||||
|
url: String,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_warning),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = { Text("Open URL?") },
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "⚠️ This URL may be dangerous. You are responsible for any " +
|
||||||
|
"content you access through it.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = url,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
onClick = onConfirm,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
) { Text("Open anyway") }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) { Text("Cancel") }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
91
app/src/main/java/com/manalejandro/urlfinder/UrlHistoryRepository.kt
Archivo normal
@@ -0,0 +1,91 @@
|
|||||||
|
package com.manalejandro.urlfinder
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A persisted URL history entry with a stable unique [id], timestamp and source type.
|
||||||
|
*/
|
||||||
|
data class HistoryEntry(
|
||||||
|
val id: String = UUID.randomUUID().toString(),
|
||||||
|
val url: String,
|
||||||
|
val source: UrlSource,
|
||||||
|
val timestamp: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository that persists detected URLs locally using SharedPreferences (JSON).
|
||||||
|
* Provides a [StateFlow] of [HistoryEntry] items and operations to add/clear them.
|
||||||
|
*/
|
||||||
|
class UrlHistoryRepository(context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREFS_NAME = "url_history_prefs"
|
||||||
|
private const val KEY_HISTORY = "history"
|
||||||
|
private const val MAX_ENTRIES = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
private val _history = MutableStateFlow<List<HistoryEntry>>(load())
|
||||||
|
/** Live list of persisted URL history entries, newest first. */
|
||||||
|
val history: StateFlow<List<HistoryEntry>> = _history.asStateFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a [DetectedUrl] to history if it is not already present.
|
||||||
|
* Keeps at most [MAX_ENTRIES] entries.
|
||||||
|
*/
|
||||||
|
fun add(detectedUrl: DetectedUrl) {
|
||||||
|
val current = _history.value
|
||||||
|
if (current.any { it.url == detectedUrl.url }) return
|
||||||
|
val entry = HistoryEntry(url = detectedUrl.url, source = detectedUrl.source)
|
||||||
|
val updated = (listOf(entry) + current).take(MAX_ENTRIES)
|
||||||
|
_history.value = updated
|
||||||
|
save(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes all history entries from memory and disk. */
|
||||||
|
fun clear() {
|
||||||
|
_history.value = emptyList()
|
||||||
|
prefs.edit { remove(KEY_HISTORY) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Serialization ---
|
||||||
|
|
||||||
|
private fun save(entries: List<HistoryEntry>) {
|
||||||
|
val array = JSONArray()
|
||||||
|
entries.forEach { e ->
|
||||||
|
val obj = JSONObject().apply {
|
||||||
|
put("id", e.id)
|
||||||
|
put("url", e.url)
|
||||||
|
put("source", e.source.name)
|
||||||
|
put("ts", e.timestamp)
|
||||||
|
}
|
||||||
|
array.put(obj)
|
||||||
|
}
|
||||||
|
prefs.edit { putString(KEY_HISTORY, array.toString()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load(): List<HistoryEntry> {
|
||||||
|
val json = prefs.getString(KEY_HISTORY, null) ?: return emptyList()
|
||||||
|
return runCatching {
|
||||||
|
val array = JSONArray(json)
|
||||||
|
(0 until array.length()).map { i ->
|
||||||
|
val obj = array.getJSONObject(i)
|
||||||
|
HistoryEntry(
|
||||||
|
id = obj.optString("id", UUID.randomUUID().toString()),
|
||||||
|
url = obj.getString("url"),
|
||||||
|
source = UrlSource.valueOf(obj.getString("source")),
|
||||||
|
timestamp = obj.getLong("ts")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.getOrDefault(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
130
app/src/main/java/com/manalejandro/urlfinder/UrlResultsPanel.kt
Archivo normal
@@ -0,0 +1,130 @@
|
|||||||
|
package com.manalejandro.urlfinder
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a scrollable list of [DetectedUrl] items.
|
||||||
|
* Each item triggers [onUrlClick] when tapped instead of opening the browser
|
||||||
|
* directly, allowing the caller to show a safety warning first.
|
||||||
|
*
|
||||||
|
* @param urls The list of detected URLs to display.
|
||||||
|
* @param onUrlClick Called with the tapped [DetectedUrl].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun UrlResultsPanel(
|
||||||
|
urls: List<DetectedUrl>,
|
||||||
|
onUrlClick: (DetectedUrl) -> Unit = {}
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 300.dp)
|
||||||
|
) {
|
||||||
|
itemsIndexed(urls) { index, detectedUrl ->
|
||||||
|
UrlItem(detectedUrl = detectedUrl, onClick = { onUrlClick(detectedUrl) })
|
||||||
|
if (index < urls.lastIndex) {
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single URL card item.
|
||||||
|
* Calls [onClick] when tapped so the parent can handle navigation/warnings.
|
||||||
|
*
|
||||||
|
* @param detectedUrl The URL data to display.
|
||||||
|
* @param onClick Callback invoked on tap.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun UrlItem(detectedUrl: DetectedUrl, onClick: () -> Unit) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp)
|
||||||
|
) {
|
||||||
|
// Icon varies by source type
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(
|
||||||
|
id = if (detectedUrl.source == UrlSource.QR_CODE)
|
||||||
|
R.drawable.ic_qr_code
|
||||||
|
else
|
||||||
|
R.drawable.ic_link
|
||||||
|
),
|
||||||
|
contentDescription = if (detectedUrl.source == UrlSource.QR_CODE)
|
||||||
|
"QR Code URL"
|
||||||
|
else
|
||||||
|
"Text URL",
|
||||||
|
tint = if (detectedUrl.source == UrlSource.QR_CODE)
|
||||||
|
MaterialTheme.colorScheme.secondary
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = if (detectedUrl.source == UrlSource.QR_CODE) "QR Code" else "Text",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = detectedUrl.url,
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_open_in_browser),
|
||||||
|
contentDescription = "Open in browser",
|
||||||
|
tint = MaterialTheme.colorScheme.outline,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/src/main/java/com/manalejandro/urlfinder/ui/theme/Color.kt
Archivo normal
@@ -0,0 +1,11 @@
|
|||||||
|
package com.manalejandro.urlfinder.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)
|
||||||
58
app/src/main/java/com/manalejandro/urlfinder/ui/theme/Theme.kt
Archivo normal
@@ -0,0 +1,58 @@
|
|||||||
|
package com.manalejandro.urlfinder.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.platform.LocalContext
|
||||||
|
|
||||||
|
private val DarkColorScheme = darkColorScheme(
|
||||||
|
primary = Purple80,
|
||||||
|
secondary = PurpleGrey80,
|
||||||
|
tertiary = Pink80
|
||||||
|
)
|
||||||
|
|
||||||
|
private val LightColorScheme = lightColorScheme(
|
||||||
|
primary = Purple40,
|
||||||
|
secondary = PurpleGrey40,
|
||||||
|
tertiary = Pink40
|
||||||
|
|
||||||
|
/* Other default colors to override
|
||||||
|
background = Color(0xFFFFFBFE),
|
||||||
|
surface = Color(0xFFFFFBFE),
|
||||||
|
onPrimary = Color.White,
|
||||||
|
onSecondary = Color.White,
|
||||||
|
onTertiary = Color.White,
|
||||||
|
onBackground = Color(0xFF1C1B1F),
|
||||||
|
onSurface = Color(0xFF1C1B1F),
|
||||||
|
*/
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun URLFinderTheme(
|
||||||
|
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 -> DarkColorScheme
|
||||||
|
else -> LightColorScheme
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = Typography,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
34
app/src/main/java/com/manalejandro/urlfinder/ui/theme/Type.kt
Archivo normal
@@ -0,0 +1,34 @@
|
|||||||
|
package com.manalejandro.urlfinder.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
|
||||||
|
|
||||||
|
// Set of Material typography styles to start with
|
||||||
|
val Typography = Typography(
|
||||||
|
bodyLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
/* Other default text styles to override
|
||||||
|
titleLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 22.sp,
|
||||||
|
lineHeight = 28.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
labelSmall = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
)
|
||||||
10
app/src/main/res/drawable/ic_flash_off.xml
Archivo normal
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/black"
|
||||||
|
android:pathData="M3.27,3L2,4.27l5,5V13h3v9l3.58,-6.14L17.73,20 19,18.73 3.27,3zM17,10h-4l4,-8H7v2.18l8.46,8.46L17,10z"/>
|
||||||
|
</vector>
|
||||||
|
|
||||||
10
app/src/main/res/drawable/ic_flash_on.xml
Archivo normal
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/black"
|
||||||
|
android:pathData="M7,2v11h3v9l7,-12h-4l4,-8z"/>
|
||||||
|
</vector>
|
||||||
|
|
||||||
11
app/src/main/res/drawable/ic_github.xml
Archivo normal
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<!-- GitHub Octocat simplified outline -->
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/black"
|
||||||
|
android:pathData="M12,2A10,10 0,0 0,2 12c0,4.42 2.87,8.17 6.84,9.5 0.5,0.08 0.66,-0.23 0.66,-0.5v-1.69c-2.77,0.6 -3.36,-1.34 -3.36,-1.34 -0.46,-1.16 -1.11,-1.47 -1.11,-1.47 -0.91,-0.62 0.07,-0.6 0.07,-0.6 1,0.07 1.53,1.03 1.53,1.03 0.87,1.52 2.34,1.07 2.91,0.83 0.09,-0.65 0.35,-1.09 0.63,-1.34 -2.22,-0.25 -4.55,-1.11 -4.55,-4.92 0,-1.11 0.38,-2 1.03,-2.71 -0.1,-0.25 -0.44,-1.29 0.1,-2.64 0,0 0.84,-0.27 2.75,1.02 0.79,-0.22 1.65,-0.33 2.5,-0.33s1.71,0.11 2.5,0.33c1.91,-1.29 2.75,-1.02 2.75,-1.02 0.55,1.35 0.2,2.39 0.1,2.64 0.65,0.71 1.03,1.6 1.03,2.71 0,3.82 -2.34,4.66 -4.57,4.91 0.36,0.31 0.69,0.92 0.69,1.85V21c0,0.27 0.16,0.59 0.67,0.5C19.14,20.16 22,16.42 22,12A10,10 0,0 0,12 2Z"/>
|
||||||
|
</vector>
|
||||||
|
|
||||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Archivo normal
@@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
</vector>
|
||||||
59
app/src/main/res/drawable/ic_launcher_foreground.xml
Archivo normal
@@ -0,0 +1,59 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="96"
|
||||||
|
android:viewportHeight="96">
|
||||||
|
<group android:scaleX="0.58"
|
||||||
|
android:scaleY="0.58"
|
||||||
|
android:translateX="20.16"
|
||||||
|
android:translateY="20.16">
|
||||||
|
<path
|
||||||
|
android:pathData="M48,48m-44,0a44,44 0,1 1,88 0a44,44 0,1 1,-88 0">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="4"
|
||||||
|
android:startY="4"
|
||||||
|
android:endX="92"
|
||||||
|
android:endY="92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FF1565C0"/>
|
||||||
|
<item android:offset="1" android:color="#FF0D47A1"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M48,48m-42,0a42,42 0,1 1,84 0a42,42 0,1 1,-84 0"
|
||||||
|
android:strokeAlpha="0.4"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#42A5F5"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M42,44m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"
|
||||||
|
android:strokeWidth="4.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#ffffff"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M56,58L70,72"
|
||||||
|
android:strokeWidth="4.5"
|
||||||
|
android:strokeColor="#ffffff"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M34,40L42,40A4,4 0,0 1,46 44L46,44A4,4 0,0 1,42 48L34,48A4,4 0,0 1,30 44L30,44A4,4 0,0 1,34 40z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M42,40L50,40A4,4 0,0 1,54 44L54,44A4,4 0,0 1,50 48L42,48A4,4 0,0 1,38 44L38,44A4,4 0,0 1,42 40z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M38,41.5h8v5h-8z"
|
||||||
|
android:fillColor="#1565C0"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M72,20m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||||
|
android:fillColor="#64B5F6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M72,20m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
|
||||||
|
android:fillColor="#E3F2FD"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
10
app/src/main/res/drawable/ic_link.xml
Archivo normal
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/black"
|
||||||
|
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
|
||||||
|
</vector>
|
||||||
|
|
||||||
10
app/src/main/res/drawable/ic_open_in_browser.xml
Archivo normal
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/black"
|
||||||
|
android:pathData="M19,19L5,19L5,5h7L12,3L5,3C3.89,3 3,3.9 3,5v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41L19,10h2L21,3h-7z"/>
|
||||||
|
</vector>
|
||||||
|
|
||||||
11
app/src/main/res/drawable/ic_open_source.xml
Archivo normal
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<!-- Open source / code icon -->
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/black"
|
||||||
|
android:pathData="M9.4,16.6L4.8,12l4.6,-4.6L8,6l-6,6 6,6 1.4,-1.4zM14.6,16.6l4.6,-4.6 -4.6,-4.6L16,6l6,6 -6,6 -1.4,-1.4z"/>
|
||||||
|
</vector>
|
||||||
|
|
||||||
10
app/src/main/res/drawable/ic_qr_code.xml
Archivo normal
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/black"
|
||||||
|
android:pathData="M3,11L5,11L5,13L3,13L3,11ZM11,5L13,5L13,7L11,7L11,5ZM9,3L9,9L3,9L3,3L9,3ZM7,5L5,5L5,7L7,7L7,5ZM15,3L21,3L21,9L15,9L15,3ZM17,5L17,7L19,7L19,5L17,5ZM3,15L9,15L9,21L3,21L3,15ZM5,17L5,19L7,19L7,17L5,17ZM21,11L21,13L17,13L17,11L21,11ZM13,13L13,15L11,15L11,13L13,13ZM15,11L15,13L17,13L17,11L15,11ZM19,15L21,15L21,17L19,17L19,15ZM13,17L13,21L11,21L11,17L13,17ZM15,19L15,21L17,21L17,19L15,19ZM19,17L21,17L21,21L19,21L19,19ZM17,15L19,15L19,17L17,17L17,15Z"/>
|
||||||
|
</vector>
|
||||||
|
|
||||||
58
app/src/main/res/drawable/ic_scan_frame.xml
Archivo normal
@@ -0,0 +1,58 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="240dp"
|
||||||
|
android:height="240dp"
|
||||||
|
android:viewportWidth="240"
|
||||||
|
android:viewportHeight="240">
|
||||||
|
|
||||||
|
<!-- Top-left corner -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeWidth="6"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M20,60 L20,20 L60,20" />
|
||||||
|
|
||||||
|
<!-- Top-right corner -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeWidth="6"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M180,20 L220,20 L220,60" />
|
||||||
|
|
||||||
|
<!-- Bottom-left corner -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeWidth="6"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M20,180 L20,220 L60,220" />
|
||||||
|
|
||||||
|
<!-- Bottom-right corner -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeWidth="6"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M180,220 L220,220 L220,180" />
|
||||||
|
|
||||||
|
<!-- Center crosshair horizontal -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:strokeAlpha="0.5"
|
||||||
|
android:pathData="M100,120 L140,120" />
|
||||||
|
|
||||||
|
<!-- Center crosshair vertical -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:strokeAlpha="0.5"
|
||||||
|
android:pathData="M120,100 L120,140" />
|
||||||
|
|
||||||
|
</vector>
|
||||||
|
|
||||||
63
app/src/main/res/drawable/ic_urlfinder_logo.xml
Archivo normal
@@ -0,0 +1,63 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="96dp"
|
||||||
|
android:height="96dp"
|
||||||
|
android:viewportWidth="96"
|
||||||
|
android:viewportHeight="96">
|
||||||
|
|
||||||
|
<!-- Background circle — deep blue -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#1565C0"
|
||||||
|
android:pathData="M48,4 C23.699,4 4,23.699 4,48 C4,72.301 23.699,92 48,92 C72.301,92 92,72.301 92,48 C92,23.699 72.301,4 48,4 Z" />
|
||||||
|
|
||||||
|
<!-- Subtle inner ring -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#42A5F5"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M48,8 C26.461,8 9,25.461 9,47 C9,68.539 26.461,86 48,86 C69.539,86 87,68.539 87,47 C87,25.461 69.539,8 48,8 Z" />
|
||||||
|
|
||||||
|
<!-- Magnifying glass circle — white -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeWidth="4.5"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M42,25 C29.85,25 20,34.85 20,47 C20,59.15 29.85,69 42,69 C54.15,69 64,59.15 64,47 C64,34.85 54.15,25 42,25 Z" />
|
||||||
|
|
||||||
|
<!-- Magnifying glass handle — white -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeWidth="4.5"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M58,62 L73,77" />
|
||||||
|
|
||||||
|
<!-- Chain link background — semi-transparent white -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#33FFFFFF"
|
||||||
|
android:pathData="M29,43 C29,40.239 31.239,38 34,38 L50,38 C52.761,38 55,40.239 55,43 L55,51 C55,53.761 52.761,56 50,56 L34,56 C31.239,56 29,53.761 29,51 Z" />
|
||||||
|
|
||||||
|
<!-- Chain link left — white -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M29,43 C29,40.791 30.791,39 33,39 L42,39 C44.209,39 46,40.791 46,43 L46,51 C46,53.209 44.209,55 42,55 L33,55 C30.791,55 29,53.209 29,51 Z" />
|
||||||
|
|
||||||
|
<!-- Chain link right — light blue -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#90CAF9"
|
||||||
|
android:pathData="M38,43 C38,40.791 39.791,39 42,39 L51,39 C53.209,39 55,40.791 55,43 L55,51 C55,53.209 53.209,55 51,55 L42,55 C39.791,55 38,53.209 38,51 Z" />
|
||||||
|
|
||||||
|
<!-- Center gap to simulate interlinked chain -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#1565C0"
|
||||||
|
android:pathData="M41,40 L43,40 L43,54 L41,54 Z" />
|
||||||
|
|
||||||
|
<!-- Accent dot — light blue -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#64B5F6"
|
||||||
|
android:pathData="M72,14 C72,11.239 74.239,9 77,9 C79.761,9 82,11.239 82,14 C82,16.761 79.761,19 77,19 C74.239,19 72,16.761 72,14 Z" />
|
||||||
|
|
||||||
|
<!-- Accent dot inner — white -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M74.5,14 C74.5,12.619 75.619,11.5 77,11.5 C78.381,11.5 79.5,12.619 79.5,14 C79.5,15.381 78.381,16.5 77,16.5 C75.619,16.5 74.5,15.381 74.5,14 Z" />
|
||||||
|
|
||||||
|
</vector>
|
||||||
10
app/src/main/res/drawable/ic_warning.xml
Archivo normal
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/black"
|
||||||
|
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
|
||||||
|
</vector>
|
||||||
|
|
||||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Archivo normal
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Archivo normal
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 2.6 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 4.5 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 1.7 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 2.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 3.6 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 6.7 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 5.5 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 10 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 7.4 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Archivo normal
|
Después Anchura: | Altura: | Tamaño: 14 KiB |
10
app/src/main/res/values/colors.xml
Archivo normal
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
||||||
4
app/src/main/res/values/ic_launcher_background.xml
Archivo normal
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#145CB7</color>
|
||||||
|
</resources>
|
||||||
9
app/src/main/res/values/strings.xml
Archivo normal
@@ -0,0 +1,9 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">URLFinder</string>
|
||||||
|
<string name="permission_camera_rationale">URLFinder needs camera access to scan URLs and QR codes.</string>
|
||||||
|
<string name="grant_permission">Grant Camera Permission</string>
|
||||||
|
<string name="detected_urls">Detected URLs</string>
|
||||||
|
<string name="scan_hint">Point the camera at a URL or QR code</string>
|
||||||
|
<string name="clear_results">Clear results</string>
|
||||||
|
<string name="open_in_browser">Open in browser</string>
|
||||||
|
</resources>
|
||||||
5
app/src/main/res/values/themes.xml
Archivo normal
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Theme.URLFinder" parent="android:Theme.Material.Light.NoActionBar" />
|
||||||
|
</resources>
|
||||||
13
app/src/main/res/xml/backup_rules.xml
Archivo normal
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample backup rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/guide/topics/data/autobackup
|
||||||
|
for details.
|
||||||
|
Note: This file is ignored for devices older than API 31
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore
|
||||||
|
-->
|
||||||
|
<full-backup-content>
|
||||||
|
<!--
|
||||||
|
<include domain="sharedpref" path="."/>
|
||||||
|
<exclude domain="sharedpref" path="device.xml"/>
|
||||||
|
-->
|
||||||
|
</full-backup-content>
|
||||||
19
app/src/main/res/xml/data_extraction_rules.xml
Archivo normal
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
Sample data extraction rules file; uncomment and customize as necessary.
|
||||||
|
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||||
|
for details.
|
||||||
|
-->
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup>
|
||||||
|
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
-->
|
||||||
|
</cloud-backup>
|
||||||
|
<!--
|
||||||
|
<device-transfer>
|
||||||
|
<include .../>
|
||||||
|
<exclude .../>
|
||||||
|
</device-transfer>
|
||||||
|
-->
|
||||||
|
</data-extraction-rules>
|
||||||
17
app/src/test/java/com/manalejandro/urlfinder/ExampleUnitTest.kt
Archivo normal
@@ -0,0 +1,17 @@
|
|||||||
|
package com.manalejandro.urlfinder
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
build.gradle.kts
Archivo normal
@@ -0,0 +1,5 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application) apply false
|
||||||
|
alias(libs.plugins.kotlin.compose) apply false
|
||||||
|
}
|
||||||
23
gradle.properties
Archivo normal
@@ -0,0 +1,23 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. For more details, visit
|
||||||
|
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
12
gradle/gradle-daemon-jvm.properties
Archivo normal
@@ -0,0 +1,12 @@
|
|||||||
|
#This file is generated by updateDaemonJvm
|
||||||
|
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
|
||||||
|
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
|
||||||
|
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
|
||||||
|
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
|
||||||
|
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect
|
||||||
|
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect
|
||||||
|
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
|
||||||
|
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
|
||||||
|
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect
|
||||||
|
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect
|
||||||
|
toolchainVersion=21
|
||||||
46
gradle/libs.versions.toml
Archivo normal
@@ -0,0 +1,46 @@
|
|||||||
|
[versions]
|
||||||
|
agp = "9.0.1"
|
||||||
|
coreKtx = "1.17.0"
|
||||||
|
junit = "4.13.2"
|
||||||
|
junitVersion = "1.3.0"
|
||||||
|
espressoCore = "3.7.0"
|
||||||
|
lifecycleRuntimeKtx = "2.10.0"
|
||||||
|
lifecycleViewmodelCompose = "2.10.0"
|
||||||
|
activityCompose = "1.12.4"
|
||||||
|
kotlin = "2.0.21"
|
||||||
|
composeBom = "2024.09.00"
|
||||||
|
cameraX = "1.4.2"
|
||||||
|
mlkitTextRecognition = "16.0.1"
|
||||||
|
mlkitBarcodeScanning = "17.3.0"
|
||||||
|
accompanistPermissions = "0.37.0"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
|
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||||
|
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
|
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||||
|
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" }
|
||||||
|
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||||
|
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||||
|
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||||
|
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||||
|
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||||
|
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||||
|
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||||
|
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
|
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
|
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
|
||||||
|
androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "cameraX" }
|
||||||
|
androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraX" }
|
||||||
|
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraX" }
|
||||||
|
androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraX" }
|
||||||
|
mlkit-text-recognition = { group = "com.google.mlkit", name = "text-recognition", version.ref = "mlkitTextRecognition" }
|
||||||
|
mlkit-barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "mlkitBarcodeScanning" }
|
||||||
|
accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanistPermissions" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendido
Archivo normal
9
gradle/wrapper/gradle-wrapper.properties
vendido
Archivo normal
@@ -0,0 +1,9 @@
|
|||||||
|
#Sun Mar 01 13:28:10 CET 2026
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionSha256Sum=72f44c9f8ebcb1af43838f45ee5c4aa9c5444898b3468ab3f4af7b6076c5bc3f
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
251
gradlew
vendido
Archivo ejecutable
@@ -0,0 +1,251 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015 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.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# 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/platforms/jvm/plugins-application/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 -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || 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="\\\"\\\""
|
||||||
|
|
||||||
|
|
||||||
|
# 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, 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" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# 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" "$@"
|
||||||
94
gradlew.bat
vendido
Archivo normal
@@ -0,0 +1,94 @@
|
|||||||
|
@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
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@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. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
|
: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
|
||||||
26
settings.gradle.kts
Archivo normal
@@ -0,0 +1,26 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google {
|
||||||
|
content {
|
||||||
|
includeGroupByRegex("com\\.android.*")
|
||||||
|
includeGroupByRegex("com\\.google.*")
|
||||||
|
includeGroupByRegex("androidx.*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugins {
|
||||||
|
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "URLFinder"
|
||||||
|
include(":app")
|
||||||