restore github
All checks were successful
Teste de Execução do Gitea Actions / verificacao_do_runner (push) Successful in 6s

This commit is contained in:
2025-12-09 15:38:03 -03:00
parent 0151f4cfc9
commit d274df72b3
8 changed files with 1055 additions and 1 deletions

240
.github/workflows/release_build.yml vendored Normal file
View File

@@ -0,0 +1,240 @@
name: Build, Push, Publish
on:
push:
branches:
- main
workflow_dispatch:
schedule:
- cron: '28 5 * * *'
workflow_run:
workflows: ["Sync Repo"]
types:
- completed
jobs:
release:
name: Build & Release
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- name: 📥 Checkout code with full history and tags
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if any tags exist
id: check_tags_exist
run: |
git fetch --tags
TAG_COUNT=$(git tag | wc -l)
if [ "$TAG_COUNT" -eq 0 ]; then
echo "has_tags=false" >> "$GITHUB_OUTPUT"
echo "latest_tag=v0.0.0" >> "$GITHUB_OUTPUT"
else
echo "has_tags=true" >> "$GITHUB_OUTPUT"
LATEST_TAG=$(git describe --tags --abbrev=0)
echo "latest_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
fi
- name: Check if meaningful commits exist since latest tag
id: check_commits
run: |
if [ "${{ steps.check_tags_exist.outputs.has_tags }}" = "false" ]; then
# No tags exist, so we should create first release
echo "commit_count=1" >> "$GITHUB_OUTPUT"
CHANGED_FILES=$(git ls-files | grep -v '^manifest.json$' || true)
if [ -n "$CHANGED_FILES" ]; then
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
printf '%s\n' "$CHANGED_FILES" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
else
echo "changed_files=Initial release" >> "$GITHUB_OUTPUT"
fi
else
LATEST_TAG="${{ steps.check_tags_exist.outputs.latest_tag }}"
CHANGED_FILES="$(git diff --name-only "${LATEST_TAG}..HEAD" | grep -v '^manifest.json$' || true)"
if [ -n "$CHANGED_FILES" ]; then
echo "commit_count=1" >> "$GITHUB_OUTPUT"
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
printf '%s\n' "$CHANGED_FILES" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
else
echo "commit_count=0" >> "$GITHUB_OUTPUT"
fi
fi
- name: Get latest release tag (from GitHub API)
id: get_latest_release
run: |
LATEST_RELEASE_TAG=$(curl -sL -H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/latest" | jq -r .tag_name)
if [ -z "$LATEST_RELEASE_TAG" ] || [ "$LATEST_RELEASE_TAG" = "null" ]; then
LATEST_RELEASE_TAG="v1.0.0"
fi
echo "latest_release_tag=$LATEST_RELEASE_TAG" >> "$GITHUB_OUTPUT"
echo "latest_release_version=${LATEST_RELEASE_TAG#v}" >> "$GITHUB_OUTPUT"
# -------------------------------
# Sync manifest.json to last release version if behind (only when no meaningful commits)
# -------------------------------
- name: 🛠 Ensure manifest.json matches latest release version
if: steps.check_commits.outputs.commit_count == '0'
run: |
if [ -f manifest.json ]; then
MANIFEST_VERSION=$(jq -r '.version // empty' manifest.json)
else
MANIFEST_VERSION=""
fi
LATEST_RELEASE_VERSION="${{ steps.get_latest_release.outputs.latest_release_version }}"
PYTHON_CODE="from packaging import version; \
print(version.parse('$LATEST_RELEASE_VERSION') > version.parse('$MANIFEST_VERSION') if '$MANIFEST_VERSION' else True)"
NEED_UPDATE=$(python3 -c "$PYTHON_CODE")
if [ "$NEED_UPDATE" = "True" ]; then
echo "Updating manifest.json to version $LATEST_RELEASE_VERSION (sync with release)"
jq --arg v "$LATEST_RELEASE_VERSION" '.version = $v' manifest.json > tmp.json && mv tmp.json manifest.json
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git add manifest.json
git commit -m "Sync manifest.json to release $LATEST_RELEASE_VERSION [🔄]" || echo "Nothing to commit"
git push origin main || true
else
echo "Manifest.json is already up-to-date with the latest release."
fi
# -------------------------------
# Continue normal workflow if commits exist
# -------------------------------
- name: 📃 Get list of changed files (Markdown bullet list)
if: steps.check_commits.outputs.commit_count != '0'
id: changed_files
run: |
BULLET_LIST="$(printf '%s\n' "${{ steps.check_commits.outputs.changed_files }}" | sed 's/^/- /')"
echo "CHANGED<<EOF" >> "$GITHUB_OUTPUT"
printf '%s\n' "$BULLET_LIST" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
COUNT="$(printf '%s\n' "${{ steps.check_commits.outputs.changed_files }}" | wc -l)"
echo "COUNT=$COUNT" >> "$GITHUB_OUTPUT"
- name: Get manifest version
if: steps.check_commits.outputs.commit_count != '0'
id: get_manifest_version
run: |
if [ -f manifest.json ]; then
MANIFEST_VERSION=$(jq -r '.version // empty' manifest.json)
if [ -z "$MANIFEST_VERSION" ] || [ "$MANIFEST_VERSION" = "null" ]; then
MANIFEST_VERSION="1.0.0"
fi
else
MANIFEST_VERSION="1.0.0"
fi
echo "manifest_version=$MANIFEST_VERSION" >> "$GITHUB_OUTPUT"
- name: Pick base version
if: steps.check_commits.outputs.commit_count != '0'
id: pick_base_version
run: |
LATEST_RELEASE="${{ steps.get_latest_release.outputs.latest_release_version }}"
MANIFEST="${{ steps.get_manifest_version.outputs.manifest_version }}"
BASE_VERSION=$(python3 -c "from packaging import version; \
print(str(max(version.parse('$LATEST_RELEASE'), version.parse('$MANIFEST'))))")
echo "base_version=$BASE_VERSION" >> "$GITHUB_OUTPUT"
- name: 🔢 Determine version
if: steps.check_commits.outputs.commit_count != '0'
id: version
run: |
BASE_VERSION="${{ steps.pick_base_version.outputs.base_version }}"
IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION"
COUNT="${{ steps.changed_files.outputs.COUNT }}"
if [ "$COUNT" -ge 5 ]; then
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
elif [ "$COUNT" -ge 3 ]; then
MINOR=$((MINOR + 1))
PATCH=0
else
PATCH=$((PATCH + 1))
fi
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
REPO_NAME="$(basename "$GITHUB_REPOSITORY")"
ZIP_NAME="${REPO_NAME}-${NEW_VERSION}.zip"
echo "VERSION=$NEW_VERSION" >> "$GITHUB_OUTPUT"
echo "ZIP_NAME=$ZIP_NAME" >> "$GITHUB_OUTPUT"
echo "REPO_NAME=$REPO_NAME" >> "$GITHUB_OUTPUT"
- name: 🛠 Update or create manifest.json
if: steps.check_commits.outputs.commit_count != '0'
run: |
VERSION="${{ steps.version.outputs.VERSION }}"
AUTHOR="Ivan Carlos"
VERSION_FILE="manifest.json"
if [ -f "$VERSION_FILE" ]; then
jq --arg v "$VERSION" --arg a "$AUTHOR" \
'.version = $v | .author = $a' "$VERSION_FILE" > tmp.json && mv tmp.json "$VERSION_FILE"
else
echo "{ \"version\": \"$VERSION\", \"author\": \"$AUTHOR\" }" > "$VERSION_FILE"
fi
- name: 💾 Commit and push updated manifest.json
if: steps.check_commits.outputs.commit_count != '0'
run: |
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git add manifest.json
git commit -m "Update manifest version to ${{ steps.version.outputs.VERSION }} [▶️]" || echo "Nothing to commit"
git push origin main
- name: 📦 Create ZIP package (excluding certain files)
if: steps.check_commits.outputs.commit_count != '0'
run: |
ZIP_NAME="${{ steps.version.outputs.ZIP_NAME }}"
zip -r "$ZIP_NAME" . -x ".git/*" ".github/*" "docker/*" ".dockerignore" "CNAME" "Dockerfile" "README.md" "LICENSE"
- name: 🚀 Create GitHub Release
if: steps.check_commits.outputs.commit_count != '0'
uses: softprops/action-gh-release@v2
with:
tag_name: "v${{ steps.version.outputs.VERSION }}"
name: "${{ steps.version.outputs.REPO_NAME }} v${{ steps.version.outputs.VERSION }}"
body: |
### Changelog
Files changed in this release:
${{ steps.changed_files.outputs.CHANGED }}
files: ${{ steps.version.outputs.ZIP_NAME }}
# ----- Docker steps -----
- name: 🔍 Check if Dockerfile exists
if: steps.check_commits.outputs.commit_count != '0'
id: dockerfile_check
run: |
if [ -f Dockerfile ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: 🛠 Set up Docker Buildx
if: steps.check_commits.outputs.commit_count != '0' && steps.dockerfile_check.outputs.exists == 'true'
uses: docker/setup-buildx-action@v3
- name: 🔐 Login to GitHub Container Registry
if: steps.check_commits.outputs.commit_count != '0' && steps.dockerfile_check.outputs.exists == 'true'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 🐳 Build and Push Docker image
if: steps.check_commits.outputs.commit_count != '0' && steps.dockerfile_check.outputs.exists == 'true'
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:latest

78
.github/workflows/update_readme.yml vendored Normal file
View File

@@ -0,0 +1,78 @@
name: Update README
# Allow GitHub Actions to commit and push changes
permissions:
contents: write
on:
workflow_dispatch:
schedule:
- cron: '0 4 * * *' # Every day at 4 AM UTC
jobs:
update-readme:
runs-on: ubuntu-latest
env:
SOURCE_REPO: ivancarlosti/.github
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 }}
path: source_readme
- name: Update README.md (buttons and footer)
run: |
set -e
REPO_NAME="${GITHUB_REPOSITORY##*/}"
# --- Extract buttons block from source ---
BUTTONS=$(awk '/<!-- buttons -->/{flag=1;next}/<!-- endbuttons -->/{flag=0}flag' source_readme/README.md)
BUTTONS_UPDATED=$(echo "$BUTTONS" | sed "s/\.github/${REPO_NAME}/g")
# --- Extract footer block from source (everything from <!-- footer --> onward) ---
FOOTER=$(awk '/<!-- footer -->/{flag=1}flag' source_readme/README.md)
# --- Replace buttons section in README.md ---
UPDATED=$(awk -v buttons="$BUTTONS_UPDATED" '
BEGIN { skip=0 }
/<!-- buttons -->/ {
print
print buttons
skip=1
next
}
/<!-- endbuttons -->/ && skip {
print
skip=0
next
}
!skip { print }
' README.md)
# --- Replace everything after <!-- footer --> with FOOTER ---
echo "$UPDATED" | awk -v footer="$FOOTER" '
/<!-- footer -->/ {
print footer
found=1
exit
}
{ print }
' > README.tmp && mv README.tmp README.md
- name: Remove source_readme from git index
run: git rm --cached -r source_readme || true
- name: Commit and push changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
file_pattern: README.md
commit_message: "Sync README from template [▶️]"
branch: ${{ github.ref_name }}

1
CNAME Normal file
View File

@@ -0,0 +1 @@
teste.git.icc.gg

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Ivan Carlos
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.

View File

@@ -1,2 +1,45 @@
# devicetester
# Device 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
1. Project inspired in multiple device complex testers across the web.
2. The music used on test is royalty free and generated by AI.
3. The page was co-created by AI.
<!-- footer -->
---
## 🧑‍💻 Consulting and technical support
* For personal support and queries, please submit a new issue to have it addressed.
* For commercial related questions, please [**contact me**][ivancarlos] for consulting costs.
## 🩷 Project support
| 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**](../..)⭐
|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
[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
[paypal]: https://icc.gg/donate
[sponsor]: https://github.com/sponsors/ivancarlosti

667
index.html Normal file
View File

@@ -0,0 +1,667 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<title>Teste de Dispositivos Pro</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #f8f9fa;
--text-color: #1f2937;
--card-bg: #ffffff;
--primary: #2563eb;
--primary-hover: #1d4ed8;
--danger: #dc2626;
--danger-hover: #b91c1c;
--border-color: #e5e7eb;
--success: #10b981;
}
body.dark-mode {
--bg-color: #111827;
--text-color: #f9fafb;
--card-bg: #1f2937;
--primary: #3b82f6;
--primary-hover: #60a5fa;
--border-color: #374151;
}
body {
font-family: 'Inter', sans-serif;
margin: 0;
padding: 20px;
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
}
.container {
max-width: 600px;
margin: 0 auto;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin: 0;
background: linear-gradient(90deg, var(--primary), #8b5cf6);
-webkit-background-clip: text;
-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 {
background-color: var(--card-bg);
padding: 24px;
margin-bottom: 20px;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid var(--border-color);
transition: transform 0.2s;
}
.test-card:hover {
transform: translateY(-2px);
}
h2 {
font-size: 1.1rem;
margin-top: 0;
margin-bottom: 16px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
}
label {
font-weight: 500;
display: block;
margin-top: 12px;
font-size: 0.9rem;
}
select,
textarea {
width: 100%;
padding: 10px;
box-sizing: border-box;
margin-top: 6px;
font-size: 0.95rem;
border: 1px solid var(--border-color);
border-radius: 6px;
background-color: var(--bg-color);
color: var(--text-color);
font-family: inherit;
}
button {
margin-top: 12px;
margin-right: 8px;
padding: 10px 16px;
font-size: 0.9rem;
font-weight: 500;
border: none;
border-radius: 6px;
background-color: var(--primary);
color: white;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: var(--primary-hover);
}
button.stop {
background-color: var(--danger);
}
button.stop:hover {
background-color: var(--danger-hover);
}
video {
width: 100%;
background: #000;
border-radius: 8px;
margin-top: 12px;
}
.bar-container {
width: 100%;
height: 8px;
background: var(--border-color);
border-radius: 4px;
overflow: hidden;
margin-top: 12px;
}
.bar-fill {
height: 100%;
background: var(--success);
width: 0%;
transition: width 0.1s linear;
}
#speaker-bar {
background: #eab308;
}
.status-text {
font-size: 0.85rem;
color: var(--text-color);
opacity: 0.8;
margin-top: 8px;
}
/* Screen Test Overlay */
#screen-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
z-index: 9999;
cursor: crosshair;
}
#screen-instruction {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 20px;
border-radius: 8px;
pointer-events: none;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Teste de Dispositivos</h1>
<button class="theme-toggle" onclick="toggleTheme()">🌓 Tema</button>
</header>
<!-- Rede -->
<div class="test-card">
<h2>📡 Rede e Navegador</h2>
<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>IPv6: <strong id="ipv6">...</strong> <span class="status-text" id="geo-ipv6"></span></p>
<p class="status-text" id="isp-label-ipv6"></p>
<label>User Agent:</label>
<textarea id="user-agent" rows="2" readonly></textarea>
</div>
<!-- Bateria -->
<div class="test-card">
<h2>🔋 Bateria</h2>
<p id="battery-status">Detectando...</p>
<div class="bar-container">
<div id="battery-bar" class="bar-fill" style="background-color: var(--primary);"></div>
</div>
</div>
<!-- Tela -->
<div class="test-card">
<h2>🖥️ Tela & Dead Pixel</h2>
<p class="status-text">Cicla entre cores primárias para identificar pixels mortos.</p>
<button onclick="startScreenTest()">Iniciar Teste de Tela</button>
</div>
<!-- Vibração -->
<div class="test-card">
<h2>📳 Vibração</h2>
<p class="status-text">Funciona apenas em dispositivos móveis compatíveis.</p>
<button onclick="testVibration()">Vibrar Dispositivo</button>
</div>
<!-- Webcam -->
<div class="test-card">
<h2>📷 Webcam</h2>
<select id="webcam-select"></select>
<div>
<button onclick="startWebcam()">Ligar</button>
<button onclick="stopWebcam()" class="stop">Desligar</button>
</div>
<video id="webcam" autoplay playsinline></video>
<p id="webcam-status" class="status-text">Status: Aguardando...</p>
</div>
<!-- Microfone -->
<div class="test-card">
<h2>🎤 Microfone</h2>
<select id="mic-select"></select>
<div>
<button onclick="startMic()">Monitorar Ao Vivo</button>
<button onclick="stopMic()" class="stop">Parar</button>
<button onclick="recordMicFor10s()">Gravar 10s</button>
</div>
<div class="bar-container">
<div id="mic-bar" class="bar-fill"></div>
</div>
<p id="mic-status" class="status-text">Status: Aguardando...</p>
<audio id="mic-playback" controls style="width: 100%; margin-top: 10px; display: none;"></audio>
</div>
<!-- Som -->
<div class="test-card">
<h2>🔊 Saída de Som</h2>
<select id="spk-select"></select>
<audio id="audio-test" src="test-sound.mp3" preload="auto"></audio>
<div>
<button id="playBtn" onclick="playSound()">Tocar Música</button>
<button onclick="stopSound()" class="stop">Parar</button>
</div>
<div class="bar-container">
<div id="speaker-bar" class="bar-fill" style="background-color: #f59e0b;"></div>
</div>
</div>
</div>
<!-- Overlay para teste de tela -->
<div id="screen-overlay" onclick="nextScreenColor()">
<div id="screen-instruction">Clique para trocar a cor. Escura para sair.</div>
</div>
<script>
// Theme
function toggleTheme() {
document.body.classList.toggle('dark-mode');
}
// Populate UA
document.getElementById("user-agent").value = navigator.userAgent;
// Elements
const webcamSelect = document.getElementById("webcam-select");
const micSelect = document.getElementById("mic-select");
const spkSelect = document.getElementById("spk-select");
const video = document.getElementById("webcam");
const micBar = document.getElementById("mic-bar");
const speakerBar = document.getElementById("speaker-bar");
const playBtn = document.getElementById("playBtn");
const audio = document.getElementById("audio-test");
const micPlayback = document.getElementById("mic-playback");
// State
let webcamStream = null;
let micStream = null;
let micAudioContext = null;
let micWorkletNode = null;
let speakerAudioContext = null;
let speakerWorkletNode = null;
let speakerSource = null;
let micRecorder = null;
let recordedChunks = [];
let recordAudioContext = null;
let recordWorkletNode = null;
// WORKLET CODE
function makeLevelWorkletURL() {
const code = `
class LevelMeterProcessor extends AudioWorkletProcessor {
process(inputs) {
const input = inputs[0];
if (input && input.length > 0) {
let maxAvg = 0;
// Iterate over all channels (e.g. left, right)
for (let c = 0; c < input.length; c++) {
const ch = input[c];
let sum = 0;
for (let i = 0; i < ch.length; i++) sum += Math.abs(ch[i]);
const avg = ch.length ? sum / ch.length : 0;
if (avg > maxAvg) maxAvg = avg;
}
this.port.postMessage(maxAvg);
}
return true;
}
}
registerProcessor('level-meter', LevelMeterProcessor);
`;
return URL.createObjectURL(new Blob([code], { type: 'application/javascript' }));
}
async function addLevelWorkletTo(ac) {
const url = makeLevelWorkletURL();
await ac.audioWorklet.addModule(url);
URL.revokeObjectURL(url);
}
// Devices
function listDevices() {
navigator.mediaDevices.enumerateDevices().then(devices => {
// Preserve selection if possible? For now simple rebuild
webcamSelect.innerHTML = "";
micSelect.innerHTML = "";
spkSelect.innerHTML = "";
devices.forEach(device => {
const option = document.createElement("option");
option.value = device.deviceId;
option.text = device.label || "Dispositivo não identificado";
if (device.kind === "videoinput") webcamSelect.appendChild(option.cloneNode(true));
if (device.kind === "audioinput") micSelect.appendChild(option.cloneNode(true));
if (device.kind === "audiooutput" && typeof spkSelect.setSinkId === "function")
spkSelect.appendChild(option.cloneNode(true));
});
if (spkSelect.options.length === 0) {
const option = document.createElement("option");
option.value = "";
option.text = "Saída Padrão";
spkSelect.appendChild(option);
}
});
}
// Webcam
function startWebcam() {
stopWebcam();
const deviceId = webcamSelect.value;
const constraints = { video: { deviceId: deviceId ? { exact: deviceId } : undefined } };
navigator.mediaDevices.getUserMedia(constraints)
.then(stream => {
webcamStream = stream;
video.srcObject = stream;
document.getElementById("webcam-status").textContent = "Webcam: Funcionando (OK)";
document.getElementById("webcam-status").style.color = "var(--success)";
})
.catch((e) => {
document.getElementById("webcam-status").textContent = "Erro: " + e.message;
document.getElementById("webcam-status").style.color = "var(--danger)";
});
}
function stopWebcam() {
if (webcamStream) {
webcamStream.getTracks().forEach(t => t.stop());
video.srcObject = null;
webcamStream = null;
document.getElementById("webcam-status").textContent = "Status: Parado";
document.getElementById("webcam-status").style.color = "var(--text-color)";
}
}
// Mic
async function startMic() {
stopMic();
const deviceId = micSelect.value;
const constraints = { audio: deviceId ? { deviceId: { exact: deviceId } } : true };
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: constraints.audio });
micStream = stream;
micAudioContext = new (window.AudioContext || window.webkitAudioContext)();
await addLevelWorkletTo(micAudioContext);
const source = micAudioContext.createMediaStreamSource(stream);
micWorkletNode = new AudioWorkletNode(micAudioContext, 'level-meter');
micWorkletNode.port.onmessage = (e) => {
const pct = Math.min(100, Math.round(e.data * 100));
micBar.style.width = pct + "%";
if (pct > 5) {
const st = document.getElementById("mic-status");
if (st.textContent !== "Capturando áudio...") {
st.textContent = "Capturando áudio...";
st.style.color = "var(--success)";
}
}
};
source.connect(micWorkletNode);
document.getElementById("mic-status").textContent = "Iniciando captura...";
} catch (e) {
document.getElementById("mic-status").textContent = "Erro: " + e.message;
document.getElementById("mic-status").style.color = "var(--danger)";
}
}
function stopMic() {
if (micWorkletNode) { try { micWorkletNode.disconnect(); } catch { } }
if (micAudioContext) { try { micAudioContext.close(); } catch { } }
if (micStream) { micStream.getTracks().forEach(t => t.stop()); }
micWorkletNode = null;
micAudioContext = null;
micStream = null;
micBar.style.width = "0%";
const st = document.getElementById("mic-status");
st.textContent = "Status: Parado";
st.style.color = "var(--text-color)";
}
async function recordMicFor10s() {
try {
stopMic(); // ensure clean state
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
micRecorder = new MediaRecorder(stream);
recordedChunks = [];
recordAudioContext = new (window.AudioContext || window.webkitAudioContext)();
await addLevelWorkletTo(recordAudioContext);
const source = recordAudioContext.createMediaStreamSource(stream);
recordWorkletNode = new AudioWorkletNode(recordAudioContext, 'level-meter');
recordWorkletNode.port.onmessage = (e) => {
const pct = Math.min(100, Math.round(e.data * 100));
micBar.style.width = pct + "%";
};
source.connect(recordWorkletNode);
micRecorder.ondataavailable = e => { if (e.data.size > 0) recordedChunks.push(e.data); };
micRecorder.onstop = () => {
try { recordWorkletNode.disconnect(); } catch { }
try { recordAudioContext.close(); } catch { }
micBar.style.width = "0%";
const blob = new Blob(recordedChunks, { type: "audio/webm" });
const url = URL.createObjectURL(blob);
micPlayback.src = url;
micPlayback.style.display = "block";
micPlayback.play();
};
micRecorder.start();
let remaining = 10;
const status = document.getElementById("mic-status");
status.style.color = "var(--primary)";
status.textContent = `Gravando... ${remaining}s`;
const countdown = setInterval(() => {
remaining--;
if (remaining > 0) status.textContent = `Gravando... ${remaining}s`;
}, 1000);
setTimeout(() => {
clearInterval(countdown);
micRecorder.stop();
stream.getTracks().forEach(t => t.stop());
status.textContent = "Reproduzindo gravação...";
status.style.color = "var(--text-color)";
}, 10000);
} catch (err) {
document.getElementById("mic-status").textContent = "Erro: " + err;
}
}
// Speaker
async function setupSpeakerAnalyzer() {
if (!speakerAudioContext) {
speakerAudioContext = new (window.AudioContext || window.webkitAudioContext)();
await addLevelWorkletTo(speakerAudioContext);
speakerSource = speakerAudioContext.createMediaElementSource(audio);
speakerWorkletNode = new AudioWorkletNode(speakerAudioContext, 'level-meter');
speakerWorkletNode.port.onmessage = (e) => {
const level = Math.min(100, Math.round(e.data * 100));
speakerBar.style.width = level + "%";
};
speakerSource.connect(speakerWorkletNode);
speakerSource.connect(speakerAudioContext.destination);
}
}
function playSound() {
playBtn.disabled = true;
setupSpeakerAnalyzer().then(() => {
if (speakerAudioContext.state === "suspended") {
speakerAudioContext.resume().then(startPlayback).catch(startPlayback);
} else {
startPlayback();
}
});
function startPlayback() {
const outputId = spkSelect.value;
playBtn.disabled = false;
if (typeof audio.setSinkId === "function" && outputId) {
audio.setSinkId(outputId).then(() => audio.play()).catch(() => audio.play());
} else {
audio.play();
}
}
}
function stopSound() {
audio.pause();
audio.currentTime = 0;
speakerBar.style.width = "0%";
if (speakerAudioContext && speakerAudioContext.state !== "closed") {
speakerAudioContext.suspend();
}
}
// IPs
function getIPv4AndISP() {
fetch("https://ipwhois.app/json/?lang=pt")
.then(res => res.json())
.then(data => {
document.getElementById("ipv4").textContent = data.ip || "Indisponível";
const geo = [data.city, data.region, data.country].filter(Boolean);
document.getElementById("geo-ipv4").textContent = geo.length ? "(" + geo.join(", ") + ")" : "";
document.getElementById("isp-label-ipv4").textContent = data.isp ? "ISP: " + decodeURIComponent(escape(data.isp)) : "";
})
.catch(() => { });
}
function getIPv6AndISP() {
fetch("https://api64.ipify.org?format=json")
.then(res => res.json())
.then(ip => {
if (ip.ip && ip.ip.includes(":")) {
document.getElementById("ipv6").textContent = ip.ip;
return fetch(`https://ipwhois.app/json/${ip.ip}?lang=pt`);
} else {
document.getElementById("ipv6").textContent = "Não detectado";
}
})
.then(res => res.json())
.then(data => {
if (!data) return;
const geo = [data.city, data.region, data.country].filter(Boolean);
document.getElementById("geo-ipv6").textContent = geo.length ? "(" + geo.join(", ") + ")" : "";
document.getElementById("isp-label-ipv6").textContent = data.isp ? "ISP: " + decodeURIComponent(escape(data.isp)) : "";
})
.catch(() => { });
}
// NEW FEATURES
// Battery
if (navigator.getBattery) {
navigator.getBattery().then(battery => {
function updateBattery() {
const level = Math.round(battery.level * 100);
const charging = battery.charging ? "⚡ Carregando" : "Desconectado";
document.getElementById("battery-status").textContent = `${level}% - ${charging}`;
document.getElementById("battery-bar").style.width = level + "%";
if (battery.charging) document.getElementById("battery-bar").style.background = "var(--success)";
else if (level < 20) document.getElementById("battery-bar").style.background = "var(--danger)";
else document.getElementById("battery-bar").style.background = "var(--primary)";
}
battery.addEventListener('levelchange', updateBattery);
battery.addEventListener('chargingchange', updateBattery);
updateBattery();
});
} else {
document.getElementById("battery-status").textContent = "API de Bateria não suportada neste navegador.";
document.getElementById("battery-bar").style.width = "0%";
}
// Vibration
function testVibration() {
if (navigator.vibrate) {
navigator.vibrate([200, 100, 200]); // Vibrate twice
} else {
alert("Seu dispositivo não suporta vibração via API web.");
}
}
// Screen
let screenColorIndex = 0;
const colors = ["#FF0000", "#00FF00", "#0000FF", "#FFFFFF", "#000000"];
function startScreenTest() {
const overlay = document.getElementById("screen-overlay");
overlay.style.display = "block";
document.documentElement.requestFullscreen().catch(() => { });
screenColorIndex = -1;
nextScreenColor();
}
function nextScreenColor() {
screenColorIndex++;
const overlay = document.getElementById("screen-overlay");
if (screenColorIndex >= colors.length) {
// Exit
overlay.style.display = "none";
if (document.fullscreenElement) document.exitFullscreen();
} else {
overlay.style.backgroundColor = colors[screenColorIndex];
// Hide instruction after first click for clean view
if (screenColorIndex > 0) document.getElementById("screen-instruction").style.display = "none";
else document.getElementById("screen-instruction").style.display = "block";
}
}
// On Load
window.onload = () => {
navigator.mediaDevices.getUserMedia({ audio: true }).then(s => {
s.getTracks().forEach(t => t.stop());
listDevices();
}).catch(() => listDevices());
getIPv4AndISP();
getIPv6AndISP();
};
navigator.mediaDevices.addEventListener("devicechange", listDevices);
audio.addEventListener("ended", () => {
speakerBar.style.width = "0%";
});
</script>
</body>
</html>

4
manifest.json Normal file
View File

@@ -0,0 +1,4 @@
{
"version": "1.0.10",
"author": "Ivan Carlos"
}

BIN
test-sound.mp3 Normal file

Binary file not shown.