21 Commits

Author SHA1 Message Date
Gitea Actions
4e280796b9 Update manifest version to 3.1.2 [▶️] 2026-01-09 14:51:04 +00:00
a54f52b04e Update README.md
All checks were successful
Build, Push, Publish / Build & Release (push) Successful in 10m50s
2026-01-09 14:46:04 +00:00
Gitea Actions
40fe720d3f Update manifest version to 3.1.1 [▶️] 2026-01-09 14:42:53 +00:00
65639a2f04 Update public/index.html
Some checks failed
Build, Push, Publish / Build & Release (push) Has been cancelled
2026-01-09 14:42:41 +00:00
Gitea Actions
f74d9038b7 Sync README from template [▶️] 2026-01-01 04:01:51 +00:00
Gitea Actions
14acaf3c62 Sync README from template [▶️] 2025-12-28 04:02:51 +00:00
Gitea Actions
d475f88ab9 Update manifest version to 3.1.0 [▶️] 2025-12-22 05:32:22 +00:00
465c0c11e9 fix release build, add readme update for some repo
All checks were successful
Build, Push, Publish / Build & Release (push) Successful in 10m6s
2025-12-22 01:43:50 -03:00
Gitea Actions
cf860814c5 Sync README from template [▶️] 2025-12-22 04:02:04 +00:00
Gitea Actions
633ce7c550 Update manifest version to 3.0.5 [▶️] 2025-12-22 03:00:21 +00:00
e458f278b1 Merge branch 'main' of https://git.icc.gg/ivancarlos/devicetester
All checks were successful
Build, Push, Publish / Build & Release (push) Successful in 10m9s
2025-12-21 23:33:49 -03:00
efb940fc8c update actions 2025-12-21 23:33:45 -03:00
Gitea Actions
053427d46d Update manifest version to 3.0.4 [▶️] 2025-12-14 03:47:51 +00:00
8094287f97 remove Pro title
All checks were successful
Build, Push, Publish / Build & Release (push) Successful in 10m3s
2025-12-14 03:47:39 +00:00
Gitea Actions
1b99769918 Update manifest version to 3.0.3 [▶️] 2025-12-13 19:47:03 +00:00
bd39dd2b3f Update public/index.html
All checks were successful
Build, Push, Publish / Build & Release (push) Successful in 9m58s
2025-12-13 19:46:51 +00:00
Gitea Actions
45fb07bc8b Update manifest version to 3.0.2 [▶️] 2025-12-13 19:28:02 +00:00
5cea0866ae Merge branch 'main' of https://git.icc.gg/ivancarlos/devicetester
All checks were successful
Build, Push, Publish / Build & Release (push) Successful in 9m56s
2025-12-13 16:27:45 -03:00
cb87f1908e improvements
Add language selector, flags, add footer with links, fix battery detection text
2025-12-13 16:27:36 -03:00
Gitea Actions
d8f149e595 Update manifest version to 3.0.1 [▶️] 2025-12-13 18:17:35 +00:00
e08b1bfa41 languages
All checks were successful
Build, Push, Publish / Build & Release (push) Successful in 9m57s
2025-12-13 15:17:09 -03:00
5 changed files with 447 additions and 120 deletions

View File

@@ -5,9 +5,6 @@ on:
branches: branches:
- main - main
workflow_dispatch: workflow_dispatch:
schedule:
- cron: '28 5 * * *'
# workflow_run support in Gitea can be tricky, keeping it but might need adjustment
workflow_run: workflow_run:
workflows: ["Sync Repo"] workflows: ["Sync Repo"]
types: types:
@@ -265,6 +262,11 @@ jobs:
git commit -m "Update manifest version to ${{ steps.version.outputs.VERSION }} [▶️]" || echo "Nothing to commit" git commit -m "Update manifest version to ${{ steps.version.outputs.VERSION }} [▶️]" || echo "Nothing to commit"
git push origin main git push origin main
- name: 🛠 Install zip
if: steps.check_commits.outputs.commit_count != '0'
run: |
apt-get update && apt-get install -y zip
- name: 📦 Create ZIP package (excluding certain files) - name: 📦 Create ZIP package (excluding certain files)
if: steps.check_commits.outputs.commit_count != '0' if: steps.check_commits.outputs.commit_count != '0'
run: | run: |
@@ -319,18 +321,17 @@ jobs:
ZIP_NAME="${{ steps.version.outputs.ZIP_NAME }}" ZIP_NAME="${{ steps.version.outputs.ZIP_NAME }}"
FILE_PATH="./$ZIP_NAME" FILE_PATH="./$ZIP_NAME"
curl -s -X POST "${{ gitea.api_url }}/repos/${{ gitea.repository }}/releases/$RELEASE_ID/assets" \ curl --fail -s -X POST "${{ gitea.api_url }}/repos/${{ gitea.repository }}/releases/$RELEASE_ID/assets?name=$ZIP_NAME" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/zip" \ -H "Content-Type: application/zip" \
--data-binary @"$FILE_PATH" \ --data-binary @"$FILE_PATH"
-o /dev/null
# ----- Docker steps ----- # ----- Docker steps -----
- name: Clone Upstream Code (if needed) - name: Clone Upstream Code (if needed)
if: steps.check_commits.outputs.commit_count != '0' && (steps.check_upstream.outputs.upstream_needs_update == 'true' || steps.check_upstream.outputs.repo_url != '') if: steps.check_commits.outputs.commit_count != '0' && (steps.check_upstream.outputs.upstream_needs_update == 'true' || steps.check_upstream.outputs.repo_url != '')
run: | run: |
rm -rf upstream_src rm -rf upstream_src
git clone --depth 1 --branch ${{ steps.check_upstream.outputs.repo_branch }} ${{ steps.check_upstream.outputs.repo_url }} upstream_src git clone --depth 1 --branch ${{ steps.check_upstream.outputs.repo_branch }} ${{ steps.check_upstream.outputs.repo_url }} upstream_src
- name: 🔍 Check if Dockerfile exists - name: 🔍 Check if Dockerfile exists
if: steps.check_commits.outputs.commit_count != '0' || steps.check_upstream.outputs.upstream_needs_update == 'true' if: steps.check_commits.outputs.commit_count != '0' || steps.check_upstream.outputs.upstream_needs_update == 'true'

View File

@@ -0,0 +1,59 @@
name: Update README
permissions:
contents: write
on:
workflow_dispatch:
schedule:
- cron: "0 4 * * *" # Every day at 4 AM UTC
jobs:
update-readme:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
env:
SOURCE_REPO: ivancarlos/.gitea
SOURCE_BRANCH: main
steps:
- name: Checkout current repository
uses: actions/checkout@v4
- name: Checkout source README template
uses: actions/checkout@v4
with:
repository: ${{ env.SOURCE_REPO }}
ref: ${{ env.SOURCE_BRANCH }}
token: ${{ secrets.CR_PAT }}
path: source_readme
- name: Update README.md (footer only)
run: |
set -e
# --- Extract footer block from source (everything from <!-- footer --> onward) ---
FOOTER=$(awk '/<!-- footer -->/{flag=1}flag' source_readme/README.md)
# --- Replace everything after <!-- footer --> with FOOTER ---
awk -v footer="$FOOTER" '
/<!-- footer -->/ {
print footer
found=1
exit
}
{ print }
' README.md > README.tmp && mv README.tmp README.md
- name: Remove source_readme from git index
run: rm -rf source_readme
- name: Commit and push changes
run: |
git config user.name "Gitea Actions"
git config user.email "actions@git.icc.gg"
git add README.md
git commit -m "Sync README from template [▶️]" || echo "Nothing to commit"
git push origin ${{ github.ref_name }}

View File

@@ -1,24 +1,25 @@
# Device Tester # Device Tester
A simple and useful webcam, microphone and audio output tester. A simple and useful webcam, microphone and audio output tester.
<!-- buttons -->
[![Stars](https://img.shields.io/github/stars/ivancarlosti/devicetester?label=⭐%20Stars&color=gold&style=flat)](https://github.com/ivancarlosti/devicetester/stargazers)
[![Watchers](https://img.shields.io/github/watchers/ivancarlosti/devicetester?label=Watchers&style=flat&color=red)](https://github.com/sponsors/ivancarlosti)
[![Forks](https://img.shields.io/github/forks/ivancarlosti/devicetester?label=Forks&style=flat&color=ff69b4)](https://github.com/sponsors/ivancarlosti)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/ivancarlosti/devicetester?label=Activity)](https://github.com/ivancarlosti/devicetester/pulse)
[![GitHub Issues](https://img.shields.io/github/issues/ivancarlosti/devicetester?label=Issues&color=orange)](https://github.com/ivancarlosti/devicetester/issues)
[![License](https://img.shields.io/github/license/ivancarlosti/devicetester?label=License)](LICENSE)
[![GitHub last commit](https://img.shields.io/github/last-commit/ivancarlosti/devicetester?label=Last%20Commit)](https://github.com/ivancarlosti/devicetester/commits)
[![Security](https://img.shields.io/badge/Security-View%20Here-purple)](https://github.com/ivancarlosti/devicetester/security)
[![Code of Conduct](https://img.shields.io/badge/Code%20of%20Conduct-2.1-4baaaa)](https://github.com/ivancarlosti/devicetester?tab=coc-ov-file)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/ivancarlosti?label=GitHub%20Sponsors&color=ffc0cb)][sponsor]
<!-- endbuttons -->
## Notes ## Notes
1. Project inspired in multiple device complex testers across the web. 1. Project inspired in multiple device complex testers across the web.
2. The music used on test is royalty free and generated by AI. 2. The music used on test is royalty free and generated by AI.
3. The page was co-created by AI. 3. The page was co-created by AI.
## Docker Compose suggestion
```
name: devicetester
services:
devicetester:
image: git.icc.gg/ivancarlos/devicetester:latest
container_name: devicetester
restart: unless-stopped
ports:
- "3987:80"
```
<!-- footer --> <!-- footer -->
--- ---
@@ -29,17 +30,9 @@ A simple and useful webcam, microphone and audio output tester.
## 🩷 Project support ## 🩷 Project support
| If you found this project helpful, consider | | If you found this project helpful, consider |
| :---: | | :---: |
[**buying me a coffee**][buymeacoffee], [**donate by paypal**][paypal], [**sponsor this project**][sponsor] or just [**leave a star**](../..)⭐ [**buying me a coffee**][buymeacoffee] or [**supporting me on Patreon**][patreon]
|Thanks for your support, it is much appreciated!| |Thanks for your support, it is much appreciated!|
[cc]: https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-a-code-of-conduct-to-your-project [ivancarlos]: https://ivancarlos.me
[contributing]: https://docs.github.com/en/articles/setting-guidelines-for-repository-contributors
[security]: https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository
[support]: https://docs.github.com/en/articles/adding-support-resources-to-your-project
[it]: https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
[prt]: https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository
[funding]: https://docs.github.com/en/articles/displaying-a-sponsor-button-in-your-repository
[ivancarlos]: https://ivancarlos.it
[buymeacoffee]: https://www.buymeacoffee.com/ivancarlos [buymeacoffee]: https://www.buymeacoffee.com/ivancarlos
[paypal]: https://icc.gg/donate [patreon]: https://patreon.com/ivancarlos
[sponsor]: https://github.com/sponsors/ivancarlosti

View File

@@ -1,4 +1,4 @@
{ {
"version": "3.0.0", "version": "3.1.2",
"author": "Ivan Carlos" "author": "Ivan Carlos"
} }

View File

@@ -1,9 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="pt-BR"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Teste de Dispositivos Pro</title> <title>Device Tester</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -45,10 +46,73 @@
} }
header { header {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 30px;
}
#ipv4, #ipv6 {
word-break: break-all;
overflow-wrap: anywhere;
display: inline-block;
max-width: 100%;
vertical-align: top;
}
/* Shared styles for header buttons */
.theme-toggle,
.icon-btn {
background: none;
border: 2px solid var(--border-color);
color: var(--text-color);
padding: 0 12px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
font-family: inherit;
font-size: 1.2rem;
/* Larger emoji */
display: inline-flex;
align-items: center;
justify-content: center;
height: 40px;
/* Enforce consistent height */
box-sizing: border-box;
min-width: 44px;
}
.theme-toggle {
font-size: 0.9rem;
/* Restore font size for theme text */
gap: 6px;
}
.theme-toggle:hover,
.icon-btn:hover {
background-color: var(--border-color);
}
.icon-btn.active {
background-color: var(--primary);
color: white;
border-color: var(--primary);
}
.header-top {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 30px; width: 100%;
flex-wrap: wrap;
gap: 10px;
}
.header-controls {
display: flex;
align-items: center;
gap: 8px;
} }
h1 { h1 {
@@ -60,20 +124,7 @@
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
} }
.theme-toggle {
background: none;
border: 2px solid var(--border-color);
color: var(--text-color);
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.theme-toggle:hover {
background-color: var(--border-color);
}
.test-card { .test-card {
background-color: var(--card-bg); background-color: var(--card-bg);
@@ -82,11 +133,6 @@
border-radius: 12px; border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
transition: transform 0.2s;
}
.test-card:hover {
transform: translateY(-2px);
} }
h2 { h2 {
@@ -104,7 +150,7 @@
font-size: 0.9rem; font-size: 0.9rem;
} }
select, select.device-select,
textarea { textarea {
width: 100%; width: 100%;
padding: 10px; padding: 10px;
@@ -202,6 +248,27 @@
pointer-events: none; pointer-events: none;
text-align: center; text-align: center;
} }
footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
text-align: center;
font-size: 0.9rem;
color: var(--text-color);
opacity: 0.8;
}
footer a {
color: var(--text-color);
text-decoration: none;
transition: color 0.2s;
}
footer a:hover {
color: var(--primary);
text-decoration: underline;
}
</style> </style>
</head> </head>
@@ -209,94 +276,277 @@
<div class="container"> <div class="container">
<header> <header>
<h1>Teste de Dispositivos</h1> <div class="header-top">
<button class="theme-toggle" onclick="toggleTheme()">🌓 Tema</button> <h1 data-i18n="app_title">Device Tester</h1>
<div class="header-controls">
<button class="icon-btn" onclick="changeLanguage('en')" id="btn-en" aria-label="English"><img
src="https://flagcdn.com/24x18/us.png" alt="US" width="24" height="18"></button>
<button class="icon-btn" onclick="changeLanguage('pt')" id="btn-pt" aria-label="Português"><img
src="https://flagcdn.com/24x18/br.png" alt="BR" width="24" height="18"></button>
<button class="icon-btn" onclick="changeLanguage('es')" id="btn-es" aria-label="Español"><img
src="https://flagcdn.com/24x18/mx.png" alt="MX" width="24" height="18"></button>
<div style="width: 1px; height: 24px; background: var(--border-color); margin: 0 4px;"></div>
<button class="theme-toggle" onclick="toggleTheme()" data-i18n="theme_toggle">🌓 Theme</button>
</div>
</div>
</header> </header>
<!-- Rede --> <!-- Network -->
<div class="test-card"> <div class="test-card">
<h2>📡 Rede e Navegador</h2> <h2 data-i18n="network_title">📡 Network & Browser</h2>
<p>IPv4: <strong id="ipv4">...</strong> <span class="status-text" id="geo-ipv4"></span></p> <p>IPv4: <strong id="ipv4">...</strong> <span class="status-text" id="geo-ipv4"></span></p>
<p class="status-text" id="isp-label-ipv4"></p> <p class="status-text" id="isp-label-ipv4"></p>
<p>IPv6: <strong id="ipv6">...</strong> <span class="status-text" id="geo-ipv6"></span></p> <p>IPv6: <strong id="ipv6">...</strong> <span class="status-text" id="geo-ipv6"></span></p>
<p class="status-text" id="isp-label-ipv6"></p> <p class="status-text" id="isp-label-ipv6"></p>
<label>User Agent:</label> <label data-i18n="user_agent_label">User Agent:</label>
<textarea id="user-agent" rows="2" readonly></textarea> <textarea id="user-agent" rows="2" readonly></textarea>
</div> </div>
<!-- Bateria --> <!-- Battery -->
<div class="test-card"> <div class="test-card">
<h2>🔋 Bateria</h2> <h2 data-i18n="battery_title">🔋 Battery</h2>
<p id="battery-status">Detectando...</p> <p id="battery-status">Detecting...</p>
<div class="bar-container"> <div class="bar-container">
<div id="battery-bar" class="bar-fill" style="background-color: var(--primary);"></div> <div id="battery-bar" class="bar-fill" style="background-color: var(--primary);"></div>
</div> </div>
</div> </div>
<!-- Tela --> <!-- Screen -->
<div class="test-card"> <div class="test-card">
<h2>🖥️ Tela & Dead Pixel</h2> <h2 data-i18n="screen_title">🖥️ Screen & Dead Pixel</h2>
<p class="status-text">Cicla entre cores primárias para identificar pixels mortos.</p> <p class="status-text" data-i18n="screen_desc">Cycles through primary colors to identify dead pixels.</p>
<button onclick="startScreenTest()">Iniciar Teste de Tela</button> <button onclick="startScreenTest()" data-i18n="start_screen_test">Start Screen Test</button>
</div> </div>
<!-- Vibração --> <!-- Vibration -->
<div class="test-card"> <div class="test-card">
<h2>📳 Vibração</h2> <h2 data-i18n="vibration_title">📳 Vibration</h2>
<p class="status-text">Funciona apenas em dispositivos móveis compatíveis.</p> <p class="status-text" data-i18n="vibration_desc">Works only on compatible mobile devices.</p>
<button onclick="testVibration()">Vibrar Dispositivo</button> <button onclick="testVibration()" data-i18n="vibrate_device">Vibrate Device</button>
</div> </div>
<!-- Webcam --> <!-- Webcam -->
<div class="test-card"> <div class="test-card">
<h2>📷 Webcam</h2> <h2 data-i18n="webcam_title">📷 Webcam</h2>
<select id="webcam-select"></select> <select id="webcam-select" class="device-select"></select>
<div> <div>
<button onclick="startWebcam()">Ligar</button> <button onclick="startWebcam()" data-i18n="turn_on">Turn On</button>
<button onclick="stopWebcam()" class="stop">Desligar</button> <button onclick="stopWebcam()" class="stop" data-i18n="turn_off">Turn Off</button>
</div> </div>
<video id="webcam" autoplay playsinline></video> <video id="webcam" autoplay playsinline></video>
<p id="webcam-status" class="status-text">Status: Aguardando...</p> <p id="webcam-status" class="status-text" data-i18n="status_waiting">Status: Waiting...</p>
</div> </div>
<!-- Microfone --> <!-- Microphone -->
<div class="test-card"> <div class="test-card">
<h2>🎤 Microfone</h2> <h2 data-i18n="mic_title">🎤 Microphone</h2>
<select id="mic-select"></select> <select id="mic-select" class="device-select"></select>
<div> <div>
<button onclick="startMic()">Monitorar Ao Vivo</button> <button onclick="startMic()" data-i18n="monitor_live">Monitor Live</button>
<button onclick="stopMic()" class="stop">Parar</button> <button onclick="stopMic()" class="stop" data-i18n="stop">Stop</button>
<button onclick="recordMicFor10s()">Gravar 10s</button> <button onclick="recordMicFor10s()" data-i18n="record_10s">Record 10s</button>
</div> </div>
<div class="bar-container"> <div class="bar-container">
<div id="mic-bar" class="bar-fill"></div> <div id="mic-bar" class="bar-fill"></div>
</div> </div>
<p id="mic-status" class="status-text">Status: Aguardando...</p> <p id="mic-status" class="status-text" data-i18n="status_waiting">Status: Waiting...</p>
<audio id="mic-playback" controls style="width: 100%; margin-top: 10px; display: none;"></audio> <audio id="mic-playback" controls style="width: 100%; margin-top: 10px; display: none;"></audio>
</div> </div>
<!-- Som --> <!-- Sound -->
<div class="test-card"> <div class="test-card">
<h2>🔊 Saída de Som</h2> <h2 data-i18n="sound_title">🔊 Sound Output</h2>
<select id="spk-select"></select> <select id="spk-select" class="device-select"></select>
<audio id="audio-test" src="test-sound.mp3" preload="auto"></audio> <audio id="audio-test" src="test-sound.mp3" preload="auto"></audio>
<div> <div>
<button id="playBtn" onclick="playSound()">Tocar Música</button> <button id="playBtn" onclick="playSound()" data-i18n="play_music">Play Music</button>
<button onclick="stopSound()" class="stop">Parar</button> <button onclick="stopSound()" class="stop" data-i18n="stop">Stop</button>
</div> </div>
<div class="bar-container"> <div class="bar-container">
<div id="speaker-bar" class="bar-fill" style="background-color: #f59e0b;"></div> <div id="speaker-bar" class="bar-fill" style="background-color: #f59e0b;"></div>
</div> </div>
</div> </div>
<footer>
<p>
<a href="https://icc.gg/privacidade" target="_blank" data-i18n="privacy_policy">Privacy Policy</a>
<a href="https://icc.gg/termos" target="_blank" data-i18n="terms_conditions">Terms and Conditions</a>
</p>
</footer>
</div> </div>
<!-- Overlay para teste de tela --> <!-- Overlay para teste de tela -->
<div id="screen-overlay" onclick="nextScreenColor()"> <div id="screen-overlay" onclick="nextScreenColor()">
<div id="screen-instruction">Clique para trocar a cor. Escura para sair.</div> <div id="screen-instruction" data-i18n="screen_instruction">Click to change color. Esc to exit.</div>
</div> </div>
<script> <script>
// i18n Dictionary
const dictionary = {
en: {
app_title: "Device Tester",
theme_toggle: "🌓 Theme",
network_title: "📡 Network & Browser",
user_agent_label: "User Agent:",
battery_title: "🔋 Battery",
detecting: "Detecting...",
charging: "⚡ Charging",
disconnected: "Disconnected",
battery_unsupported: "Battery API not supported in this browser.",
screen_title: "🖥️ Screen & Dead Pixel",
screen_desc: "Cycles through primary colors to identify dead pixels.",
start_screen_test: "Start Screen Test",
screen_instruction: "Click to change color. Dark to exit.",
vibration_title: "📳 Vibration",
vibration_desc: "Works only on compatible mobile devices.",
vibrate_device: "Vibrate Device",
vibration_unsupported: "Your device does not support vibration via web API.",
webcam_title: "📷 Webcam",
turn_on: "Turn On",
turn_off: "Turn Off",
status_waiting: "Status: Waiting...",
webcam_ok: "Webcam: Working (OK)",
status_stopped: "Status: Stopped",
mic_title: "🎤 Microphone",
monitor_live: "Monitor Live",
stop: "Stop",
record_10s: "Record 10s",
capturing_audio: "Capturing audio...",
starting_capture: "Starting capture...",
recording: "Recording...",
playing_recording: "Playing recording...",
sound_title: "🔊 Sound Output",
play_music: "Play Music",
device_unknown: "Unknown Device",
default_output: "Default Output",
ipv4_unavailable: "Unavailable",
ipv6_not_detected: "Not detected",
privacy_policy: "Privacy Policy",
terms_conditions: "Terms and Conditions"
},
pt: {
app_title: "Teste de Dispositivos",
theme_toggle: "🌓 Tema",
network_title: "📡 Rede e Navegador",
user_agent_label: "Agente de Usuário:",
battery_title: "🔋 Bateria",
detecting: "Detectando...",
charging: "⚡ Carregando",
disconnected: "Desconectado",
battery_unsupported: "API de Bateria não suportada neste navegador.",
screen_title: "🖥️ Tela & Dead Pixel",
screen_desc: "Cicla entre cores primárias para identificar pixels mortos.",
start_screen_test: "Iniciar Teste de Tela",
screen_instruction: "Clique para trocar a cor. Escura para sair.",
vibration_title: "📳 Vibração",
vibration_desc: "Funciona apenas em dispositivos móveis compatíveis.",
vibrate_device: "Vibrar Dispositivo",
vibration_unsupported: "Seu dispositivo não suporta vibração via API web.",
webcam_title: "📷 Webcam",
turn_on: "Ligar",
turn_off: "Desligar",
status_waiting: "Status: Aguardando...",
webcam_ok: "Webcam: Funcionando (OK)",
status_stopped: "Status: Parado",
mic_title: "🎤 Microfone",
monitor_live: "Monitorar Ao Vivo",
stop: "Parar",
record_10s: "Gravar 10s",
capturing_audio: "Capturando áudio...",
starting_capture: "Iniciando captura...",
recording: "Gravando...",
playing_recording: "Reproduzindo gravação...",
sound_title: "🔊 Saída de Som",
play_music: "Tocar Música",
device_unknown: "Dispositivo não identificado",
default_output: "Saída Padrão",
ipv4_unavailable: "Indisponível",
ipv6_not_detected: "Não detectado",
privacy_policy: "Política de Privacidade",
terms_conditions: "Termos e Condições"
},
es: {
app_title: "Prueba de Dispositivos",
theme_toggle: "🌓 Tema",
network_title: "📡 Red y Navegador",
user_agent_label: "Agente de Usuario:",
battery_title: "🔋 Batería",
detecting: "Detectando...",
charging: "⚡ Cargando",
disconnected: "Desconectado",
battery_unsupported: "API de batería no soportada en este navegador.",
screen_title: "🖥️ Pantalla y Píxeles Muertos",
screen_desc: "Alterna entre colores primarios para identificar píxeles muertos.",
start_screen_test: "Iniciar Prueba de Pantalla",
screen_instruction: "Haga clic para cambiar de color. Oscuro para salir.",
vibration_title: "📳 Vibración",
vibration_desc: "Funciona solo en dispositivos móviles compatibles.",
vibrate_device: "Vibrar Dispositivo",
vibration_unsupported: "Su dispositivo no admite vibración a través de la API web.",
webcam_title: "📷 Cámara Web",
turn_on: "Encender",
turn_off: "Apagar",
status_waiting: "Estado: Esperando...",
webcam_ok: "Cámara: Funcionando (OK)",
status_stopped: "Estado: Detenido",
mic_title: "🎤 Micrófono",
monitor_live: "Monitorear en Vivo",
stop: "Parar",
record_10s: "Grabar 10s",
capturing_audio: "Capturando audio...",
starting_capture: "Iniciando captura...",
recording: "Grabando...",
playing_recording: "Reproduciendo grabación...",
sound_title: "🔊 Salida de Sonido",
play_music: "Reproducir Música",
device_unknown: "Dispositivo desconocido",
default_output: "Salida Predeterminada",
ipv4_unavailable: "No disponible",
ipv6_not_detected: "No detectado",
privacy_policy: "Política de Privacidad",
terms_conditions: "Términos y Condiciones"
}
};
let currentLang = localStorage.getItem('deviceTesterLang') || 'en';
function changeLanguage(lang) {
currentLang = lang;
localStorage.setItem('deviceTesterLang', lang);
// Update active button state
document.querySelectorAll('.icon-btn').forEach(btn => btn.classList.remove('active'));
const activeBtn = document.getElementById(`btn-${lang}`);
if (activeBtn) activeBtn.classList.add('active');
document.documentElement.lang = lang === 'pt' ? 'pt-BR' : (lang === 'es' ? 'es-MX' : 'en-US'); // Set html tag lang
applyTranslations();
// Refresh dynamic content that depends on language (like IP detection messages if needed, though they are mostly dynamic data)
// Re-listing devices to update "Unknown Device" text
listDevices();
// Trigger battery update to refresh text
if (navigator.getBattery) {
navigator.getBattery().then(b => b.dispatchEvent(new Event('levelchange')));
} else {
document.getElementById("battery-status").textContent = dictionary[currentLang].battery_unsupported;
}
}
function applyTranslations() {
const elements = document.querySelectorAll('[data-i18n]');
elements.forEach(el => {
const key = el.getAttribute('data-i18n');
if (dictionary[currentLang] && dictionary[currentLang][key]) {
el.textContent = dictionary[currentLang][key];
}
});
}
// Initialize Language
changeLanguage(currentLang);
// Theme // Theme
function toggleTheme() { function toggleTheme() {
document.body.classList.toggle('dark-mode'); document.body.classList.toggle('dark-mode');
@@ -365,6 +615,10 @@
function listDevices() { function listDevices() {
navigator.mediaDevices.enumerateDevices().then(devices => { navigator.mediaDevices.enumerateDevices().then(devices => {
// Preserve selection if possible? For now simple rebuild // Preserve selection if possible? For now simple rebuild
const oldWebcam = webcamSelect.value;
const oldMic = micSelect.value;
const oldSpk = spkSelect.value;
webcamSelect.innerHTML = ""; webcamSelect.innerHTML = "";
micSelect.innerHTML = ""; micSelect.innerHTML = "";
spkSelect.innerHTML = ""; spkSelect.innerHTML = "";
@@ -372,7 +626,7 @@
devices.forEach(device => { devices.forEach(device => {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = device.deviceId; option.value = device.deviceId;
option.text = device.label || "Dispositivo não identificado"; option.text = device.label || dictionary[currentLang].device_unknown;
if (device.kind === "videoinput") webcamSelect.appendChild(option.cloneNode(true)); if (device.kind === "videoinput") webcamSelect.appendChild(option.cloneNode(true));
if (device.kind === "audioinput") micSelect.appendChild(option.cloneNode(true)); if (device.kind === "audioinput") micSelect.appendChild(option.cloneNode(true));
if (device.kind === "audiooutput" && typeof spkSelect.setSinkId === "function") if (device.kind === "audiooutput" && typeof spkSelect.setSinkId === "function")
@@ -382,9 +636,14 @@
if (spkSelect.options.length === 0) { if (spkSelect.options.length === 0) {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = ""; option.value = "";
option.text = "Saída Padrão"; option.text = dictionary[currentLang].default_output;
spkSelect.appendChild(option); spkSelect.appendChild(option);
} }
// Try to restore selection
if (oldWebcam) webcamSelect.value = oldWebcam;
if (oldMic) micSelect.value = oldMic;
if (oldSpk) spkSelect.value = oldSpk;
}); });
} }
@@ -397,7 +656,7 @@
.then(stream => { .then(stream => {
webcamStream = stream; webcamStream = stream;
video.srcObject = stream; video.srcObject = stream;
document.getElementById("webcam-status").textContent = "Webcam: Funcionando (OK)"; document.getElementById("webcam-status").textContent = dictionary[currentLang].webcam_ok;
document.getElementById("webcam-status").style.color = "var(--success)"; document.getElementById("webcam-status").style.color = "var(--success)";
}) })
.catch((e) => { .catch((e) => {
@@ -410,7 +669,7 @@
webcamStream.getTracks().forEach(t => t.stop()); webcamStream.getTracks().forEach(t => t.stop());
video.srcObject = null; video.srcObject = null;
webcamStream = null; webcamStream = null;
document.getElementById("webcam-status").textContent = "Status: Parado"; document.getElementById("webcam-status").textContent = dictionary[currentLang].status_stopped;
document.getElementById("webcam-status").style.color = "var(--text-color)"; document.getElementById("webcam-status").style.color = "var(--text-color)";
} }
} }
@@ -432,14 +691,14 @@
micBar.style.width = pct + "%"; micBar.style.width = pct + "%";
if (pct > 5) { if (pct > 5) {
const st = document.getElementById("mic-status"); const st = document.getElementById("mic-status");
if (st.textContent !== "Capturando áudio...") { if (st.textContent !== dictionary[currentLang].capturing_audio) {
st.textContent = "Capturando áudio..."; st.textContent = dictionary[currentLang].capturing_audio;
st.style.color = "var(--success)"; st.style.color = "var(--success)";
} }
} }
}; };
source.connect(micWorkletNode); source.connect(micWorkletNode);
document.getElementById("mic-status").textContent = "Iniciando captura..."; document.getElementById("mic-status").textContent = dictionary[currentLang].starting_capture;
} catch (e) { } catch (e) {
document.getElementById("mic-status").textContent = "Erro: " + e.message; document.getElementById("mic-status").textContent = "Erro: " + e.message;
document.getElementById("mic-status").style.color = "var(--danger)"; document.getElementById("mic-status").style.color = "var(--danger)";
@@ -455,7 +714,7 @@
micStream = null; micStream = null;
micBar.style.width = "0%"; micBar.style.width = "0%";
const st = document.getElementById("mic-status"); const st = document.getElementById("mic-status");
st.textContent = "Status: Parado"; st.textContent = dictionary[currentLang].status_stopped;
st.style.color = "var(--text-color)"; st.style.color = "var(--text-color)";
} }
@@ -493,18 +752,18 @@
let remaining = 10; let remaining = 10;
const status = document.getElementById("mic-status"); const status = document.getElementById("mic-status");
status.style.color = "var(--primary)"; status.style.color = "var(--primary)";
status.textContent = `Gravando... ${remaining}s`; status.textContent = `${dictionary[currentLang].recording} ${remaining}s`;
const countdown = setInterval(() => { const countdown = setInterval(() => {
remaining--; remaining--;
if (remaining > 0) status.textContent = `Gravando... ${remaining}s`; if (remaining > 0) status.textContent = `${dictionary[currentLang].recording} ${remaining}s`;
}, 1000); }, 1000);
setTimeout(() => { setTimeout(() => {
clearInterval(countdown); clearInterval(countdown);
micRecorder.stop(); micRecorder.stop();
stream.getTracks().forEach(t => t.stop()); stream.getTracks().forEach(t => t.stop());
status.textContent = "Reproduzindo gravação..."; status.textContent = dictionary[currentLang].playing_recording;
status.style.color = "var(--text-color)"; status.style.color = "var(--text-color)";
}, 10000); }, 10000);
} catch (err) { } catch (err) {
@@ -559,25 +818,31 @@
// IPs // IPs
function getIPv4AndISP() { function getIPv4AndISP() {
fetch("https://ipwhois.app/json/?lang=pt") fetch("https://ipwhois.app/json/?lang=en") // Using 'en' for consistency, can be improved to use currentLang but ISP data might be mix
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
document.getElementById("ipv4").textContent = data.ip || "Indisponível"; document.getElementById("ipv4").textContent = data.ip || dictionary[currentLang].ipv4_unavailable;
const geo = [data.city, data.region, data.country].filter(Boolean); const geo = [data.city, data.region, data.country].filter(Boolean);
document.getElementById("geo-ipv4").textContent = geo.length ? "(" + geo.join(", ") + ")" : ""; document.getElementById("geo-ipv4").textContent = geo.length ? "(" + geo.join(", ") + ")" : "";
document.getElementById("isp-label-ipv4").textContent = data.isp ? "ISP: " + decodeURIComponent(escape(data.isp)) : ""; document.getElementById("isp-label-ipv4").textContent = data.isp ? "ISP: " + decodeURIComponent(escape(data.isp)) : "";
}) })
.catch(() => { }); .catch(() => { });
} }
function getIPv6AndISP() { function getIPv6AndISP() {
fetch("https://api64.ipify.org?format=json") // Use IPv6 specific endpoint to force IPv6 usage
.then(res => res.json()) fetch("https://api6.ipify.org?format=json")
.then(res => {
if (!res.ok) throw new Error("IPv6 Fetch Failed");
return res.json();
})
.then(ip => { .then(ip => {
if (ip.ip && ip.ip.includes(":")) { if (ip.ip && ip.ip.includes(":")) {
document.getElementById("ipv6").textContent = ip.ip; document.getElementById("ipv6").textContent = ip.ip;
return fetch(`https://ipwhois.app/json/${ip.ip}?lang=pt`); // Fetch validation/Geo for IPv6
return fetch(`https://ipwhois.app/json/${ip.ip}?lang=en`);
} else { } else {
document.getElementById("ipv6").textContent = "Não detectado"; throw new Error("No IPv6 detected");
} }
}) })
.then(res => res.json()) .then(res => res.json())
@@ -587,7 +852,10 @@
document.getElementById("geo-ipv6").textContent = geo.length ? "(" + geo.join(", ") + ")" : ""; document.getElementById("geo-ipv6").textContent = geo.length ? "(" + geo.join(", ") + ")" : "";
document.getElementById("isp-label-ipv6").textContent = data.isp ? "ISP: " + decodeURIComponent(escape(data.isp)) : ""; document.getElementById("isp-label-ipv6").textContent = data.isp ? "ISP: " + decodeURIComponent(escape(data.isp)) : "";
}) })
.catch(() => { }); .catch((e) => {
console.log("IPv6 Check Failed:", e);
document.getElementById("ipv6").textContent = dictionary[currentLang].ipv6_not_detected;
});
} }
// NEW FEATURES // NEW FEATURES
@@ -597,7 +865,7 @@
navigator.getBattery().then(battery => { navigator.getBattery().then(battery => {
function updateBattery() { function updateBattery() {
const level = Math.round(battery.level * 100); const level = Math.round(battery.level * 100);
const charging = battery.charging ? "⚡ Carregando" : "Desconectado"; const charging = battery.charging ? dictionary[currentLang].charging : dictionary[currentLang].disconnected;
document.getElementById("battery-status").textContent = `${level}% - ${charging}`; document.getElementById("battery-status").textContent = `${level}% - ${charging}`;
document.getElementById("battery-bar").style.width = level + "%"; document.getElementById("battery-bar").style.width = level + "%";
@@ -610,8 +878,11 @@
updateBattery(); updateBattery();
}); });
} else { } else {
document.getElementById("battery-status").textContent = "API de Bateria não suportada neste navegador."; // Wait for dictionary to be ready or just set it
document.getElementById("battery-bar").style.width = "0%"; setTimeout(() => {
document.getElementById("battery-status").textContent = dictionary[currentLang].battery_unsupported;
document.getElementById("battery-bar").style.width = "0%";
}, 100);
} }
// Vibration // Vibration
@@ -619,7 +890,7 @@
if (navigator.vibrate) { if (navigator.vibrate) {
navigator.vibrate([200, 100, 200]); // Vibrate twice navigator.vibrate([200, 100, 200]); // Vibrate twice
} else { } else {
alert("Seu dispositivo não suporta vibração via API web."); alert(dictionary[currentLang].vibration_unsupported);
} }
} }
@@ -656,6 +927,9 @@
}).catch(() => listDevices()); }).catch(() => listDevices());
getIPv4AndISP(); getIPv4AndISP();
getIPv6AndISP(); getIPv6AndISP();
// Ensure initial text is correct
applyTranslations();
}; };
navigator.mediaDevices.addEventListener("devicechange", listDevices); navigator.mediaDevices.addEventListener("devicechange", listDevices);
audio.addEventListener("ended", () => { audio.addEventListener("ended", () => {