Compare commits
39 Commits
49e981a4b5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e6ef7818da | |||
|
|
688d6ef42e | ||
| d96a641467 | |||
|
|
d501b9db29 | ||
| bf3570b623 | |||
| 67333c114c | |||
| 830af0d212 | |||
| d3349869e8 | |||
| 2e5f30451c | |||
|
|
85d49d55b6 | ||
| 54f93281c5 | |||
| 068c7282ea | |||
| efe538b49c | |||
| 252350e10e | |||
| 1cce73175c | |||
| ae3ce89516 | |||
| 2bbd388524 | |||
| d9e8389777 | |||
| f583109aab | |||
| fbf23d5b84 | |||
| fb0066767d | |||
| ff32dc3103 | |||
| c0878763dc | |||
| 0e4b568cea | |||
| 17267fb32a | |||
| 75bd84116f | |||
| e30aa089e4 | |||
| 236112a7ba | |||
| fd06e2fe83 | |||
| 880d327a5d | |||
| dcc279ddda | |||
| 306c762136 | |||
| 4f85ea84d3 | |||
| d012bfe1ca | |||
|
|
201c41e4f5 | ||
| 31541d66f0 | |||
|
|
ce1a0ab873 | ||
| 750a90f059 | |||
| 5bfb857f41 |
54
.gitea/workflows/1-version-assets.yml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
name: Version Static Assets
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'www/script.js'
|
||||||
|
- 'www/styles.css'
|
||||||
|
- 'www/logo.png'
|
||||||
|
- 'www/index.html'
|
||||||
|
- 'version-assets.sh'
|
||||||
|
- '.gitea/workflows/1-version-assets.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
version-assets:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.APP_SECRET }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Delete old versioned assets
|
||||||
|
run: |
|
||||||
|
echo "🗑️ Borrando archivos hasheados antiguos..."
|
||||||
|
rm -f www/*.*.js www/*.*.css www/*.*.png || true
|
||||||
|
git add -A
|
||||||
|
|
||||||
|
- name: Run asset versioning
|
||||||
|
run: |
|
||||||
|
chmod +x version-assets.sh
|
||||||
|
./version-assets.sh
|
||||||
|
|
||||||
|
- name: Check for changes
|
||||||
|
id: check_changes
|
||||||
|
run: |
|
||||||
|
if git diff --quiet && git diff --cached --quiet; then
|
||||||
|
echo "changes=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "No hay cambios para commitear"
|
||||||
|
else
|
||||||
|
echo "changes=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit and push versioned assets
|
||||||
|
if: steps.check_changes.outputs.changes == 'true'
|
||||||
|
run: |
|
||||||
|
git config --local user.email "ci@dariosevilla.es"
|
||||||
|
git config --local user.name "CI Action"
|
||||||
|
git add www/*.*.js www/*.*.css www/*.*.png www/index.html
|
||||||
|
git commit -m "chore: update asset versions [skip ci]"
|
||||||
|
git push
|
||||||
150
.gitea/workflows/2-build-apk.yml
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
name: Build Android APK
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Version Static Assets"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Only run if triggered by workflow_dispatch, push to main, or successful completion of version-assets
|
||||||
|
if: >
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
github.event_name == 'push' ||
|
||||||
|
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '21'
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Sync Capacitor
|
||||||
|
run: npx cap sync android
|
||||||
|
|
||||||
|
- name: Decode keystore
|
||||||
|
if: ${{ secrets.KEYSTORE_BASE64 != '' }}
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/impostor.keystore
|
||||||
|
|
||||||
|
- name: Create keystore.properties
|
||||||
|
if: ${{ secrets.KEYSTORE_BASE64 != '' }}
|
||||||
|
run: |
|
||||||
|
cat > android/keystore.properties << EOF
|
||||||
|
storeFile=../impostor.keystore
|
||||||
|
storePassword=${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
|
keyAlias=${{ secrets.KEY_ALIAS }}
|
||||||
|
keyPassword=${{ secrets.KEY_PASSWORD }}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Build Release APK
|
||||||
|
if: ${{ secrets.KEYSTORE_BASE64 != '' }}
|
||||||
|
working-directory: android
|
||||||
|
run: ./gradlew assembleRelease --no-daemon
|
||||||
|
|
||||||
|
- name: Build Debug APK (fallback)
|
||||||
|
if: ${{ secrets.KEYSTORE_BASE64 == '' }}
|
||||||
|
working-directory: android
|
||||||
|
run: ./gradlew assembleDebug --no-daemon
|
||||||
|
|
||||||
|
- name: Rename APK
|
||||||
|
run: |
|
||||||
|
if [ -f android/app/build/outputs/apk/release/app-release.apk ]; then
|
||||||
|
cp android/app/build/outputs/apk/release/app-release.apk impostor-game.apk
|
||||||
|
else
|
||||||
|
cp android/app/build/outputs/apk/debug/app-debug.apk impostor-game.apk
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update latest release (Gitea API)
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
API_BASE="${GITEA_API_URL:-${GITHUB_API_URL:-}}"
|
||||||
|
if [ -z "$API_BASE" ]; then
|
||||||
|
if [ -n "${GITEA_SERVER_URL:-}" ]; then
|
||||||
|
API_BASE="${GITEA_SERVER_URL}/api/v1"
|
||||||
|
elif [ -n "${GITHUB_SERVER_URL:-}" ]; then
|
||||||
|
API_BASE="${GITHUB_SERVER_URL}/api/v1"
|
||||||
|
else
|
||||||
|
echo "::error::Missing API base (GITEA_API_URL/GITHUB_API_URL)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOKEN="${TOKEN:-${GITHUB_TOKEN:-}}"
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
echo "::error::Missing API token (GITHUB_TOKEN or secrets.GITEA_TOKEN)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPO="${GITHUB_REPOSITORY}"
|
||||||
|
TAG="latest"
|
||||||
|
NAME="Latest Build"
|
||||||
|
BODY=$'Última versión del APK generada automáticamente.\n\nCommit: '"${GITHUB_SHA}"
|
||||||
|
ASSET_NAME="impostor-game.apk"
|
||||||
|
AUTH_HEADER="Authorization: token ${TOKEN}"
|
||||||
|
export TAG NAME BODY ASSET_NAME
|
||||||
|
|
||||||
|
tmp_file="$(mktemp)"
|
||||||
|
status="$(curl -s -o "$tmp_file" -w "%{http_code}" -H "$AUTH_HEADER" \
|
||||||
|
"$API_BASE/repos/$REPO/releases/tags/$TAG")"
|
||||||
|
|
||||||
|
if [ "$status" = "200" ]; then
|
||||||
|
release_id="$(node -e 'const fs=require("fs");const r=JSON.parse(fs.readFileSync(0,"utf8"));console.log(r.id);' < "$tmp_file")"
|
||||||
|
update_payload="$(node -e 'console.log(JSON.stringify({name:process.env.NAME,body:process.env.BODY,prerelease:false,draft:false}))')"
|
||||||
|
curl -fsSL -X PATCH -H "$AUTH_HEADER" -H "Content-Type: application/json" \
|
||||||
|
-d "$update_payload" "$API_BASE/repos/$REPO/releases/$release_id" >/dev/null
|
||||||
|
elif [ "$status" = "404" ]; then
|
||||||
|
create_payload="$(node -e 'console.log(JSON.stringify({tag_name:process.env.TAG,name:process.env.NAME,body:process.env.BODY,prerelease:false,draft:false}))')"
|
||||||
|
release_id="$(curl -fsSL -X POST -H "$AUTH_HEADER" -H "Content-Type: application/json" \
|
||||||
|
-d "$create_payload" "$API_BASE/repos/$REPO/releases" \
|
||||||
|
| node -e 'const fs=require("fs");const r=JSON.parse(fs.readFileSync(0,"utf8"));console.log(r.id);')"
|
||||||
|
else
|
||||||
|
echo "::error::Failed to fetch release tag (${status})."
|
||||||
|
cat "$tmp_file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm -f "$tmp_file"
|
||||||
|
|
||||||
|
assets_json="$(curl -fsSL -H "$AUTH_HEADER" "$API_BASE/repos/$REPO/releases/$release_id/assets")"
|
||||||
|
asset_id="$(node -e 'const fs=require("fs");const assets=JSON.parse(fs.readFileSync(0,"utf8"));const name=process.env.ASSET_NAME;const match=assets.find(a=>a.name===name);if (match) console.log(match.id);' <<<"$assets_json")"
|
||||||
|
|
||||||
|
if [ -n "${asset_id:-}" ]; then
|
||||||
|
curl -fsSL -X DELETE -H "$AUTH_HEADER" \
|
||||||
|
"$API_BASE/repos/$REPO/releases/$release_id/assets/$asset_id" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -fsSL -X POST -H "$AUTH_HEADER" \
|
||||||
|
-F "attachment=@${ASSET_NAME}" \
|
||||||
|
"$API_BASE/repos/$REPO/releases/$release_id/assets?name=${ASSET_NAME}" >/dev/null
|
||||||
95
.github/workflows/build-apk.yml
vendored
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
name: Build Android APK
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '21'
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Sync Capacitor
|
||||||
|
run: npx cap sync android
|
||||||
|
|
||||||
|
- name: Check if keystore exists
|
||||||
|
id: keystore-check
|
||||||
|
run: |
|
||||||
|
if [ -n "$KEYSTORE_BASE64" ]; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
|
||||||
|
|
||||||
|
- name: Decode keystore
|
||||||
|
if: steps.keystore-check.outputs.exists == 'true'
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/impostor.keystore
|
||||||
|
|
||||||
|
- name: Create keystore.properties
|
||||||
|
if: steps.keystore-check.outputs.exists == 'true'
|
||||||
|
run: |
|
||||||
|
cat > android/keystore.properties << EOF
|
||||||
|
storeFile=../impostor.keystore
|
||||||
|
storePassword=${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
|
keyAlias=${{ secrets.KEY_ALIAS }}
|
||||||
|
keyPassword=${{ secrets.KEY_PASSWORD }}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Build Release APK
|
||||||
|
if: steps.keystore-check.outputs.exists == 'true'
|
||||||
|
working-directory: android
|
||||||
|
run: ./gradlew assembleRelease --no-daemon
|
||||||
|
|
||||||
|
- name: Build Debug APK (fallback)
|
||||||
|
if: steps.keystore-check.outputs.exists == 'false'
|
||||||
|
working-directory: android
|
||||||
|
run: ./gradlew assembleDebug --no-daemon
|
||||||
|
|
||||||
|
- name: Rename APK
|
||||||
|
run: |
|
||||||
|
if [ "${{ steps.keystore-check.outputs.exists }}" == "true" ]; then
|
||||||
|
cp android/app/build/outputs/apk/release/app-release.apk impostor-game.apk
|
||||||
|
else
|
||||||
|
cp android/app/build/outputs/apk/debug/app-debug.apk impostor-game.apk
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Update latest release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: latest
|
||||||
|
name: Latest Build
|
||||||
|
body: |
|
||||||
|
Última versión del APK generada automáticamente.
|
||||||
|
|
||||||
|
Commit: ${{ github.sha }}
|
||||||
|
files: impostor-game.apk
|
||||||
|
prerelease: false
|
||||||
|
make_latest: true
|
||||||
19
.gitignore
vendored
@@ -1 +1,20 @@
|
|||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Android build artifacts
|
||||||
|
android/app/build/
|
||||||
|
android/.gradle/
|
||||||
|
android/local.properties
|
||||||
|
android/keystore.properties
|
||||||
|
*.apk
|
||||||
|
*.aab
|
||||||
|
*.keystore
|
||||||
|
*.jks
|
||||||
|
|
||||||
|
# iOS build artifacts (if added later)
|
||||||
|
ios/App/Pods/
|
||||||
|
ios/App/App.xcworkspace/
|
||||||
|
ios/DerivedData/
|
||||||
|
*.ipa
|
||||||
|
|||||||
@@ -82,3 +82,4 @@ This makes the game more interesting as impostors have a related word to work wi
|
|||||||
- State is saved in `localStorage` (`impostorGameStateV2`). Use "New match" to clear it.
|
- State is saved in `localStorage` (`impostorGameStateV2`). Use "New match" to clear it.
|
||||||
- Mobile optimized: **No scrolling required** on setup screen, reveal via swipe or tap, compact adaptive UI.
|
- Mobile optimized: **No scrolling required** on setup screen, reveal via swipe or tap, compact adaptive UI.
|
||||||
- Pool selection grid has vertical scroll when needed to fit all 22 categories.
|
- Pool selection grid has vertical scroll when needed to fit all 22 categories.
|
||||||
|
|
||||||
|
|||||||
101
android/.gitignore
vendored
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
||||||
|
|
||||||
|
# Built application files
|
||||||
|
*.apk
|
||||||
|
*.aar
|
||||||
|
*.ap_
|
||||||
|
*.aab
|
||||||
|
|
||||||
|
# Files for the ART/Dalvik VM
|
||||||
|
*.dex
|
||||||
|
|
||||||
|
# Java class files
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
bin/
|
||||||
|
gen/
|
||||||
|
out/
|
||||||
|
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||||
|
# release/
|
||||||
|
|
||||||
|
# Gradle files
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Local configuration file (sdk path, etc)
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# Proguard folder generated by Eclipse
|
||||||
|
proguard/
|
||||||
|
|
||||||
|
# Log Files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Android Studio Navigation editor temp files
|
||||||
|
.navigation/
|
||||||
|
|
||||||
|
# Android Studio captures folder
|
||||||
|
captures/
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
*.iml
|
||||||
|
.idea/workspace.xml
|
||||||
|
.idea/tasks.xml
|
||||||
|
.idea/gradle.xml
|
||||||
|
.idea/assetWizardSettings.xml
|
||||||
|
.idea/dictionaries
|
||||||
|
.idea/libraries
|
||||||
|
# Android Studio 3 in .gitignore file.
|
||||||
|
.idea/caches
|
||||||
|
.idea/modules.xml
|
||||||
|
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||||
|
.idea/navEditor.xml
|
||||||
|
|
||||||
|
# Keystore files
|
||||||
|
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||||
|
#*.jks
|
||||||
|
#*.keystore
|
||||||
|
|
||||||
|
# External native build folder generated in Android Studio 2.2 and later
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Google Services (e.g. APIs or Firebase)
|
||||||
|
# google-services.json
|
||||||
|
|
||||||
|
# Freeline
|
||||||
|
freeline.py
|
||||||
|
freeline/
|
||||||
|
freeline_project_description.json
|
||||||
|
|
||||||
|
# fastlane
|
||||||
|
fastlane/report.xml
|
||||||
|
fastlane/Preview.html
|
||||||
|
fastlane/screenshots
|
||||||
|
fastlane/test_output
|
||||||
|
fastlane/readme.md
|
||||||
|
|
||||||
|
# Version control
|
||||||
|
vcs.xml
|
||||||
|
|
||||||
|
# lint
|
||||||
|
lint/intermediates/
|
||||||
|
lint/generated/
|
||||||
|
lint/outputs/
|
||||||
|
lint/tmp/
|
||||||
|
# lint/reports/
|
||||||
|
|
||||||
|
# Android Profiling
|
||||||
|
*.hprof
|
||||||
|
|
||||||
|
# Cordova plugins for Capacitor
|
||||||
|
capacitor-cordova-android-plugins
|
||||||
|
|
||||||
|
# Copied web assets
|
||||||
|
app/src/main/assets/public
|
||||||
|
|
||||||
|
# Generated Config files
|
||||||
|
app/src/main/assets/capacitor.config.json
|
||||||
|
app/src/main/assets/capacitor.plugins.json
|
||||||
|
app/src/main/res/xml/config.xml
|
||||||
2
android/app/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/build/*
|
||||||
|
!/build/.npmkeep
|
||||||
77
android/app/build.gradle
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||||
|
def keystoreProperties = new Properties()
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "es.dariosevilla.impostor"
|
||||||
|
compileSdk = rootProject.ext.compileSdkVersion
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "es.dariosevilla.impostor"
|
||||||
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
aaptOptions {
|
||||||
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||||
|
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
storeFile file(keystoreProperties['storeFile'])
|
||||||
|
storePassword keystoreProperties['storePassword']
|
||||||
|
keyAlias keystoreProperties['keyAlias']
|
||||||
|
keyPassword keystoreProperties['keyPassword']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
flatDir{
|
||||||
|
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||||
|
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||||
|
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||||
|
implementation project(':capacitor-android')
|
||||||
|
testImplementation "junit:junit:$junitVersion"
|
||||||
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||||
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||||
|
implementation project(':capacitor-cordova-android-plugins')
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: 'capacitor.build.gradle'
|
||||||
|
|
||||||
|
try {
|
||||||
|
def servicesJSON = file('google-services.json')
|
||||||
|
if (servicesJSON.text) {
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||||
|
}
|
||||||
19
android/app/capacitor.build.gradle
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (hasProperty('postBuildExtras')) {
|
||||||
|
postBuildExtras()
|
||||||
|
}
|
||||||
21
android/app/proguard-rules.pro
vendored
Normal file
@@ -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,26 @@
|
|||||||
|
package com.getcapacitor.myapp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class ExampleInstrumentedTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void useAppContext() throws Exception {
|
||||||
|
// Context of the app under test.
|
||||||
|
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||||
|
|
||||||
|
assertEquals("com.getcapacitor.app", appContext.getPackageName());
|
||||||
|
}
|
||||||
|
}
|
||||||
41
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/title_activity_main"
|
||||||
|
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths"></meta-data>
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<!-- Permissions -->
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package es.dariosevilla.impostor;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import androidx.core.view.WindowCompat;
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat;
|
||||||
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
|
||||||
|
public class MainActivity extends BridgeActivity {
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
Window window = getWindow();
|
||||||
|
|
||||||
|
// Enable edge-to-edge mode so CSS safe-area-inset-* values work correctly
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false);
|
||||||
|
|
||||||
|
// Make status bar transparent so content can draw underneath
|
||||||
|
window.setStatusBarColor(Color.TRANSPARENT);
|
||||||
|
window.setNavigationBarColor(Color.TRANSPARENT);
|
||||||
|
|
||||||
|
// Set status bar icons to light (for dark backgrounds) or dark (for light backgrounds)
|
||||||
|
WindowInsetsControllerCompat insetsController = WindowCompat.getInsetsController(window, window.getDecorView());
|
||||||
|
if (insetsController != null) {
|
||||||
|
// Use dark icons (false = dark icons for light status bar background)
|
||||||
|
// The CSS handles the actual content offset via safe-area-inset-top
|
||||||
|
insetsController.setAppearanceLightStatusBars(false);
|
||||||
|
insetsController.setAppearanceLightNavigationBars(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
android/app/src/main/res/drawable-land-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
android/app/src/main/res/drawable-land-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
android/app/src/main/res/drawable-land-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
android/app/src/main/res/drawable-land-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/drawable-land-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
android/app/src/main/res/drawable-port-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
android/app/src/main/res/drawable-port-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
android/app/src/main/res/drawable-port-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
android/app/src/main/res/drawable-port-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/drawable-port-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="78.5885"
|
||||||
|
android:endY="90.9159"
|
||||||
|
android:startX="48.7653"
|
||||||
|
android:startY="61.0927"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1" />
|
||||||
|
</vector>
|
||||||
170
android/app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -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:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#26A69A"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
</vector>
|
||||||
BIN
android/app/src/main/res/drawable/splash.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
12
android/app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -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="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -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="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 17 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
7
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Impostor</string>
|
||||||
|
<string name="title_activity_main">Impostor</string>
|
||||||
|
<string name="package_name">es.dariosevilla.impostor</string>
|
||||||
|
<string name="custom_url_scheme">es.dariosevilla.impostor</string>
|
||||||
|
</resources>
|
||||||
22
android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="windowNoTitle">true</item>
|
||||||
|
<item name="android:background">@null</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||||
|
<item name="android:background">@drawable/splash</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
5
android/app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-path name="my_images" path="." />
|
||||||
|
<cache-path name="my_cache_images" path="." />
|
||||||
|
</paths>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.getcapacitor.myapp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
public class ExampleUnitTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addition_isCorrect() throws Exception {
|
||||||
|
assertEquals(4, 2 + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
android/build.gradle
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:8.13.0'
|
||||||
|
classpath 'com.google.gms:google-services:4.4.4'
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "variables.gradle"
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
afterEvaluate { project ->
|
||||||
|
if (project.hasProperty("android")) {
|
||||||
|
android {
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
3
android/capacitor.settings.gradle
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
|
include ':capacitor-android'
|
||||||
|
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||||
22
android/gradle.properties
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 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=-Xmx1536m
|
||||||
|
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec: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
|
||||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
7
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
251
android/gradlew
vendored
Executable file
@@ -0,0 +1,251 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# 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
android/gradlew.bat
vendored
Normal file
@@ -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
|
||||||
5
android/settings.gradle
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
include ':app'
|
||||||
|
include ':capacitor-cordova-android-plugins'
|
||||||
|
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
||||||
|
|
||||||
|
apply from: 'capacitor.settings.gradle'
|
||||||
16
android/variables.gradle
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
ext {
|
||||||
|
minSdkVersion = 24
|
||||||
|
compileSdkVersion = 36
|
||||||
|
targetSdkVersion = 36
|
||||||
|
androidxActivityVersion = '1.11.0'
|
||||||
|
androidxAppCompatVersion = '1.7.1'
|
||||||
|
androidxCoordinatorLayoutVersion = '1.3.0'
|
||||||
|
androidxCoreVersion = '1.17.0'
|
||||||
|
androidxFragmentVersion = '1.8.9'
|
||||||
|
coreSplashScreenVersion = '1.2.0'
|
||||||
|
androidxWebkitVersion = '1.14.0'
|
||||||
|
junitVersion = '4.13.2'
|
||||||
|
androidxJunitVersion = '1.3.0'
|
||||||
|
androidxEspressoCoreVersion = '3.7.0'
|
||||||
|
cordovaAndroidVersion = '14.0.1'
|
||||||
|
}
|
||||||
15
capacitor.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { CapacitorConfig } from '@capacitor/cli';
|
||||||
|
|
||||||
|
const config: CapacitorConfig = {
|
||||||
|
appId: 'es.dariosevilla.impostor',
|
||||||
|
appName: 'Juego del Impostor',
|
||||||
|
webDir: 'www',
|
||||||
|
server: {
|
||||||
|
androidScheme: 'https'
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
backgroundColor: '#0a0a0a'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
1137
package-lock.json
generated
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "web-imposter-game",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Role-based impostor-style game for mobile, 100% in the browser with no backend.",
|
||||||
|
"main": "script.js",
|
||||||
|
"scripts": {
|
||||||
|
"cap:sync": "npx cap sync",
|
||||||
|
"cap:open:android": "npx cap open android",
|
||||||
|
"cap:run:android": "npx cap run android"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.dariosevilla.es/dasemu/web-imposter-game"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@capacitor/android": "^8.0.1",
|
||||||
|
"@capacitor/cli": "^8.0.1",
|
||||||
|
"@capacitor/core": "^8.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
86
version-assets.sh
Executable file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Script de versionado de archivos estáticos
|
||||||
|
# Genera hashes basados en el contenido y actualiza referencias en HTML
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 Iniciando versionado de archivos estáticos..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Directorio de trabajo
|
||||||
|
WWW_DIR="www"
|
||||||
|
|
||||||
|
# Archivos a versionar
|
||||||
|
ASSETS=("script.js" "styles.css" "logo.png")
|
||||||
|
HTML_FILE="index.html"
|
||||||
|
|
||||||
|
# Función para generar hash MD5 de un archivo
|
||||||
|
generate_hash() {
|
||||||
|
local file=$1
|
||||||
|
md5sum "$file" | cut -c1-8
|
||||||
|
}
|
||||||
|
|
||||||
|
# Función para obtener el nombre versionado
|
||||||
|
get_versioned_name() {
|
||||||
|
local file=$1
|
||||||
|
local hash=$2
|
||||||
|
local base="${file%.*}"
|
||||||
|
local ext="${file##*.}"
|
||||||
|
echo "${base}.${hash}.${ext}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Limpiar archivos versionados antiguos
|
||||||
|
echo "🗑️ Limpiando versiones antiguas..."
|
||||||
|
rm -f "$WWW_DIR"/*.*.js "$WWW_DIR"/*.*.css "$WWW_DIR"/*.*.png 2>/dev/null || true
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Crear backup del HTML
|
||||||
|
cp "$WWW_DIR/$HTML_FILE" "$WWW_DIR/${HTML_FILE}.bak"
|
||||||
|
|
||||||
|
# Versionar cada archivo
|
||||||
|
for asset in "${ASSETS[@]}"; do
|
||||||
|
asset_path="$WWW_DIR/$asset"
|
||||||
|
|
||||||
|
if [[ ! -f "$asset_path" ]]; then
|
||||||
|
echo "⚠️ Advertencia: $asset_path no encontrado, saltando..."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generar hash
|
||||||
|
hash=$(generate_hash "$asset_path")
|
||||||
|
versioned=$(get_versioned_name "$asset" "$hash")
|
||||||
|
versioned_path="$WWW_DIR/$versioned"
|
||||||
|
|
||||||
|
# Copiar archivo con versión
|
||||||
|
echo "📦 Versionando: $asset → $versioned"
|
||||||
|
cp "$asset_path" "$versioned_path"
|
||||||
|
|
||||||
|
# Obtener nombre base y extensión para el patrón
|
||||||
|
base="${asset%.*}"
|
||||||
|
ext="${asset##*.}"
|
||||||
|
|
||||||
|
# Actualizar referencia en HTML (busca versión original o hasheada)
|
||||||
|
case "$asset" in
|
||||||
|
*.js)
|
||||||
|
sed -i -E "s|src=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|src=\"${versioned}\"|g" "$WWW_DIR/$HTML_FILE"
|
||||||
|
;;
|
||||||
|
*.css)
|
||||||
|
sed -i -E "s|href=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|href=\"${versioned}\"|g" "$WWW_DIR/$HTML_FILE"
|
||||||
|
;;
|
||||||
|
*.png)
|
||||||
|
sed -i -E "s|href=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|href=\"${versioned}\"|g" "$WWW_DIR/$HTML_FILE"
|
||||||
|
sed -i -E "s|src=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|src=\"${versioned}\"|g" "$WWW_DIR/$HTML_FILE"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "✏️ Actualizado: $asset → $versioned en $HTML_FILE"
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
# Limpiar backup
|
||||||
|
rm -f "$WWW_DIR/${HTML_FILE}.bak"
|
||||||
|
|
||||||
|
echo "✅ Versionado completado exitosamente!"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Archivos versionados:"
|
||||||
|
ls -1 "$WWW_DIR"/*.*.{js,css,png} 2>/dev/null || echo " (ninguno)"
|
||||||
@@ -2,13 +2,92 @@
|
|||||||
<html lang="es">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
<title>Juego del Impostor</title>
|
<title>Juego del Impostor - Juego de Rol Gratis Online | Encuentra al Impostor</title>
|
||||||
|
|
||||||
|
<!-- SEO Meta Tags -->
|
||||||
|
<meta name="description" content="Juego del Impostor: un emocionante juego de rol social gratuito para 3-10 jugadores. Descubre quién es el impostor antes de que sea tarde. Sin descargas, juega desde el navegador.">
|
||||||
|
<meta name="keywords" content="juego del impostor, impostor game, juego de rol, juego social, juego gratis, juego online, juego de palabras, juego de deducción, juego para fiestas, juego multijugador, among us estilo">
|
||||||
|
<meta name="author" content="Darío Sevilla">
|
||||||
|
<meta name="robots" content="index, follow">
|
||||||
|
<meta name="theme-color" content="#1a1a2e">
|
||||||
|
<link rel="canonical" href="https://impostor.dariosevilla.es/">
|
||||||
|
<link rel="alternate" hreflang="es" href="https://impostor.dariosevilla.es/">
|
||||||
|
<link rel="alternate" hreflang="en" href="https://impostor.dariosevilla.es/?lang=en">
|
||||||
|
<link rel="alternate" hreflang="x-default" href="https://impostor.dariosevilla.es/">
|
||||||
|
|
||||||
|
<!-- Open Graph / Facebook -->
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://impostor.dariosevilla.es/">
|
||||||
|
<meta property="og:title" content="Juego del Impostor - Juego de Rol Social Gratis">
|
||||||
|
<meta property="og:description" content="¿Podrás descubrir quién es el impostor? Juego de deducción social gratuito para 3-10 jugadores. Sin descargas, juega directamente en tu navegador.">
|
||||||
|
<meta property="og:image" content="https://impostor.dariosevilla.es/og-image.png">
|
||||||
|
<meta property="og:image:width" content="1200">
|
||||||
|
<meta property="og:image:height" content="630">
|
||||||
|
<meta property="og:locale" content="es_ES">
|
||||||
|
<meta property="og:locale:alternate" content="en_US">
|
||||||
|
<meta property="og:site_name" content="Juego del Impostor">
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:url" content="https://impostor.dariosevilla.es/">
|
||||||
|
<meta name="twitter:title" content="Juego del Impostor - Juego de Rol Social Gratis">
|
||||||
|
<meta name="twitter:description" content="¿Podrás descubrir quién es el impostor? Juego de deducción social para 3-10 jugadores. Sin descargas.">
|
||||||
|
<meta name="twitter:image" content="https://impostor.dariosevilla.es/og-image.png">
|
||||||
|
|
||||||
|
<!-- Additional SEO -->
|
||||||
|
<meta name="application-name" content="Juego del Impostor">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Impostor">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="format-detection" content="telephone=no">
|
||||||
|
|
||||||
|
<!-- Structured Data JSON-LD -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebApplication",
|
||||||
|
"name": "Juego del Impostor",
|
||||||
|
"alternateName": "The Impostor Game",
|
||||||
|
"description": "Juego de rol social gratuito donde los jugadores deben descubrir quién es el impostor usando pistas y deducción.",
|
||||||
|
"url": "https://impostor.dariosevilla.es/",
|
||||||
|
"applicationCategory": "GameApplication",
|
||||||
|
"operatingSystem": "Web Browser",
|
||||||
|
"offers": {
|
||||||
|
"@type": "Offer",
|
||||||
|
"price": "0",
|
||||||
|
"priceCurrency": "EUR"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "Darío Sevilla",
|
||||||
|
"url": "https://dariosevilla.es"
|
||||||
|
},
|
||||||
|
"inLanguage": ["es", "en"],
|
||||||
|
"genre": ["Party Game", "Social Deduction", "Word Game"],
|
||||||
|
"numberOfPlayers": {
|
||||||
|
"@type": "QuantitativeValue",
|
||||||
|
"minValue": 3,
|
||||||
|
"maxValue": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<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>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Crimson+Text:wght@600;700&family=Courier+Prime:wght@400;700&family=JetBrains+Mono:wght@400;700;800&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Crimson+Text:wght@600;700&family=Courier+Prime:wght@400;700&family=JetBrains+Mono:wght@400;700;800&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.0590c814.css">
|
||||||
<link rel="icon" type="image/png" href="logo.png">
|
<link rel="icon" type="image/png" href="logo.78f51359.png">
|
||||||
|
<link rel="sitemap" type="application/xml" href="/www/sitemap.xml">
|
||||||
|
<link rel="stylesheet" href="styles.0590c814.css">
|
||||||
|
<link rel="icon" type="image/png" href="logo.78f51359.png">
|
||||||
|
<link rel="manifest" href="manifest.webmanifest">
|
||||||
|
<meta name="theme-color" content="#ff4444">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Impostor">
|
||||||
|
<link rel="apple-touch-icon" href="logo.78f51359.png">
|
||||||
<script defer src="https://analytics.dariosevilla.es/script.js" data-website-id="0520a008-d309-477f-9742-b4a674ac42eb"></script>
|
<script defer src="https://analytics.dariosevilla.es/script.js" data-website-id="0520a008-d309-477f-9742-b4a674ac42eb"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -38,12 +117,18 @@
|
|||||||
<!-- Welcome screen -->
|
<!-- Welcome screen -->
|
||||||
<div id="welcome-screen" class="screen active">
|
<div id="welcome-screen" class="screen active">
|
||||||
<div class="welcome-content">
|
<div class="welcome-content">
|
||||||
<img src="logo.png" alt="Logo" class="welcome-logo">
|
<img src="logo.78f51359.png" alt="Logo" class="welcome-logo">
|
||||||
<h1 class="welcome-title">Juego del Impostor</h1>
|
<h1 class="welcome-title">Juego del Impostor</h1>
|
||||||
<p class="welcome-subtitle">¿Podrás descubrir quién es el impostor?</p>
|
<p class="welcome-subtitle">¿Podrás descubrir quién es el impostor?</p>
|
||||||
<div class="welcome-buttons">
|
<div class="welcome-buttons">
|
||||||
<button onclick="showScreen('setup-screen')" class="btn-primary">Jugar</button>
|
<button onclick="showScreen('setup-screen')" class="btn-primary">Jugar</button>
|
||||||
<button onclick="showScreen('rules-screen')" class="btn-secondary">Reglas</button>
|
<button onclick="showScreen('rules-screen')" class="btn-secondary">Reglas</button>
|
||||||
|
<a href="https://git.dariosevilla.es/dasemu/web-imposter-game/releases/download/latest/impostor-game.apk"
|
||||||
|
class="btn-download"
|
||||||
|
id="download-app-btn"
|
||||||
|
style="display: none;">
|
||||||
|
📱 Descargar App Android
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="welcome-credits">Creado por Darío Sevilla</p>
|
<p class="welcome-credits">Creado por Darío Sevilla</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,6 +189,19 @@
|
|||||||
<label for="deliberation-time">Deliberación (seg):</label>
|
<label for="deliberation-time">Deliberación (seg):</label>
|
||||||
<input type="number" id="deliberation-time" min="30" max="300" step="10" value="170">
|
<input type="number" id="deliberation-time" min="30" max="300" step="10" value="170">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group compact">
|
||||||
|
<label id="voting-mode-label">Modo de votación:</label>
|
||||||
|
<div class="voting-mode-selector" id="voting-mode-selector">
|
||||||
|
<button type="button" class="voting-mode-btn selected" id="voting-mode-individual" onclick="setVotingMode('individual')">
|
||||||
|
<span class="voting-mode-icon">🗳️</span>
|
||||||
|
<span class="voting-mode-name">Individual</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="voting-mode-btn" id="voting-mode-raisedHand" onclick="setVotingMode('raisedHand')">
|
||||||
|
<span class="voting-mode-icon">✋</span>
|
||||||
|
<span class="voting-mode-name">Mano alzada</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button onclick="goToPools()">Siguiente</button>
|
<button onclick="goToPools()">Siguiente</button>
|
||||||
<button class="ghost" onclick="showScreen('welcome-screen')">← Volver</button>
|
<button class="ghost" onclick="showScreen('welcome-screen')">← Volver</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,7 +209,6 @@
|
|||||||
<!-- Pools selection screen -->
|
<!-- Pools selection screen -->
|
||||||
<div id="pools-screen" class="screen">
|
<div id="pools-screen" class="screen">
|
||||||
<h1>Selección de Pools</h1>
|
<h1>Selección de Pools</h1>
|
||||||
<p class="info-text">Toca para seleccionar las categorías de palabras que quieres usar en la partida.</p>
|
|
||||||
<div class="pool-buttons-wrapper">
|
<div class="pool-buttons-wrapper">
|
||||||
<div id="pool-buttons" class="pool-buttons"></div>
|
<div id="pool-buttons" class="pool-buttons"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,7 +228,6 @@
|
|||||||
<div id="pre-reveal-screen" class="screen">
|
<div id="pre-reveal-screen" class="screen">
|
||||||
<h1>Listo para revelar</h1>
|
<h1>Listo para revelar</h1>
|
||||||
<div class="info-text" id="config-summary"></div>
|
<div class="info-text" id="config-summary"></div>
|
||||||
<p class="info-text">Cada jugador debe ver su rol en secreto. Desliza la cortina hacia arriba para revelar.</p>
|
|
||||||
<button onclick="showScreen('reveal-screen'); loadCurrentReveal();">Empezar revelación</button>
|
<button onclick="showScreen('reveal-screen'); loadCurrentReveal();">Empezar revelación</button>
|
||||||
<button class="ghost" onclick="showScreen('names-screen')">← Volver</button>
|
<button class="ghost" onclick="showScreen('names-screen')">← Volver</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,7 +235,7 @@
|
|||||||
<!-- Revelation screen -->
|
<!-- Revelation screen -->
|
||||||
<div id="reveal-screen" class="screen">
|
<div id="reveal-screen" class="screen">
|
||||||
<h1>Revelación</h1>
|
<h1>Revelación</h1>
|
||||||
<p class="info-text">Turno de: <strong><span id="current-player-name">Jugador 1</span></strong><br><small>Los demás, no miréis. Mantén levantada la cortina para ver tu rol.</small></p>
|
<p class="info-text">Turno de: <strong><span id="current-player-name">Jugador 1</span></strong></p>
|
||||||
<div class="curtain" id="curtain">
|
<div class="curtain" id="curtain">
|
||||||
<div class="curtain-cover" id="curtain-cover">
|
<div class="curtain-cover" id="curtain-cover">
|
||||||
<div class="curtain-icon">▲</div>
|
<div class="curtain-icon">▲</div>
|
||||||
@@ -178,6 +274,16 @@
|
|||||||
<button id="confirm-vote-btn" disabled onclick="confirmCurrentVote()">Confirmar voto</button>
|
<button id="confirm-vote-btn" disabled onclick="confirmCurrentVote()">Confirmar voto</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tie screen -->
|
||||||
|
<div id="tie-screen" class="screen">
|
||||||
|
<h1>EMPATE</h1>
|
||||||
|
<div class="tie-info">
|
||||||
|
<p class="info-text"><strong id="tie-eliminated-label">JUGADOR/ES ELIMINADOS:</strong></p>
|
||||||
|
<p class="eliminated-players" id="tie-eliminated-players"></p>
|
||||||
|
</div>
|
||||||
|
<button onclick="proceedToTiebreaker()">IR A DESEMPATE</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Results screen -->
|
<!-- Results screen -->
|
||||||
<div id="results-screen" class="screen">
|
<div id="results-screen" class="screen">
|
||||||
<h1>Resultados</h1>
|
<h1>Resultados</h1>
|
||||||
@@ -186,7 +292,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="script.js"></script>
|
<script src="script.0bc8c210.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
BIN
www/logo.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
21
www/manifest.webmanifest
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "Juego del Impostor",
|
||||||
|
"short_name": "Impostor",
|
||||||
|
"description": "Un juego de deducción social para descubrir quién es el impostor",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#0a0a0a",
|
||||||
|
"theme_color": "#ff4444",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "logo.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": ["games", "entertainment"],
|
||||||
|
"lang": "es",
|
||||||
|
"dir": "ltr"
|
||||||
|
}
|
||||||
18
www/robots.txt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Robots.txt for Juego del Impostor
|
||||||
|
# https://impostor.dariosevilla.es
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
# Sitemap location
|
||||||
|
Sitemap: https://impostor.dariosevilla.es/sitemap.xml
|
||||||
|
|
||||||
|
# Allow all crawlers to access the main content
|
||||||
|
User-agent: Googlebot
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
User-agent: Bingbot
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
# Crawl-delay for polite crawling (optional)
|
||||||
|
Crawl-delay: 1
|
||||||
1641
www/script.0bc8c210.js
Normal file
@@ -1,3 +1,4 @@
|
|||||||
|
// Game configuration constants
|
||||||
const STORAGE_KEY = 'impostorGameStateV2';
|
const STORAGE_KEY = 'impostorGameStateV2';
|
||||||
const MAX_PLAYERS = 10;
|
const MAX_PLAYERS = 10;
|
||||||
const MIN_PLAYERS = 3;
|
const MIN_PLAYERS = 3;
|
||||||
@@ -6,6 +7,22 @@ const POOLS_MANIFEST_URL = 'word-pools/manifest.json';
|
|||||||
const THEME_STORAGE_KEY = 'impostorGameTheme';
|
const THEME_STORAGE_KEY = 'impostorGameTheme';
|
||||||
const LANGUAGE_STORAGE_KEY = 'impostorGameLanguage';
|
const LANGUAGE_STORAGE_KEY = 'impostorGameLanguage';
|
||||||
const SCREEN_LOCK_STORAGE_KEY = 'impostorGameScreenLock';
|
const SCREEN_LOCK_STORAGE_KEY = 'impostorGameScreenLock';
|
||||||
|
const APP_VERSION = 1; // Simple incremental version number.
|
||||||
|
const LATEST_VERSION_URL = 'https://git.dariosevilla.es/api/v1/repos/dasemu/web-imposter-game/releases/latest';
|
||||||
|
const APK_DOWNLOAD_URL = 'https://git.dariosevilla.es/dasemu/web-imposter-game/releases/download/latest/impostor-game.apk';
|
||||||
|
|
||||||
|
// Detect if running as a native app (Capacitor/Cordova)
|
||||||
|
let isNativeApp = false;
|
||||||
|
(function detectNativeApp() {
|
||||||
|
const isCapacitor = window.Capacitor !== undefined;
|
||||||
|
const isCordova = window.cordova !== undefined;
|
||||||
|
isNativeApp = isCapacitor || isCordova;
|
||||||
|
|
||||||
|
if (isNativeApp) {
|
||||||
|
document.documentElement.classList.add('native-app');
|
||||||
|
checkAppVersion();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// ---------- Internationalization system ----------
|
// ---------- Internationalization system ----------
|
||||||
const TRANSLATIONS = {
|
const TRANSLATIONS = {
|
||||||
@@ -77,7 +94,23 @@ const TRANSLATIONS = {
|
|||||||
everydayObjects: 'Objetos Cotidianos',
|
everydayObjects: 'Objetos Cotidianos',
|
||||||
exitGame: 'Salir de la partida',
|
exitGame: 'Salir de la partida',
|
||||||
poolsSelection: 'Selección de Pools',
|
poolsSelection: 'Selección de Pools',
|
||||||
poolsSelectionText: 'Toca para seleccionar las categorías de palabras que quieres usar en la partida.'
|
poolsSelectionText: 'Toca para seleccionar las categorías de palabras que quieres usar en la partida.',
|
||||||
|
votingMode: 'Modo de votación',
|
||||||
|
individualVoting: 'Individual',
|
||||||
|
individualVotingDesc: 'Cada jugador vota en secreto',
|
||||||
|
raisedHandVoting: 'Mano alzada',
|
||||||
|
raisedHandVotingDesc: 'Votación grupal única',
|
||||||
|
groupVotingTitle: 'Votación a mano alzada',
|
||||||
|
groupVotingInstruction: 'Decidid en grupo a quién eliminar. Seleccionad',
|
||||||
|
groupVotingSuspects: 'sospechoso(s)',
|
||||||
|
tie: 'EMPATE',
|
||||||
|
eliminatedPlayers: 'JUGADOR/ES ELIMINADOS:',
|
||||||
|
goToTiebreaker: 'IR A DESEMPATE',
|
||||||
|
downloadApp: 'Descargar App Android',
|
||||||
|
updateAvailable: 'Actualización disponible',
|
||||||
|
updateMessage: 'Hay una nueva versión de la app disponible. ¿Quieres actualizar ahora?',
|
||||||
|
update: 'Actualizar',
|
||||||
|
later: 'Más tarde'
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
gameTitle: 'The Impostor Game',
|
gameTitle: 'The Impostor Game',
|
||||||
@@ -147,7 +180,23 @@ const TRANSLATIONS = {
|
|||||||
everydayObjects: 'Everyday Objects',
|
everydayObjects: 'Everyday Objects',
|
||||||
exitGame: 'Exit Game',
|
exitGame: 'Exit Game',
|
||||||
poolsSelection: 'Pool Selection',
|
poolsSelection: 'Pool Selection',
|
||||||
poolsSelectionText: 'Tap to select the word categories you want to use in the game.'
|
poolsSelectionText: 'Tap to select the word categories you want to use in the game.',
|
||||||
|
votingMode: 'Voting mode',
|
||||||
|
individualVoting: 'Individual',
|
||||||
|
individualVotingDesc: 'Each player votes secretly',
|
||||||
|
raisedHandVoting: 'Raised hand',
|
||||||
|
raisedHandVotingDesc: 'Single group vote',
|
||||||
|
groupVotingTitle: 'Raised hand voting',
|
||||||
|
groupVotingInstruction: 'Decide as a group who to eliminate. Select',
|
||||||
|
groupVotingSuspects: 'suspect(s)',
|
||||||
|
tie: 'TIE',
|
||||||
|
eliminatedPlayers: 'ELIMINATED PLAYER(S):',
|
||||||
|
goToTiebreaker: 'GO TO TIEBREAKER',
|
||||||
|
downloadApp: 'Download Android App',
|
||||||
|
updateAvailable: 'Update available',
|
||||||
|
updateMessage: 'A new version of the app is available. Do you want to update now?',
|
||||||
|
update: 'Update',
|
||||||
|
later: 'Later'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -158,7 +207,21 @@ function getBrowserLanguage() {
|
|||||||
return lang.startsWith('es') ? 'es' : 'en';
|
return lang.startsWith('es') ? 'es' : 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUrlLanguage() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const lang = urlParams.get('lang');
|
||||||
|
if (lang === 'en' || lang === 'es') {
|
||||||
|
return lang;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function loadLanguage() {
|
function loadLanguage() {
|
||||||
|
// Priority: URL param > localStorage > browser language
|
||||||
|
const urlLang = getUrlLanguage();
|
||||||
|
if (urlLang) {
|
||||||
|
return urlLang;
|
||||||
|
}
|
||||||
const saved = localStorage.getItem(LANGUAGE_STORAGE_KEY);
|
const saved = localStorage.getItem(LANGUAGE_STORAGE_KEY);
|
||||||
return saved || getBrowserLanguage();
|
return saved || getBrowserLanguage();
|
||||||
}
|
}
|
||||||
@@ -193,6 +256,9 @@ async function updateUI() {
|
|||||||
// Update all static text elements
|
// Update all static text elements
|
||||||
updateStaticTexts();
|
updateStaticTexts();
|
||||||
|
|
||||||
|
// Update voting mode buttons
|
||||||
|
updateVotingModeButtons();
|
||||||
|
|
||||||
// Reload pools for the new language (wait for it to complete)
|
// Reload pools for the new language (wait for it to complete)
|
||||||
await loadPoolsList();
|
await loadPoolsList();
|
||||||
|
|
||||||
@@ -205,6 +271,8 @@ async function updateUI() {
|
|||||||
renderVoting();
|
renderVoting();
|
||||||
} else if (state.phase === 'results') {
|
} else if (state.phase === 'results') {
|
||||||
showResults();
|
showResults();
|
||||||
|
} else if (state.phase === 'tie') {
|
||||||
|
showTieScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,9 +341,6 @@ function updateStaticTexts() {
|
|||||||
const poolsTitle = document.querySelector('#pools-screen h1');
|
const poolsTitle = document.querySelector('#pools-screen h1');
|
||||||
if (poolsTitle) poolsTitle.textContent = t('poolsSelection');
|
if (poolsTitle) poolsTitle.textContent = t('poolsSelection');
|
||||||
|
|
||||||
const poolsText = document.querySelector('#pools-screen .info-text');
|
|
||||||
if (poolsText) poolsText.textContent = t('poolsSelectionText');
|
|
||||||
|
|
||||||
// Names screen
|
// Names screen
|
||||||
const namesTitle = document.querySelector('#names-screen h1');
|
const namesTitle = document.querySelector('#names-screen h1');
|
||||||
if (namesTitle) namesTitle.textContent = t('playerNames');
|
if (namesTitle) namesTitle.textContent = t('playerNames');
|
||||||
@@ -284,9 +349,6 @@ function updateStaticTexts() {
|
|||||||
const preRevealTitle = document.querySelector('#pre-reveal-screen h1');
|
const preRevealTitle = document.querySelector('#pre-reveal-screen h1');
|
||||||
if (preRevealTitle) preRevealTitle.textContent = t('readyToReveal');
|
if (preRevealTitle) preRevealTitle.textContent = t('readyToReveal');
|
||||||
|
|
||||||
const preRevealText = document.querySelector('#pre-reveal-screen .info-text:not(#config-summary)');
|
|
||||||
if (preRevealText) preRevealText.textContent = t('eachPlayerSecret');
|
|
||||||
|
|
||||||
// Reveal screen
|
// Reveal screen
|
||||||
const revealTitle = document.querySelector('#reveal-screen h1');
|
const revealTitle = document.querySelector('#reveal-screen h1');
|
||||||
if (revealTitle) revealTitle.textContent = t('revelation');
|
if (revealTitle) revealTitle.textContent = t('revelation');
|
||||||
@@ -309,6 +371,10 @@ function updateStaticTexts() {
|
|||||||
const votingTitle = document.querySelector('#voting-screen h1');
|
const votingTitle = document.querySelector('#voting-screen h1');
|
||||||
if (votingTitle) votingTitle.textContent = t('secretVoting');
|
if (votingTitle) votingTitle.textContent = t('secretVoting');
|
||||||
|
|
||||||
|
// Tie screen
|
||||||
|
const tieTitle = document.querySelector('#tie-screen h1');
|
||||||
|
if (tieTitle) tieTitle.textContent = t('tie');
|
||||||
|
|
||||||
// Results screen
|
// Results screen
|
||||||
const resultsTitle = document.querySelector('#results-screen h1');
|
const resultsTitle = document.querySelector('#results-screen h1');
|
||||||
if (resultsTitle) resultsTitle.textContent = t('results');
|
if (resultsTitle) resultsTitle.textContent = t('results');
|
||||||
@@ -333,11 +399,18 @@ function updateStaticTexts() {
|
|||||||
else if (btn.getAttribute('onclick') === 'skipToVoting()') btn.textContent = t('goToVoting') + ' →';
|
else if (btn.getAttribute('onclick') === 'skipToVoting()') btn.textContent = t('goToVoting') + ' →';
|
||||||
else if (btn.id === 'confirm-vote-btn') btn.textContent = t('confirmVote');
|
else if (btn.id === 'confirm-vote-btn') btn.textContent = t('confirmVote');
|
||||||
else if (btn.getAttribute('onclick') === 'newMatch()') btn.textContent = t('newMatch');
|
else if (btn.getAttribute('onclick') === 'newMatch()') btn.textContent = t('newMatch');
|
||||||
|
else if (btn.getAttribute('onclick') === 'proceedToTiebreaker()') btn.textContent = t('goToTiebreaker');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Exit game button
|
// Exit game button
|
||||||
const exitText = document.querySelector('.exit-text');
|
const exitText = document.querySelector('.exit-text');
|
||||||
if (exitText) exitText.textContent = t('exitGame');
|
if (exitText) exitText.textContent = t('exitGame');
|
||||||
|
|
||||||
|
// Download app button
|
||||||
|
const downloadBtn = document.getElementById('download-app-btn');
|
||||||
|
if (downloadBtn && !isNativeApp) {
|
||||||
|
downloadBtn.textContent = `📱 ${t('downloadApp')}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Embedded pools with impostor words [civilian_word, impostor_word]
|
// Embedded pools with impostor words [civilian_word, impostor_word]
|
||||||
@@ -377,7 +450,8 @@ let state = {
|
|||||||
selectedPools: [], // Now it's an array for multiple pools, will be populated based on language
|
selectedPools: [], // Now it's an array for multiple pools, will be populated based on language
|
||||||
votingPool: null,
|
votingPool: null,
|
||||||
isTiebreak: false,
|
isTiebreak: false,
|
||||||
tiebreakCandidates: []
|
tiebreakCandidates: [],
|
||||||
|
votingMode: 'individual' // 'individual' or 'raisedHand'
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveState = () => localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
const saveState = () => localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||||
@@ -523,6 +597,33 @@ async function pickWords() {
|
|||||||
return { civilian: wordPair[0], impostor: wordPair[1] };
|
return { civilian: wordPair[0], impostor: wordPair[1] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Voting Mode ----------
|
||||||
|
function setVotingMode(mode) {
|
||||||
|
state.votingMode = mode;
|
||||||
|
saveState();
|
||||||
|
updateVotingModeButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVotingModeButtons() {
|
||||||
|
const individualBtn = document.getElementById('voting-mode-individual');
|
||||||
|
const raisedHandBtn = document.getElementById('voting-mode-raisedHand');
|
||||||
|
|
||||||
|
if (individualBtn && raisedHandBtn) {
|
||||||
|
individualBtn.classList.toggle('selected', state.votingMode === 'individual');
|
||||||
|
raisedHandBtn.classList.toggle('selected', state.votingMode === 'raisedHand');
|
||||||
|
|
||||||
|
// Update text based on language
|
||||||
|
individualBtn.querySelector('.voting-mode-name').textContent = t('individualVoting');
|
||||||
|
raisedHandBtn.querySelector('.voting-mode-name').textContent = t('raisedHandVoting');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update label
|
||||||
|
const label = document.getElementById('voting-mode-label');
|
||||||
|
if (label) {
|
||||||
|
label.textContent = t('votingMode') + ':';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderPoolButtons() {
|
function renderPoolButtons() {
|
||||||
const container = document.getElementById('pool-buttons');
|
const container = document.getElementById('pool-buttons');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -694,7 +795,7 @@ function loadCurrentReveal() {
|
|||||||
// Update curtain text
|
// Update curtain text
|
||||||
const revealText = document.querySelector('#reveal-screen .info-text');
|
const revealText = document.querySelector('#reveal-screen .info-text');
|
||||||
if (revealText) {
|
if (revealText) {
|
||||||
revealText.innerHTML = `${t('turnOf')}: <strong><span id="current-player-name">${name}</span></strong><br><small>${t('othersLookAway')}</small>`;
|
revealText.innerHTML = `${t('turnOf')}: <strong><span id="current-player-name">${name}</span></strong>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const curtainText = document.querySelector('.curtain-cover div:last-child');
|
const curtainText = document.querySelector('.curtain-cover div:last-child');
|
||||||
@@ -1036,6 +1137,26 @@ function startTiebreakDeliberation(candidates) {
|
|||||||
// ---------- Secret voting ----------
|
// ---------- Secret voting ----------
|
||||||
function renderVoting() {
|
function renderVoting() {
|
||||||
const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i);
|
const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i);
|
||||||
|
|
||||||
|
// Check if we're in raised hand mode
|
||||||
|
if (state.votingMode === 'raisedHand') {
|
||||||
|
renderRaisedHandVoting(pool);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Individual voting mode
|
||||||
|
const eliminated = state.executed || [];
|
||||||
|
|
||||||
|
// Skip eliminated players in tiebreaker
|
||||||
|
while (state.isTiebreak && eliminated.includes(state.votingPlayer) && state.votingPlayer < state.numPlayers) {
|
||||||
|
state.votingPlayer++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.votingPlayer >= state.numPlayers) {
|
||||||
|
handleVoteOutcome();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const voter = state.playerNames[state.votingPlayer];
|
const voter = state.playerNames[state.votingPlayer];
|
||||||
document.getElementById('voter-name').textContent = voter;
|
document.getElementById('voter-name').textContent = voter;
|
||||||
document.getElementById('votes-needed').textContent = state.numImpostors;
|
document.getElementById('votes-needed').textContent = state.numImpostors;
|
||||||
@@ -1046,6 +1167,12 @@ function renderVoting() {
|
|||||||
votingInfo.innerHTML = `${t('passMobileTo')} <strong id="voter-name">${voter}</strong>. ${t('chooseSuspects')} <span id="votes-needed">${state.numImpostors}</span> ${t('suspect')}.`;
|
votingInfo.innerHTML = `${t('passMobileTo')} <strong id="voter-name">${voter}</strong>. ${t('chooseSuspects')} <span id="votes-needed">${state.numImpostors}</span> ${t('suspect')}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update title
|
||||||
|
const votingTitle = document.querySelector('#voting-screen h1');
|
||||||
|
if (votingTitle) {
|
||||||
|
votingTitle.textContent = t('secretVoting');
|
||||||
|
}
|
||||||
|
|
||||||
state.selections = state.selections || [];
|
state.selections = state.selections || [];
|
||||||
const list = document.getElementById('vote-list'); list.innerHTML = '';
|
const list = document.getElementById('vote-list'); list.innerHTML = '';
|
||||||
pool.forEach(i => {
|
pool.forEach(i => {
|
||||||
@@ -1072,6 +1199,50 @@ function renderVoting() {
|
|||||||
updateConfirmButton();
|
updateConfirmButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderRaisedHandVoting(pool) {
|
||||||
|
// Update title
|
||||||
|
const votingTitle = document.querySelector('#voting-screen h1');
|
||||||
|
if (votingTitle) {
|
||||||
|
votingTitle.textContent = t('groupVotingTitle');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update voting instruction text
|
||||||
|
const votingInfo = document.querySelector('#voting-screen .info-text');
|
||||||
|
if (votingInfo) {
|
||||||
|
votingInfo.innerHTML = `${t('groupVotingInstruction')} <span id="votes-needed">${state.numImpostors}</span> ${t('groupVotingSuspects')}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.selections = state.selections || [];
|
||||||
|
const list = document.getElementById('vote-list');
|
||||||
|
list.innerHTML = '';
|
||||||
|
|
||||||
|
pool.forEach(i => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'player-item';
|
||||||
|
item.textContent = state.playerNames[i];
|
||||||
|
|
||||||
|
if (state.selections.includes(i)) {
|
||||||
|
item.classList.add('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
item.onclick = () => toggleRaisedHandSelection(i);
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateConfirmButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRaisedHandSelection(idx) {
|
||||||
|
if (state.selections.includes(idx)) {
|
||||||
|
state.selections = state.selections.filter(x => x !== idx);
|
||||||
|
} else {
|
||||||
|
if (state.selections.length >= state.numImpostors) return;
|
||||||
|
state.selections.push(idx);
|
||||||
|
}
|
||||||
|
saveState();
|
||||||
|
renderVoting();
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSelection(idx, el) {
|
function toggleSelection(idx, el) {
|
||||||
if (idx === state.votingPlayer) return;
|
if (idx === state.votingPlayer) return;
|
||||||
if (state.selections.includes(idx)) state.selections = state.selections.filter(x => x !== idx);
|
if (state.selections.includes(idx)) state.selections = state.selections.filter(x => x !== idx);
|
||||||
@@ -1089,11 +1260,30 @@ function updateConfirmButton() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function confirmCurrentVote() {
|
function confirmCurrentVote() {
|
||||||
|
// Raised hand mode: direct execution
|
||||||
|
if (state.votingMode === 'raisedHand') {
|
||||||
|
state.executed = [...state.selections];
|
||||||
|
showResults();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Individual mode: count votes
|
||||||
state.selections.forEach(t => { state.votes[t] = (state.votes[t] || 0) + 1; });
|
state.selections.forEach(t => { state.votes[t] = (state.votes[t] || 0) + 1; });
|
||||||
state.votingPlayer++;
|
state.votingPlayer++;
|
||||||
state.selections = [];
|
state.selections = [];
|
||||||
saveState();
|
saveState();
|
||||||
if (state.votingPlayer >= state.numPlayers) { handleVoteOutcome(); return; }
|
|
||||||
|
const eliminated = state.executed || [];
|
||||||
|
|
||||||
|
// Skip eliminated players in tiebreaker
|
||||||
|
while (state.isTiebreak && eliminated.includes(state.votingPlayer) && state.votingPlayer < state.numPlayers) {
|
||||||
|
state.votingPlayer++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.votingPlayer >= state.numPlayers) {
|
||||||
|
handleVoteOutcome();
|
||||||
|
return;
|
||||||
|
}
|
||||||
renderVoting();
|
renderVoting();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1122,7 +1312,10 @@ function handleVoteOutcome() {
|
|||||||
showResults(true);
|
showResults(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
startTiebreakDeliberation(group);
|
// Show tie screen with eliminated players
|
||||||
|
state.executed = executed;
|
||||||
|
state.tiebreakCandidates = group;
|
||||||
|
showTieScreen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1159,6 +1352,28 @@ function showResults(isTiebreak = false) {
|
|||||||
showScreen('results-screen');
|
showScreen('results-screen');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Tie screen ----------
|
||||||
|
function showTieScreen() {
|
||||||
|
state.phase = 'tie';
|
||||||
|
saveState();
|
||||||
|
|
||||||
|
const eliminated = state.executed || [];
|
||||||
|
const tieScreen = document.getElementById('tie-screen');
|
||||||
|
const eliminatedLabel = document.getElementById('tie-eliminated-label');
|
||||||
|
const eliminatedPlayers = document.getElementById('tie-eliminated-players');
|
||||||
|
|
||||||
|
eliminatedLabel.textContent = t('eliminatedPlayers');
|
||||||
|
eliminatedPlayers.textContent = eliminated.length > 0
|
||||||
|
? eliminated.map(i => state.playerNames[i]).join(', ')
|
||||||
|
: t('nobody');
|
||||||
|
|
||||||
|
showScreen('tie-screen');
|
||||||
|
}
|
||||||
|
|
||||||
|
function proceedToTiebreaker() {
|
||||||
|
startTiebreakDeliberation(state.tiebreakCandidates);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Utilities ----------
|
// ---------- Utilities ----------
|
||||||
function showScreen(id) {
|
function showScreen(id) {
|
||||||
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
||||||
@@ -1270,6 +1485,65 @@ function toggleScreenLock() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Event listener for theme and language buttons
|
// Event listener for theme and language buttons
|
||||||
|
// ---------- App version checking ----------
|
||||||
|
async function checkAppVersion() {
|
||||||
|
if (!isNativeApp) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(LATEST_VERSION_URL);
|
||||||
|
if (!response.ok) return;
|
||||||
|
|
||||||
|
const releaseData = await response.json();
|
||||||
|
const latestVersion = parseInt(releaseData.name) || 0;
|
||||||
|
|
||||||
|
if (latestVersion > APP_VERSION) {
|
||||||
|
showUpdateNotification();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking app version:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showUpdateNotification() {
|
||||||
|
const message = t('updateMessage');
|
||||||
|
const updateBtn = t('update');
|
||||||
|
const laterBtn = t('later');
|
||||||
|
|
||||||
|
// Create custom notification overlay
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'update-notification-overlay';
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="update-notification">
|
||||||
|
<h2>${t('updateAvailable')}</h2>
|
||||||
|
<p>${message}</p>
|
||||||
|
<div class="update-buttons">
|
||||||
|
<button class="btn-update" onclick="downloadUpdate()">${updateBtn}</button>
|
||||||
|
<button class="btn-later" onclick="dismissUpdate()">${laterBtn}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadUpdate() {
|
||||||
|
window.location.href = APK_DOWNLOAD_URL;
|
||||||
|
dismissUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissUpdate() {
|
||||||
|
const overlay = document.querySelector('.update-notification-overlay');
|
||||||
|
if (overlay) overlay.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show download button only on web (not native app)
|
||||||
|
function initDownloadButton() {
|
||||||
|
const downloadBtn = document.getElementById('download-app-btn');
|
||||||
|
if (downloadBtn && !isNativeApp) {
|
||||||
|
downloadBtn.style.display = 'inline-block';
|
||||||
|
downloadBtn.textContent = `📱 ${t('downloadApp')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
if (themeToggle) {
|
if (themeToggle) {
|
||||||
@@ -1291,6 +1565,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
currentLanguage = loadLanguage();
|
currentLanguage = loadLanguage();
|
||||||
setLanguage(currentLanguage);
|
setLanguage(currentLanguage);
|
||||||
|
|
||||||
|
// Initialize download button
|
||||||
|
initDownloadButton();
|
||||||
|
|
||||||
// Detect system theme changes
|
// Detect system theme changes
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||||
// Only apply automatically if user hasn't manually selected a theme
|
// Only apply automatically if user hasn't manually selected a theme
|
||||||
@@ -1300,6 +1577,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- Service Worker Registration ----------
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('/sw.js')
|
||||||
|
.then((registration) => {
|
||||||
|
console.log('[App] Service Worker registered:', registration.scope);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('[App] Service Worker registration failed:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- State rehydration ----------
|
// ---------- State rehydration ----------
|
||||||
(function init() {
|
(function init() {
|
||||||
const restored = loadState();
|
const restored = loadState();
|
||||||
@@ -1321,6 +1611,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
document.getElementById('deliberation-time').value = defaultDTime;
|
document.getElementById('deliberation-time').value = defaultDTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize voting mode buttons
|
||||||
|
if (!state.votingMode) state.votingMode = 'individual';
|
||||||
|
updateVotingModeButtons();
|
||||||
|
|
||||||
// Determine initial screen
|
// Determine initial screen
|
||||||
if (!restored || state.phase === 'setup' || state.phase === 'welcome') {
|
if (!restored || state.phase === 'setup' || state.phase === 'welcome') {
|
||||||
// If no saved state or we're in setup/welcome, show welcome
|
// If no saved state or we're in setup/welcome, show welcome
|
||||||
13
www/sitemap.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||||
|
xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||||
|
<url>
|
||||||
|
<loc>https://impostor.dariosevilla.es/</loc>
|
||||||
|
<lastmod>2026-01-17</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
<xhtml:link rel="alternate" hreflang="es" href="https://impostor.dariosevilla.es/"/>
|
||||||
|
<xhtml:link rel="alternate" hreflang="en" href="https://impostor.dariosevilla.es/?lang=en"/>
|
||||||
|
<xhtml:link rel="alternate" hreflang="x-default" href="https://impostor.dariosevilla.es/"/>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
2125
www/styles.0590c814.css
Normal file
@@ -102,7 +102,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 70px 16px 16px;
|
padding: calc(70px + env(safe-area-inset-top, 24px)) 16px calc(16px + env(safe-area-inset-bottom, 0px));
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -111,6 +111,27 @@ body {
|
|||||||
transition: background 0.5s ease, color 0.3s ease;
|
transition: background 0.5s ease, color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Native app (Capacitor/Cordova) status bar adjustment */
|
||||||
|
.native-app body {
|
||||||
|
padding-top: calc(70px + 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-app .theme-toggle {
|
||||||
|
top: calc(20px + 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-app .language-toggle {
|
||||||
|
top: calc(86px + 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-app .exit-game {
|
||||||
|
top: calc(20px + 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-app .screen-lock-toggle {
|
||||||
|
top: calc(152px + 32px);
|
||||||
|
}
|
||||||
|
|
||||||
/* Film grain texture overlay */
|
/* Film grain texture overlay */
|
||||||
body::before {
|
body::before {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -517,6 +538,146 @@ button:disabled {
|
|||||||
box-shadow: var(--shadow-harsh), 0 0 20px rgba(46, 78, 122, 0.3);
|
box-shadow: var(--shadow-harsh), 0 0 20px rgba(46, 78, 122, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-download {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 14px 22px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: linear-gradient(135deg, var(--accent-success) 0%, #1e7a3e 100%);
|
||||||
|
border: 3px solid var(--accent-success);
|
||||||
|
border-radius: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
box-shadow: var(--shadow-harsh), 0 0 20px rgba(46, 204, 113, 0.3);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-download:hover {
|
||||||
|
transform: translate(-2px, -2px);
|
||||||
|
box-shadow: 6px 6px 0px var(--bg-secondary), 0 0 30px rgba(46, 204, 113, 0.5);
|
||||||
|
filter: brightness(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-download:active {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
box-shadow: 3px 3px 0px var(--bg-secondary), 0 0 15px rgba(46, 204, 113, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide download button in native app */
|
||||||
|
.native-app .btn-download {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
UPDATE NOTIFICATION
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
||||||
|
|
||||||
|
.update-notification-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10000;
|
||||||
|
animation: overlayFadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes overlayFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-notification {
|
||||||
|
background: var(--surface-card);
|
||||||
|
border: 3px solid var(--accent-warning);
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 30px;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 20px;
|
||||||
|
box-shadow: var(--shadow-harsh), 0 0 40px rgba(230, 167, 60, 0.5);
|
||||||
|
animation: notificationSlideIn 0.4s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes notificationSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px) scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-notification h2 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif;
|
||||||
|
font-size: 1.8em;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
color: var(--accent-warning);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-notification p {
|
||||||
|
margin: 0 0 25px 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.6;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-update,
|
||||||
|
.btn-later {
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: var(--surface-hover);
|
||||||
|
border: 3px solid var(--border-medium);
|
||||||
|
border-radius: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-update {
|
||||||
|
background: linear-gradient(135deg, var(--accent-success) 0%, #1e7a3e 100%);
|
||||||
|
border-color: var(--accent-success);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-update:hover {
|
||||||
|
transform: translate(-2px, -2px);
|
||||||
|
box-shadow: 4px 4px 0px var(--bg-secondary), 0 0 20px rgba(46, 204, 113, 0.5);
|
||||||
|
filter: brightness(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-later:hover {
|
||||||
|
transform: translate(-2px, -2px);
|
||||||
|
box-shadow: 4px 4px 0px var(--bg-secondary);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
WELCOME SCREEN
|
WELCOME SCREEN
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
||||||
@@ -719,7 +880,7 @@ button:disabled {
|
|||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
font-size: 0.85em;
|
font-size: 1em;
|
||||||
letter-spacing: 0.3px;
|
letter-spacing: 0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1086,7 +1247,7 @@ button:disabled {
|
|||||||
.info-text {
|
.info-text {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 14px 0;
|
margin: 14px 0;
|
||||||
font-size: 0.85em;
|
font-size: 1em;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
background: var(--surface-card);
|
background: var(--surface-card);
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
@@ -1122,14 +1283,14 @@ button:disabled {
|
|||||||
|
|
||||||
.player-item {
|
.player-item {
|
||||||
padding: 18px 14px;
|
padding: 18px 14px;
|
||||||
min-height: 80px; /* Altura fija para evitar cambios de tamaño con vote-count */
|
min-height: 80px;
|
||||||
background: var(--surface-card);
|
background: var(--surface-card);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
font-size: 0.85em;
|
font-size: 1em;
|
||||||
border: 3px solid var(--border-medium);
|
border: 3px solid var(--border-medium);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
@@ -1358,6 +1519,64 @@ button:disabled {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
TIE SCREEN
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
||||||
|
|
||||||
|
#tie-screen h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
color: var(--accent-warning);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
animation: tieFlash 1s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tieFlash {
|
||||||
|
0% {
|
||||||
|
text-shadow: 0 0 10px var(--accent-warning);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
text-shadow: 0 0 20px var(--accent-warning), 0 0 30px var(--accent-warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tie-info {
|
||||||
|
background: var(--surface-card);
|
||||||
|
border: 2px solid var(--border-medium);
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tie-info .info-text {
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eliminated-players {
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-danger);
|
||||||
|
margin-top: 10px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tie-screen button {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
padding: 15px 30px;
|
||||||
|
background: var(--accent-warning);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#tie-screen button:hover {
|
||||||
|
background: var(--accent-warning);
|
||||||
|
filter: brightness(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
POOL SELECTION
|
POOL SELECTION
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
||||||
@@ -1434,13 +1653,67 @@ button:disabled {
|
|||||||
box-shadow: 0 0 0 3px rgba(212, 165, 116, 0.3), 3px 3px 0px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 0 0 3px rgba(212, 165, 116, 0.3), 3px 3px 0px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
VOTING MODE SELECTOR
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
||||||
|
|
||||||
|
.voting-mode-selector {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voting-mode-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 14px 10px;
|
||||||
|
border: 2px solid var(--border-medium);
|
||||||
|
background: var(--surface-card);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.9em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.18s ease;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
clip-path: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voting-mode-btn:hover {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.15);
|
||||||
|
filter: brightness(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.voting-mode-btn.selected {
|
||||||
|
border-color: var(--text-primary);
|
||||||
|
background: var(--accent-warning);
|
||||||
|
color: var(--text-inverted);
|
||||||
|
box-shadow: 0 0 0 3px rgba(212, 165, 116, 0.3), 3px 3px 0px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.voting-mode-icon {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voting-mode-name {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
FIXED UI CONTROLS (Theme, Language, Exit)
|
FIXED UI CONTROLS (Theme, Language, Exit)
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
|
||||||
|
|
||||||
.theme-toggle {
|
.theme-toggle {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: calc(20px + env(safe-area-inset-top, 24px));
|
||||||
right: 20px;
|
right: 20px;
|
||||||
width: 56px;
|
width: 56px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
@@ -1481,7 +1754,7 @@ button:disabled {
|
|||||||
|
|
||||||
.language-toggle {
|
.language-toggle {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 86px;
|
top: calc(86px + env(safe-area-inset-top, 24px));
|
||||||
right: 20px;
|
right: 20px;
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: 56px;
|
min-width: 56px;
|
||||||
@@ -1531,7 +1804,7 @@ button:disabled {
|
|||||||
|
|
||||||
.exit-game {
|
.exit-game {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: calc(20px + env(safe-area-inset-top, 24px));
|
||||||
left: 20px;
|
left: 20px;
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
@@ -1580,7 +1853,7 @@ button:disabled {
|
|||||||
|
|
||||||
.screen-lock-toggle {
|
.screen-lock-toggle {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 152px;
|
top: calc(152px + env(safe-area-inset-top, 24px));
|
||||||
right: 20px;
|
right: 20px;
|
||||||
width: 56px;
|
width: 56px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
@@ -1636,11 +1909,11 @@ button:disabled {
|
|||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
body {
|
body {
|
||||||
padding: 60px 10px 10px 10px;
|
padding: 60px 10px 10px 10px;
|
||||||
font-size: 13px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 1.7em;
|
font-size: 2em;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1696,22 +1969,23 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timer {
|
.timer {
|
||||||
font-size: 2.5em;
|
font-size: 3em;
|
||||||
padding: 16px;
|
padding: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-title {
|
.welcome-title {
|
||||||
font-size: 1.8em;
|
font-size: 2.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.role {
|
.role {
|
||||||
font-size: 1.6em;
|
font-size: 2em;
|
||||||
padding: 10px 18px;
|
padding: 12px 20px;
|
||||||
|
letter-spacing: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.word {
|
.word {
|
||||||
font-size: 1.3em;
|
font-size: 1.8em;
|
||||||
padding: 12px 20px;
|
padding: 14px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
@@ -1733,34 +2007,38 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rule-section h3 {
|
.rule-section h3 {
|
||||||
font-size: 0.85em;
|
font-size: 1em;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-section p {
|
.rule-section p {
|
||||||
font-size: 0.8em;
|
font-size: 0.95em;
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-name-item {
|
.player-name-item {
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-name-item span {
|
.player-name-item span {
|
||||||
font-size: 0.75em;
|
font-size: 0.9em;
|
||||||
min-width: 70px;
|
min-width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.player-name-item input {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.player-item {
|
.player-item {
|
||||||
padding: 14px 10px;
|
padding: 16px 12px;
|
||||||
min-height: 72px; /* Altura fija también en móvil */
|
min-height: 80px;
|
||||||
font-size: 0.8em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pool-btn {
|
.pool-btn {
|
||||||
padding: 10px 8px;
|
padding: 12px 10px;
|
||||||
font-size: 0.75em;
|
font-size: 0.95em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pool-buttons-wrapper {
|
.pool-buttons-wrapper {
|
||||||
@@ -1775,15 +2053,33 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.info-text {
|
.info-text {
|
||||||
padding: 12px 14px;
|
padding: 14px 16px;
|
||||||
font-size: 0.8em;
|
font-size: 1em;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.curtain {
|
.curtain {
|
||||||
height: 240px;
|
height: 260px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.curtain-cover {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.curtain-icon {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Labels más grandes en móvil */
|
||||||
|
label {
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Botones más grandes */
|
||||||
|
button {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
120
www/sw.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
const CACHE_NAME = 'impostor-game-v1';
|
||||||
|
|
||||||
|
const STATIC_ASSETS = [
|
||||||
|
'/',
|
||||||
|
'/index.html',
|
||||||
|
'/styles.css',
|
||||||
|
'/script.js',
|
||||||
|
'/logo.png',
|
||||||
|
'/manifest.webmanifest',
|
||||||
|
'/word-pools/manifest.json',
|
||||||
|
'/word-pools/animales_naturaleza.txt',
|
||||||
|
'/word-pools/objetos_cotidianos.txt',
|
||||||
|
'/word-pools/lugares_mundo.txt',
|
||||||
|
'/word-pools/escuela_educacion.txt',
|
||||||
|
'/word-pools/tecnologia_internet.txt',
|
||||||
|
'/word-pools/vehiculos_transporte.txt',
|
||||||
|
'/word-pools/instrumentos_musicales.txt',
|
||||||
|
'/word-pools/videojuegos.txt',
|
||||||
|
'/word-pools/personajes_anime.txt',
|
||||||
|
'/word-pools/personajes_disney.txt',
|
||||||
|
'/word-pools/artistas_latinos.txt',
|
||||||
|
'/word-pools/marcas_lujo.txt',
|
||||||
|
'/word-pools/personajes_ficcion.txt',
|
||||||
|
'/word-pools/cuerpo_humano.txt',
|
||||||
|
'/word-pools/playa_verano.txt',
|
||||||
|
'/word-pools/amor_romance.txt',
|
||||||
|
'/word-pools/navidad_fiestas.txt',
|
||||||
|
'/word-pools/marcas_empresas.txt',
|
||||||
|
'/word-pools/profesiones_trabajos.txt',
|
||||||
|
'/word-pools/comida_bebidas.txt',
|
||||||
|
'/word-pools/deportes.txt',
|
||||||
|
'/word-pools/peliculas_series.txt'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Install event - cache static assets
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME)
|
||||||
|
.then((cache) => {
|
||||||
|
console.log('[SW] Caching static assets');
|
||||||
|
return cache.addAll(STATIC_ASSETS);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log('[SW] All assets cached');
|
||||||
|
return self.skipWaiting();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('[SW] Failed to cache assets:', error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activate event - clean up old caches
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys()
|
||||||
|
.then((cacheNames) => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames
|
||||||
|
.filter((name) => name !== CACHE_NAME)
|
||||||
|
.map((name) => {
|
||||||
|
console.log('[SW] Deleting old cache:', name);
|
||||||
|
return caches.delete(name);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log('[SW] Activated');
|
||||||
|
return self.clients.claim();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch event - serve from cache, fallback to network
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
const { request } = event;
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
// Skip cross-origin requests (analytics, fonts, etc.)
|
||||||
|
if (url.origin !== location.origin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(request)
|
||||||
|
.then((cachedResponse) => {
|
||||||
|
if (cachedResponse) {
|
||||||
|
// Return cached version
|
||||||
|
return cachedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not in cache, fetch from network
|
||||||
|
return fetch(request)
|
||||||
|
.then((networkResponse) => {
|
||||||
|
// Don't cache non-successful responses
|
||||||
|
if (!networkResponse || networkResponse.status !== 200) {
|
||||||
|
return networkResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone the response before caching
|
||||||
|
const responseToCache = networkResponse.clone();
|
||||||
|
|
||||||
|
caches.open(CACHE_NAME)
|
||||||
|
.then((cache) => {
|
||||||
|
cache.put(request, responseToCache);
|
||||||
|
});
|
||||||
|
|
||||||
|
return networkResponse;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('[SW] Fetch failed:', error);
|
||||||
|
// Return a fallback for HTML pages
|
||||||
|
if (request.headers.get('accept')?.includes('text/html')) {
|
||||||
|
return caches.match('/index.html');
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||