Compare commits
16 Commits
gbmfixi1fi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9eb90deb41 | |||
| 43300c625b | |||
| 8fb2e25e1a | |||
| fb57f49d45 | |||
| bfe3c6f4a9 | |||
| 8e3b2e8e72 | |||
| 2bc0aa1c65 | |||
| 68776e4238 | |||
| 4d9346219d | |||
| 34cdff241b | |||
| 120bddba62 | |||
| ff05ffbb5e | |||
| 04068b3e96 | |||
| df59ae8792 | |||
| 526e5a2f63 | |||
| 9145be78a3 |
@@ -33,7 +33,8 @@ jobs:
|
|||||||
build-essential \
|
build-essential \
|
||||||
curl \
|
curl \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
xdg-utils
|
xdg-utils \
|
||||||
|
gnupg2
|
||||||
|
|
||||||
- name: Add Windows Rust target
|
- name: Add Windows Rust target
|
||||||
run: rustup target add x86_64-pc-windows-gnu
|
run: rustup target add x86_64-pc-windows-gnu
|
||||||
@@ -51,17 +52,58 @@ jobs:
|
|||||||
cd src-tauri
|
cd src-tauri
|
||||||
cargo build --release --target x86_64-pc-windows-gnu
|
cargo build --release --target x86_64-pc-windows-gnu
|
||||||
|
|
||||||
|
- name: Make Linux ELF executable
|
||||||
|
run: chmod +x src-tauri/target/release/bytechat-desktop
|
||||||
|
|
||||||
|
# 🔑 Import your private GPG key
|
||||||
|
- name: Import GPG key
|
||||||
|
run: |
|
||||||
|
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||||
|
echo "$GPG_KEY_ID:6:" | gpg --import-ownertrust
|
||||||
|
env:
|
||||||
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
|
||||||
|
|
||||||
|
# 🐧 Sign Linux binaries (.deb, .rpm, ELF)
|
||||||
|
- name: Sign Linux artifacts
|
||||||
|
run: |
|
||||||
|
for f in src-tauri/target/release/bytechat-desktop \
|
||||||
|
src-tauri/target/release/bundle/deb/*.deb \
|
||||||
|
src-tauri/target/release/bundle/rpm/*.rpm; do
|
||||||
|
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
||||||
|
--pinentry-mode loopback --detach-sign -a "$f"
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
|
||||||
|
# 🪟 Sign Windows exe with GPG
|
||||||
|
- name: Sign Windows exe
|
||||||
|
run: |
|
||||||
|
exe=src-tauri/target/x86_64-pc-windows-gnu/release/bytechat-desktop.exe
|
||||||
|
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
||||||
|
--pinentry-mode loopback --detach-sign -a "$exe"
|
||||||
|
env:
|
||||||
|
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||||
|
|
||||||
|
# 📦 Upload Linux artifacts + signatures + public key
|
||||||
- name: Upload Linux packages
|
- name: Upload Linux packages
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: tauri-linux
|
name: tauri-linux
|
||||||
path: |
|
path: |
|
||||||
src-tauri/target/release/bytechat-desktop
|
src-tauri/target/release/bytechat-desktop
|
||||||
|
src-tauri/target/release/bytechat-desktop.asc
|
||||||
src-tauri/target/release/bundle/deb/*.deb
|
src-tauri/target/release/bundle/deb/*.deb
|
||||||
|
src-tauri/target/release/bundle/deb/*.asc
|
||||||
src-tauri/target/release/bundle/rpm/*.rpm
|
src-tauri/target/release/bundle/rpm/*.rpm
|
||||||
|
src-tauri/target/release/bundle/rpm/*.asc
|
||||||
|
key/bytechat-public.gpg
|
||||||
|
|
||||||
|
# 📦 Upload Windows exe + signature
|
||||||
- name: Upload Windows exe
|
- name: Upload Windows exe
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: tauri-windows-exe
|
name: tauri-windows-exe
|
||||||
path: src-tauri/target/x86_64-pc-windows-gnu/release/bytechat-desktop.exe
|
path: |
|
||||||
|
src-tauri/target/x86_64-pc-windows-gnu/release/bytechat-desktop.exe
|
||||||
|
src-tauri/target/x86_64-pc-windows-gnu/release/bytechat-desktop.exe.asc
|
||||||
|
|||||||
13
README.md
@@ -1 +1,12 @@
|
|||||||
# Tauri Desktop Application
|
# Tauri Desktop Application
|
||||||
|
|
||||||
|
This is the Desktop App for [ByteChat](https://rattatwinko.servecounterstrike.com/gitea/rattatwinko/bytechat) ,it supports Windows and Linux for now. And probably the future
|
||||||
|
|
||||||
|
## Recent Focus:
|
||||||
|
|
||||||
|
- [x] - GPG Key Signage
|
||||||
|
|
||||||
|
### Stack
|
||||||
|
|
||||||
|
Idk Rust javascript html and css. just look at the repo.
|
||||||
|
and tauri
|
||||||
51
key/bytechat-public.gpg
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mQINBGivVW4BEADUaMzyRszPHxiTrsELHgyEK/0+si4FmZmAaGvbkVN0jbrjTe4S
|
||||||
|
0hfx6B/I4Yp2qCBXeacrb9WtLHs3KEDkJW9KodrhgDTvNRCgBE9WT4XnACealsmr
|
||||||
|
RCH6iNBcRNPQ+pm4VXvX++m4ANfhn6aNI6y4dioBaVyrd4hLNFb2qVNfOO96vEP2
|
||||||
|
Z6tiPY1iOeqjAbaRIpL4ZGodS36wzsbISePhs2JKQqEQxASkHjak6EzJwAeL4e5f
|
||||||
|
eXZ9WYD9TlgwrrG5IBV11A39PjeMz5Hofdsnm0tp19UUxtRaPyuuvhoQ02rcuWG9
|
||||||
|
x5vQGFmp4RSUT5+wNnKfknJSrzIRCD8/20gCBboBHLXO9FSUx79hXAU9OGuHhIWF
|
||||||
|
CGtMYyv6dw0/N92Lbh/NK2kUQEsr4CI/02RcbUsarg6R8Zh0DNNzigCyBHWP//RZ
|
||||||
|
/7TiT7bqEeFw4YMr99jBL5WUraEt3B/VB4bXuAk19dkkFwb/ywEFQncvN9qDm6Bo
|
||||||
|
8IZfa6MN4Pj7Z0oa9Wa4DL96jbkf1qoyWS7glzmPUgzbbb4AOgPlGx7Rdg9/Qi+i
|
||||||
|
W3fs15R0dxxcFPB3vuYQ6F2/S+TO0wsyk7OuzaoFUxf0D6/BBTIXig3EgIy7CZTH
|
||||||
|
2Ci81zPo/SHlsHAe7MjM4ybYAsZ7yeU1TIwi+HdCYGrR1ShVSxW9E6XscwARAQAB
|
||||||
|
tB5yYXR0YXR3aW5rbyAoYnl0ZWNoYXQgbGljZW5zZSmJAk4EEwEKADgWIQQneX35
|
||||||
|
Qi+iEbi4z05cXj6JuWDymAUCaK9VbgIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIX
|
||||||
|
gAAKCRBcXj6JuWDymMB9EACz4XojdYl39VKIzYKQTN/AoYA1wdYypCXh2rG/toFW
|
||||||
|
1q0zjo8H6CGuCdY2tiywWXMBSrSfriLmC1+JB1upwfLLNL8bfb4bl3+1Yqr7Kaho
|
||||||
|
GhkEamFghL0iDQ4U1pLMGAq6Hv3XjvbRzHL3/f+rJzMLRNot1P0DqsYA9thIGeus
|
||||||
|
dln6qdhgmk0RiRFTklDk1mPONXgu1DGGudjN1Qet9jJ6ZnfgFTOVBZxATX9s3Kbs
|
||||||
|
PgLQ8n0aCao1ul1+qTTF6xKjsnO1G9LNR5kAg2oS5O7qCGbMQEhxrPGN91iNlbRk
|
||||||
|
2CjgSnlDnwVk5xGi59dtcD7De5wUOXQN6OqxAFnp9D9KsiTPGQRoLDYzG2WvdY77
|
||||||
|
Z9359s1N1adbUNgCAf4DscIjUtfjLp0Z2lKq8LGYDrfQRpRVbMokH41Gum+dC2MB
|
||||||
|
nEjwUIiqlKZSnp/MU1V8S3lGlvMMsF/TG1aCYVsAFRdQPX6Pu3JH8+z8mxjUvElH
|
||||||
|
61K5JOFrmkGNPUpNovoMre1PZH19YU9OS0qQO1bUBp/EvVkkE2umTKGL9MXcN4Y7
|
||||||
|
TF6Q0NpudVxFM6IktyPNj1wK/niLQlpV8glgc3st0B1UizZqtanyA4s0IAr1IMKk
|
||||||
|
uUrZcK3OX86kDVRQ2SKHHMmGGGQH9Gu6J6WLy2NHQCLBbinn0giyTnl43EEo0VpQ
|
||||||
|
R7kCDQRor1VuARAAuMiEjUgl/tsxNjHh5dxdZoAg14ICSIkQevPxo2HDC+3Q1Xbs
|
||||||
|
ZXExC8ipEdKHrL09uOEMLlbj0apka1ncsLWpazUi02UzAEFtvNMMCgpmJTuv0d3k
|
||||||
|
pKS9XQ5xrUJxDNffBrLTMtd3ypDVjYPygkba+5b17q7o1MPZypOqJKbR3oYzD+dd
|
||||||
|
MzpRNg0EY9lbO+m9gXRvl6h3CeuWy8VS5AaqvvjTJcVTEmhXujM95lVzl5WcPj0A
|
||||||
|
lvihKjZdC88SKds901KEJYohgT3ZvqL5B1C00PQWAzGEMtP8EewR6lJLPaI44wj0
|
||||||
|
DbM+YILIRoQgXwD5+38+SyAaJAa13CfI030fGiZpdQVxSZaod1E7sWdeQBzVW8qQ
|
||||||
|
qDWLkXoBnfBKktdW/4ps0MGOAwbP53vCwDkNVfTN3sTrg4B731VUokhuvQNvJfHV
|
||||||
|
f8meg5m09VcnjZ1YnkMKmV+M2Clrz3WpcnON91z+S3vYogPwwKQ5qrootNX0Jpmr
|
||||||
|
XlsczkY4UFpB27R8yI3XkOjTpoQZOGpUgZOSGAUycONBnjCiZOk4GLFqXuDa2vAy
|
||||||
|
iVT5JvzvCVcLvhmtXONwmg1m5in4/FjAdiHzQJQcpYK4+ZgQHQL/VhUpOX9U8ax7
|
||||||
|
jFJlgBIc2YpNi5SzuRNVgdS7yR+grYK42N/7RgpYqLrDS8mJVr1gxK655fUAEQEA
|
||||||
|
AYkCNgQYAQoAIBYhBCd5fflCL6IRuLjPTlxePom5YPKYBQJor1VuAhsMAAoJEFxe
|
||||||
|
Pom5YPKYEKoQAIdB321Y6dpVb1+uSfG54XACfNhzPSrep9GVmGw3bb8bMJCtZWci
|
||||||
|
U2HDKT8uE4fVlSQB15ceEI20nkTTnJZ7WvnGNvRSYa0+7oaNMCiUHSMKMztbNoMr
|
||||||
|
qmQgQSOvaK5FJ2fYeXx/U2oYewBHsZwGvgCPDdSuD7XxbrTRfW8g6cstbjuHIscP
|
||||||
|
yVjnUwp40A50r5LJn0w4tA1WxdkCdzaSfuUQao3RqcL8Q35tDFehnXNKSzHBL0u4
|
||||||
|
SB2h/wF3QODvUwAZh0NiUvTYf7XeWBnvrQX+6WcDevRRZobTNrg8cIDNM+4EQ+i/
|
||||||
|
yAMG6y4ydBZP9pA5rT75DywSMn/TuzyhoBZ/CKa8iZHDlbZzpRBj+TQMiebtxspd
|
||||||
|
eqd+ZNZEmX20DWT8APkodFn493eWYlfdRYlbmblgq0fQGbtzIKuJb0NFyJisKt3p
|
||||||
|
hVnEsGjvvvSSTvTlRthCYy6nz06Q21e560ftSnIhRkMMLf4WbIspzzEtlwyA/Pyb
|
||||||
|
qplmC7G3oOVoO4Ag0FPCiVYwao0V0wnJ6lcJthIWRs3ghZt37uy4B+gQCVs5epsB
|
||||||
|
+xK/bQ9CmKD6g2gHLfSS8MWLiwphxGlZ/Q+5NCZYiyzZoexNZ2zhEOz2spNOwC6D
|
||||||
|
nwEUSpac1duGl7Hj2xtbo6xQ/Lz71vKLVb6H656D2h/1BkHWOsYGBamG
|
||||||
|
=4tP+
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
29
src-tauri/Cargo.lock
generated
@@ -346,6 +346,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
|
"tauri-plugin-store",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3725,6 +3726,22 @@ dependencies = [
|
|||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-store"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d85dd80d60a76ee2c2fdce09e9ef30877b239c2a6bb76e6d7d03708aa5f13a19"
|
||||||
|
dependencies = [
|
||||||
|
"dunce",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.16",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
@@ -3944,9 +3961,21 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"slab",
|
"slab",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.106",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.16"
|
version = "0.7.16"
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ tauri = { version = "2", features = [] }
|
|||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
tauri-plugin-store = "2"
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Capability for the main window",
|
"description": "Capability for the main window",
|
||||||
"windows": ["main"],
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"opener:default"
|
"opener:default",
|
||||||
|
"store:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
12
src-tauri/gen/android/.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
insert_final_newline = false
|
||||||
19
src-tauri/gen/android/.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
.DS_Store
|
||||||
|
build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
|
key.properties
|
||||||
|
|
||||||
|
/.tauri
|
||||||
|
/tauri.settings.gradle
|
||||||
6
src-tauri/gen/android/app/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/src/main/java/com/rattatwinko/bytechat_desktop/generated
|
||||||
|
/src/main/jniLibs/**/*.so
|
||||||
|
/src/main/assets/tauri.conf.json
|
||||||
|
/tauri.build.gradle.kts
|
||||||
|
/proguard-tauri.pro
|
||||||
|
/tauri.properties
|
||||||
70
src-tauri/gen/android/app/build.gradle.kts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import java.util.Properties
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
id("rust")
|
||||||
|
}
|
||||||
|
|
||||||
|
val tauriProperties = Properties().apply {
|
||||||
|
val propFile = file("tauri.properties")
|
||||||
|
if (propFile.exists()) {
|
||||||
|
propFile.inputStream().use { load(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = 36
|
||||||
|
namespace = "com.rattatwinko.bytechat_desktop"
|
||||||
|
defaultConfig {
|
||||||
|
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||||
|
applicationId = "com.rattatwinko.bytechat_desktop"
|
||||||
|
minSdk = 24
|
||||||
|
targetSdk = 36
|
||||||
|
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||||
|
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
getByName("debug") {
|
||||||
|
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||||
|
isDebuggable = true
|
||||||
|
isJniDebuggable = true
|
||||||
|
isMinifyEnabled = false
|
||||||
|
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
|
||||||
|
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
|
||||||
|
jniLibs.keepDebugSymbols.add("*/x86/*.so")
|
||||||
|
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getByName("release") {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
proguardFiles(
|
||||||
|
*fileTree(".") { include("**/*.pro") }
|
||||||
|
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||||
|
.toList().toTypedArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rust {
|
||||||
|
rootDirRel = "../../../"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("androidx.webkit:webkit:1.14.0")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||||
|
implementation("androidx.activity:activity-ktx:1.10.1")
|
||||||
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "tauri.build.gradle.kts")
|
||||||
21
src-tauri/gen/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
|
||||||
38
src-tauri/gen/android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<!-- AndroidTV support -->
|
||||||
|
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/Theme.bytechat_desktop"
|
||||||
|
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||||
|
<activity
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:label="@string/main_activity_title"
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<!-- AndroidTV support -->
|
||||||
|
<category android:name="android.intent.category.LEANBACK_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" />
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.rattatwinko.bytechat_desktop
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowInsets
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
|
||||||
|
class MainActivity : TauriActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
enableEdgeToEdge()
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Apply padding to the root view to respect system bars
|
||||||
|
val rootView: View = findViewById(android.R.id.content)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
rootView.setOnApplyWindowInsetsListener { v, insets ->
|
||||||
|
val systemBars = insets.getInsets(WindowInsets.Type.systemBars())
|
||||||
|
v.setPadding(
|
||||||
|
systemBars.left,
|
||||||
|
systemBars.top,
|
||||||
|
systemBars.right,
|
||||||
|
systemBars.bottom
|
||||||
|
)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback for older versions (bottom padding only)
|
||||||
|
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
|
||||||
|
val navBarHeight = if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0
|
||||||
|
rootView.setPadding(0, 0, 0, navBarHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="85.84757"
|
||||||
|
android:endY="92.4963"
|
||||||
|
android:startX="42.9492"
|
||||||
|
android:startY="49.59793"
|
||||||
|
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="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Hello World!"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.bytechat_desktop" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
10
src-tauri/gen/android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">bytechat-desktop</string>
|
||||||
|
<string name="main_activity_title">bytechat-desktop</string>
|
||||||
|
</resources>
|
||||||
6
src-tauri/gen/android/app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.bytechat_desktop" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -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>
|
||||||
22
src-tauri/gen/android/build.gradle.kts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.11.0")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("clean").configure {
|
||||||
|
delete("build")
|
||||||
|
}
|
||||||
|
|
||||||
23
src-tauri/gen/android/buildSrc/build.gradle.kts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
plugins {
|
||||||
|
`kotlin-dsl`
|
||||||
|
}
|
||||||
|
|
||||||
|
gradlePlugin {
|
||||||
|
plugins {
|
||||||
|
create("pluginsForCoolKids") {
|
||||||
|
id = "rust"
|
||||||
|
implementationClass = "RustPlugin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(gradleApi())
|
||||||
|
implementation("com.android.tools.build:gradle:8.11.0")
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import java.io.File
|
||||||
|
import org.apache.tools.ant.taskdefs.condition.Os
|
||||||
|
import org.gradle.api.DefaultTask
|
||||||
|
import org.gradle.api.GradleException
|
||||||
|
import org.gradle.api.logging.LogLevel
|
||||||
|
import org.gradle.api.tasks.Input
|
||||||
|
import org.gradle.api.tasks.TaskAction
|
||||||
|
|
||||||
|
open class BuildTask : DefaultTask() {
|
||||||
|
@Input
|
||||||
|
var rootDirRel: String? = null
|
||||||
|
@Input
|
||||||
|
var target: String? = null
|
||||||
|
@Input
|
||||||
|
var release: Boolean? = null
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun assemble() {
|
||||||
|
val executable = """cargo""";
|
||||||
|
try {
|
||||||
|
runTauriCli(executable)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||||
|
runTauriCli("$executable.cmd")
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runTauriCli(executable: String) {
|
||||||
|
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
|
||||||
|
val target = target ?: throw GradleException("target cannot be null")
|
||||||
|
val release = release ?: throw GradleException("release cannot be null")
|
||||||
|
val args = listOf("tauri", "android", "android-studio-script");
|
||||||
|
|
||||||
|
project.exec {
|
||||||
|
workingDir(File(project.projectDir, rootDirRel))
|
||||||
|
executable(executable)
|
||||||
|
args(args)
|
||||||
|
if (project.logger.isEnabled(LogLevel.DEBUG)) {
|
||||||
|
args("-vv")
|
||||||
|
} else if (project.logger.isEnabled(LogLevel.INFO)) {
|
||||||
|
args("-v")
|
||||||
|
}
|
||||||
|
if (release) {
|
||||||
|
args("--release")
|
||||||
|
}
|
||||||
|
args(listOf("--target", target))
|
||||||
|
}.assertNormalExitValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import com.android.build.api.dsl.ApplicationExtension
|
||||||
|
import org.gradle.api.DefaultTask
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.configure
|
||||||
|
import org.gradle.kotlin.dsl.get
|
||||||
|
|
||||||
|
const val TASK_GROUP = "rust"
|
||||||
|
|
||||||
|
open class Config {
|
||||||
|
lateinit var rootDirRel: String
|
||||||
|
}
|
||||||
|
|
||||||
|
open class RustPlugin : Plugin<Project> {
|
||||||
|
private lateinit var config: Config
|
||||||
|
|
||||||
|
override fun apply(project: Project) = with(project) {
|
||||||
|
config = extensions.create("rust", Config::class.java)
|
||||||
|
|
||||||
|
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
|
||||||
|
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
|
||||||
|
|
||||||
|
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
|
||||||
|
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
|
||||||
|
|
||||||
|
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
|
||||||
|
|
||||||
|
extensions.configure<ApplicationExtension> {
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
flavorDimensions.add("abi")
|
||||||
|
productFlavors {
|
||||||
|
create("universal") {
|
||||||
|
dimension = "abi"
|
||||||
|
ndk {
|
||||||
|
abiFilters += abiList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultArchList.forEachIndexed { index, arch ->
|
||||||
|
create(arch) {
|
||||||
|
dimension = "abi"
|
||||||
|
ndk {
|
||||||
|
abiFilters.add(defaultAbiList[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
for (profile in listOf("debug", "release")) {
|
||||||
|
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
|
||||||
|
val buildTask = tasks.maybeCreate(
|
||||||
|
"rustBuildUniversal$profileCapitalized",
|
||||||
|
DefaultTask::class.java
|
||||||
|
).apply {
|
||||||
|
group = TASK_GROUP
|
||||||
|
description = "Build dynamic library in $profile mode for all targets"
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
|
||||||
|
|
||||||
|
for (targetPair in targetsList.withIndex()) {
|
||||||
|
val targetName = targetPair.value
|
||||||
|
val targetArch = archList[targetPair.index]
|
||||||
|
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
|
||||||
|
val targetBuildTask = project.tasks.maybeCreate(
|
||||||
|
"rustBuild$targetArchCapitalized$profileCapitalized",
|
||||||
|
BuildTask::class.java
|
||||||
|
).apply {
|
||||||
|
group = TASK_GROUP
|
||||||
|
description = "Build dynamic library in $profile mode for $targetArch"
|
||||||
|
rootDirRel = config.rootDirRel
|
||||||
|
target = targetName
|
||||||
|
release = profile == "release"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTask.dependsOn(targetBuildTask)
|
||||||
|
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
|
||||||
|
targetBuildTask
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src-tauri/gen/android/gradle.properties
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. 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
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
|
android.nonFinalResIds=false
|
||||||
BIN
src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#Tue May 10 19:22:52 CST 2022
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
185
src-tauri/gen/android/gradlew
vendored
Executable file
@@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# 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"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || 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
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
89
src-tauri/gen/android/gradlew.bat
vendored
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
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%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="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!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
3
src-tauri/gen/android/settings.gradle
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
include ':app'
|
||||||
|
|
||||||
|
apply from: 'tauri.settings.gradle'
|
||||||
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 24 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 688 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
@@ -1,6 +1,7 @@
|
|||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ fn disable_dmabuf_if_true() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
"title": "ByteChat",
|
"title": "ByteChat",
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 700,
|
"height": 700,
|
||||||
"devtools": false,
|
|
||||||
"zoomHotkeysEnabled": false
|
"zoomHotkeysEnabled": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -25,8 +24,25 @@
|
|||||||
"active": true,
|
"active": true,
|
||||||
"targets": ["deb", "rpm"],
|
"targets": ["deb", "rpm"],
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/icon.ico",
|
"icons/32x32.png",
|
||||||
"icons/icon.png"
|
"icons/128x128.png",
|
||||||
]
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
],
|
||||||
|
"windows": {
|
||||||
|
"allowDowngrades": true,
|
||||||
|
"certificateThumbprint": null,
|
||||||
|
"digestAlgorithm": null,
|
||||||
|
"nsis": null,
|
||||||
|
"signCommand": null,
|
||||||
|
"timestampUrl": null,
|
||||||
|
"tsp": false,
|
||||||
|
"webviewInstallMode": {
|
||||||
|
"silent": true,
|
||||||
|
"type": "offlineInstaller"
|
||||||
|
},
|
||||||
|
"wix": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/assets/7Segment.ttf
Normal file
BIN
src/assets/icon.ico
Normal file
|
After Width: | Height: | Size: 208 KiB |
326
src/index.html
@@ -6,19 +6,18 @@
|
|||||||
<title>ByteChat</title>
|
<title>ByteChat</title>
|
||||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||||
<meta http-equiv="X-Frame-Options" content="DENY">
|
<meta http-equiv="X-Frame-Options" content="DENY">
|
||||||
<link rel="stylesheet" href="./stylesheet.css">
|
<link rel="stylesheet" href="stylesheet.css">
|
||||||
<link rel="icon" type="image/png" href="./assets/favicon.ico">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.7.2/dist/socket.io.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.7.2/dist/socket.io.min.js"></script>
|
||||||
<script src="./main.js"></script>
|
|
||||||
<script src="./frontend.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body style="overflow-y: auto;">
|
<body style="overflow-y: auto;">
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
<div class="chat-header" style="padding: 0.75rem 1rem;">
|
<div class="chat-header" style="padding: 0.75rem 1rem;">
|
||||||
<div class="header-content" style="flex-wrap: wrap; gap: 0.5rem;">
|
<div class="header-content" style="flex-wrap: wrap; gap: 0.5rem;">
|
||||||
<div class="logo" style="font-size: clamp(1.25rem, 4vw, 1.5rem);">
|
<div class="logo"
|
||||||
<a href="/" style="text-decoration: none; font-weight: inherit; font-style: inherit; color: inherit;">
|
style="font-size: clamp(1.25rem, 4vw, 1.5rem); display: flex; align-items: center;">
|
||||||
ByteChat
|
<a href="/" style="text-decoration: none; font-weight: inherit; font-style: inherit; color: inherit; font-family: SevenSegment; display: flex; align-items: center;">
|
||||||
|
<img src="assets/icon.ico" style="width:4vh; height: 5vh; margin-right: 0.5rem;">
|
||||||
|
- Bytechat
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="encryption-badge" id="encryptionBadge" style="font-size: clamp(0.75rem, 3vw, 0.85rem); padding: 0.4rem 0.8rem;">
|
<div class="encryption-badge" id="encryptionBadge" style="font-size: clamp(0.75rem, 3vw, 0.85rem); padding: 0.4rem 0.8rem;">
|
||||||
@@ -26,8 +25,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="welcomeScreen" class="welcome-screen" style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 1rem; flex: 1; min-height: 0;">
|
<!-- Welcome Screen -->
|
||||||
<h1 class="welcome-title" style="font-size: clamp(1.75rem, 8vw, 3rem); margin-bottom: 0.75rem;">ByteChat</h1>
|
<div id="welcomeScreen" class="welcome-screen" style="padding: 1rem;">
|
||||||
|
<h1 class="welcome-title" style="font-size: clamp(1.75rem, 8vw, 3rem); margin-bottom: 0.75rem; font-family: SevenSegment;">ByteChat</h1>
|
||||||
<div class="room-section" style="width: 100%; max-width: 500px;">
|
<div class="room-section" style="width: 100%; max-width: 500px;">
|
||||||
<div class="room-input-container" style="flex-direction: column; gap: 0.75rem; width: 100%;">
|
<div class="room-input-container" style="flex-direction: column; gap: 0.75rem; width: 100%;">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
@@ -49,6 +49,12 @@
|
|||||||
<button class="btn" id="joinRoomBtn" aria-label="Join existing room" style="flex: 1; min-width: 120px; font-size: clamp(0.85rem, 3.5vw, 1rem); padding: 0.6rem 1rem;">Join Room</button>
|
<button class="btn" id="joinRoomBtn" aria-label="Join existing room" style="flex: 1; min-width: 120px; font-size: clamp(0.85rem, 3.5vw, 1rem); padding: 0.6rem 1rem;">Join Room</button>
|
||||||
<button class="btn btn-secondary" id="createRoomBtn" aria-label="Create new room" style="flex: 1; min-width: 120px; font-size: clamp(0.85rem, 3.5vw, 1rem); padding: 0.6rem 1rem;">Create New Room</button>
|
<button class="btn btn-secondary" id="createRoomBtn" aria-label="Create new room" style="flex: 1; min-width: 120px; font-size: clamp(0.85rem, 3.5vw, 1rem); padding: 0.6rem 1rem;">Create New Room</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Browse Public Rooms Button -->
|
||||||
|
<div style="display: flex; justify-content: center; width: 100%;">
|
||||||
|
<button class="btn btn-secondary" id="browsePublicRoomsBtn" style="font-size: clamp(0.85rem, 3.5vw, 1rem); padding: 0.6rem 1.5rem;">
|
||||||
|
Browse Public Rooms
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-text" id="statusText" style="font-size: clamp(0.8rem, 3vw, 0.9rem);">
|
<div class="status-text" id="statusText" style="font-size: clamp(0.8rem, 3vw, 0.9rem);">
|
||||||
<div class="loading-message">
|
<div class="loading-message">
|
||||||
@@ -57,52 +63,278 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Laptop Image and Desktop App Section -->
|
||||||
|
<!--<div style="margin-top: 2rem; display: flex; flex-direction: column; align-items: center; width: 100%;">
|
||||||
|
<img class="laptopimg"
|
||||||
|
src="{{ url_for('static', filename='laptop.png')}}"
|
||||||
|
alt="ByteChat Desktop Application"
|
||||||
|
style="width: clamp(200px, 50vw, 400px); height: auto; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); transition: transform 0.3s ease, box-shadow 0.3s ease; margin-bottom: 1.5rem; display: block;">
|
||||||
|
-->
|
||||||
|
<!-- Desktop App Section -->
|
||||||
|
<!--
|
||||||
|
<div style="text-align: center; max-width: 600px; padding: 0 1rem;">
|
||||||
|
<div style="margin-top: 2rem; padding: 1rem; border: 1px solid #333; border-radius: 8px; background: rgba(0, 0, 0, 0.2);">
|
||||||
|
<h3 style="font-size: clamp(1rem, 4vw, 1.25rem); margin-bottom: 0.75rem; color: #ffffff;">Release Information</h3>
|
||||||
|
<div style="font-size: clamp(0.8rem, 3vw, 0.95rem); color: #888; line-height: 1.5;">
|
||||||
|
<div id="rss-feed" style="padding: 1rem; color: #ccc;">
|
||||||
|
<p>Loading release notes...</p>
|
||||||
|
</div>
|
||||||
|
<div id="show-legacy-container" style="display: none; text-align: center; margin-top: 1rem;">
|
||||||
|
<button id="show-legacy-btn" class="btn" style="font-size: 0.8rem; padding: 0.4rem 0.8rem;">
|
||||||
|
Show Legacy Releases
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Public Rooms Browser - Fixed structure -->
|
||||||
|
<div id="publicRoomsBrowser" style="
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
z-index: 2000;
|
||||||
|
padding: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: auto;
|
||||||
|
">
|
||||||
|
<div id="browserContent" style="
|
||||||
|
max-width: 95%;
|
||||||
|
max-height: 95%;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
min-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
">
|
||||||
|
<!-- Header -->
|
||||||
|
<div style="
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
">
|
||||||
|
<div>
|
||||||
|
<h2 style="
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: clamp(1.25rem, 5vw, 1.75rem);
|
||||||
|
color: #00ff88;
|
||||||
|
">Public Rooms</h2>
|
||||||
|
<p style="
|
||||||
|
margin: 0;
|
||||||
|
color: #888;
|
||||||
|
font-size: clamp(0.85rem, 3.5vw, 1rem);
|
||||||
|
" id="roomsStats">Browse and join active public rooms</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
id="closePublicRoomsBrowserBtn"
|
||||||
|
style="
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
"
|
||||||
|
>Close</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<div style="
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
">
|
||||||
|
<select id="roomsSortSelect" style="
|
||||||
|
background: #333;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
min-width: 120px;
|
||||||
|
">
|
||||||
|
<option value="activity">Most Active</option>
|
||||||
|
<option value="created">Newest</option>
|
||||||
|
<option value="users">Most Users</option>
|
||||||
|
<option value="messages">Most Messages</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="minUsersFilter"
|
||||||
|
placeholder="Min users"
|
||||||
|
min="0"
|
||||||
|
max="1000"
|
||||||
|
style="
|
||||||
|
background: #333;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
width: 150px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
id="refreshPublicRoomsBtn"
|
||||||
|
style="
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
"
|
||||||
|
>Refresh</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div id="roomsLoading" style="
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3rem;
|
||||||
|
flex: 1;
|
||||||
|
">
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
color: #888;
|
||||||
|
">
|
||||||
|
<div class="loading-spinner" style="
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid #333;
|
||||||
|
border-top: 2px solid #00ff88;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
"></div>
|
||||||
|
<span>Loading public rooms...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rooms List -->
|
||||||
|
<div id="roomsList" style="
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
display: none;
|
||||||
|
">
|
||||||
|
<!-- Rooms will be populated here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div id="roomsEmpty" style="
|
||||||
|
display: none;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3rem;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
">
|
||||||
|
<div>
|
||||||
|
<h3 style="margin: 0 0 0.5rem 0; color: #ffffff;">No Public Rooms Found</h3>
|
||||||
|
<p style="margin: 0; color: #888;">Check back later or create your own room</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Chat Screen -->
|
<!-- Chat Screen -->
|
||||||
<div id="chatScreen" style="display: none;" class="chat-screen">
|
<div id="chatScreen" style="display: none;" class="chat-screen">
|
||||||
<div class="room-info" id="roomInfo" style="display: none; padding: 0.6rem 0.75rem; flex-wrap: wrap; gap: 0.5rem;">
|
<!-- Room info moved to top and fixed position -->
|
||||||
<div class="room-details" style="min-width: auto; flex: 1;">
|
<div class="room-info" id="roomInfo" style="
|
||||||
<span style="font-size: clamp(0.75rem, 3vw, 0.85rem);">Room: <strong id="currentRoomId"></strong></span>
|
display: none;
|
||||||
<span id="messageCounter" style="font-size: clamp(0.75rem, 3vw, 0.85rem);">Messages: 0/256</span>
|
padding: 0.6rem 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(20, 20, 20, 0.98);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
">
|
||||||
|
<div class="room-details" style="min-width: auto; flex: 1; display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between;">
|
||||||
|
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||||
|
<span style="font-size: clamp(0.75rem, 3vw, 0.85rem);">
|
||||||
|
Room: <strong id="currentRoomId"></strong>
|
||||||
|
</span>
|
||||||
|
<span id="messageCounter" style="font-size: clamp(0.75rem, 3vw, 0.85rem);">
|
||||||
|
Messages: 0/256
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||||
|
<span id="userCounter" style="font-size: clamp(0.75rem, 3vw, 0.85rem);">
|
||||||
|
Users: 0
|
||||||
|
</span>
|
||||||
|
<span id="encryptionStatus" style="font-size: clamp(0.75rem, 3vw, 0.85rem);">
|
||||||
|
🔐 Encrypted
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="room-details" style="min-width: auto; flex: 1;">
|
|
||||||
<span id="userCounter" style="font-size: clamp(0.75rem, 3vw, 0.85rem);">Users: 0</span>
|
<!-- wrapper for scroll -->
|
||||||
<span id="encryptionStatus" style="font-size: clamp(0.75rem, 3vw, 0.85rem);">🔐 Encrypted</span>
|
<div class="messages-wrapper" id="messagesWrapper" style="padding: 0.75rem; padding-top: 0; padding-bottom: 80px; flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; scroll-behavior: smooth;">
|
||||||
|
<div class="messages-container" id="messagesContainer" role="log" aria-live="polite" aria-label="Chat messages">
|
||||||
|
<!-- Messages will be inserted here -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- wrapper for scroll -->
|
<div class="input-section" id="inputSection" style="display: none; padding: 0.6rem 0.75rem; position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000; background: rgba(20, 20, 20, 0.98); backdrop-filter: blur(10px); border-top: 1px solid #333; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.3);">
|
||||||
<div class="messages-wrapper" id="messagesWrapper" style="padding: 0.75rem; padding-bottom: 80px; flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; scroll-behavior: smooth;">
|
<div class="input-container" style="gap: 0.4rem; max-width: 100%;">
|
||||||
<div class="messages-container" id="messagesContainer" role="log" aria-live="polite" aria-label="Chat messages">
|
<label for="messageInput" class="visually-hidden">Type your message</label>
|
||||||
<!-- Messages will be inserted here -->
|
<textarea
|
||||||
</div>
|
class="message-input"
|
||||||
</div>
|
id="messageInput"
|
||||||
</div>
|
placeholder="Message ByteChat…"
|
||||||
|
rows="1"
|
||||||
<div class="input-section" id="inputSection" style="display: none; padding: 0.6rem 0.75rem; position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000; background: rgba(20, 20, 20, 0.98); backdrop-filter: blur(10px); border-top: 1px solid #333; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.3);">
|
disabled
|
||||||
<div class="input-container" style="gap: 0.4rem; max-width: 100%;">
|
maxlength="4000"
|
||||||
<label for="messageInput" class="visually-hidden">Type your message</label>
|
aria-label="Type your message"
|
||||||
<textarea
|
style="font-size: clamp(0.9rem, 4vw, 1rem); padding: 0.6rem 0.8rem; border-radius: 10px; resize: none; max-height: 120px; min-height: 44px; font-size: 16px; -webkit-appearance: none;"
|
||||||
class="message-input"
|
autocomplete="off"
|
||||||
id="messageInput"
|
autocapitalize="sentences"
|
||||||
placeholder="Send a Message…"
|
spellcheck="true"
|
||||||
rows="1"
|
></textarea>
|
||||||
disabled
|
<button class="send-button" id="sendButton" disabled aria-label="Send message" style="width: 40px; height: 40px; flex-shrink: 0; -webkit-tap-highlight-color: transparent;">
|
||||||
maxlength="4000"
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||||
aria-label="Type your message"
|
<path d="m22 2-7 20-4-9-9-4 20-7z"/>
|
||||||
style="font-size: clamp(0.9rem, 4vw, 1rem); padding: 0.6rem 0.8rem; border-radius: 10px; resize: none; max-height: 120px; min-height: 44px; font-size: 16px; -webkit-appearance: none;"
|
</svg>
|
||||||
autocomplete="off"
|
</button>
|
||||||
autocapitalize="sentences"
|
</div>
|
||||||
spellcheck="true"
|
|
||||||
></textarea>
|
|
||||||
<button class="send-button" id="sendButton" disabled aria-label="Send message" style="width: 40px; height: 40px; flex-shrink: 0; -webkit-tap-highlight-color: transparent;">
|
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
||||||
<path d="m22 2-7 20-4-9-9-4 20-7z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Load scripts at the bottom for better initialization -->
|
||||||
|
<script src="main.js" defer></script>
|
||||||
|
<script src="frontend.js" defer></script>
|
||||||
|
<script src="rooms.js"></script>
|
||||||
|
<!--<script src="rss.js" async></script>-->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
623
src/main.js
@@ -50,9 +50,62 @@ const MAX_MESSAGE_LENGTH = 4000;
|
|||||||
const MAX_ROOM_ID_LENGTH = 32;
|
const MAX_ROOM_ID_LENGTH = 32;
|
||||||
const MAX_MESSAGES = 512;
|
const MAX_MESSAGES = 512;
|
||||||
|
|
||||||
// In-memory storage for keys (Tauri-compatible)
|
// Enhanced keyStorage with better error handling and forced saves
|
||||||
const keyStorage = {
|
const keyStorage = {
|
||||||
// Store RSA keypair as JWK for persistence
|
// Store RSA keypair as JWK for persistence
|
||||||
|
async initTauriStore() {
|
||||||
|
if (!isRunningInTauri) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try modern Tauri store API first
|
||||||
|
if (window.__TAURI_PLUGIN_STORE__) {
|
||||||
|
const { Store } = window.__TAURI_PLUGIN_STORE__;
|
||||||
|
return await Store.load('bytechat-keys.json');
|
||||||
|
}
|
||||||
|
// Try legacy API
|
||||||
|
else if (window.__TAURI__ && window.__TAURI__.store) {
|
||||||
|
const { Store } = window.__TAURI__.store;
|
||||||
|
return await Store.load('bytechat-keys.json');
|
||||||
|
}
|
||||||
|
// Try direct access to store
|
||||||
|
else if (window.__TAURI__ && window.__TAURI__.invoke) {
|
||||||
|
// Use invoke API directly
|
||||||
|
return {
|
||||||
|
get: async (key) => {
|
||||||
|
try {
|
||||||
|
return await window.__TAURI__.invoke('plugin:store|get', { key });
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Store get failed:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set: async (key, value) => {
|
||||||
|
try {
|
||||||
|
await window.__TAURI__.invoke('plugin:store|set', { key, value });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Store set failed:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delete: async (key) => {
|
||||||
|
try {
|
||||||
|
await window.__TAURI__.invoke('plugin:store|delete', { key });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Store delete failed:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Tauri store initialization failed:', e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Store RSA keypair as JWK for persistence
|
||||||
async storeKeyPair(keyPair) {
|
async storeKeyPair(keyPair) {
|
||||||
try {
|
try {
|
||||||
const publicJWK = await window.crypto.subtle.exportKey('jwk', keyPair.publicKey);
|
const publicJWK = await window.crypto.subtle.exportKey('jwk', keyPair.publicKey);
|
||||||
@@ -64,71 +117,89 @@ const keyStorage = {
|
|||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isRunningInTauri && tauriStore) {
|
if (isRunningInTauri) {
|
||||||
try {
|
try {
|
||||||
await tauriStore.set('rsa_keypair', keyData);
|
const store = await this.initTauriStore();
|
||||||
await tauriStore.save();
|
if (store) {
|
||||||
console.log('RSA keypair stored in Tauri store');
|
await store.set('rsa_keypair', keyData);
|
||||||
|
console.log('RSA keypair stored in Tauri store');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} catch (storeError) {
|
} catch (storeError) {
|
||||||
console.warn('Failed to use Tauri store, falling back to memory:', storeError);
|
console.warn('Failed to use Tauri store, falling back to memory:', storeError);
|
||||||
this._memoryKeys = keyData;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Fallback to memory storage
|
|
||||||
this._memoryKeys = keyData;
|
|
||||||
console.log('RSA keypair stored in memory (no persistent storage)');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback to memory storage
|
||||||
|
this._memoryKeys = keyData;
|
||||||
|
console.log('RSA keypair stored in memory (no persistent storage)');
|
||||||
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to store keypair:', error);
|
console.error('Failed to store keypair:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Fixed loadKeyPair function with correct extractable settings
|
||||||
async loadKeyPair() {
|
async loadKeyPair() {
|
||||||
try {
|
try {
|
||||||
let keyData;
|
let keyData;
|
||||||
|
|
||||||
if (isRunningInTauri && tauriStore) {
|
if (isRunningInTauri) {
|
||||||
try {
|
try {
|
||||||
keyData = await tauriStore.get('rsa_keypair');
|
const store = await this.initTauriStore();
|
||||||
} catch (storeError) {
|
if (store) {
|
||||||
console.warn('Failed to read from Tauri store:', storeError);
|
keyData = await store.get('rsa_keypair');
|
||||||
keyData = this._memoryKeys;
|
if (keyData) {
|
||||||
|
console.log('RSA keypair loaded from Tauri store');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} catch (storeError) {
|
||||||
keyData = this._memoryKeys;
|
console.warn('Failed to read from Tauri store:', storeError);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!keyData || !keyData.publicKey || !keyData.privateKey) {
|
|
||||||
console.log('No stored keypair found');
|
// Fallback to memory if Tauri store failed
|
||||||
return null;
|
if (!keyData) {
|
||||||
|
keyData = this._memoryKeys;
|
||||||
|
if (keyData) {
|
||||||
|
console.log('RSA keypair loaded from memory');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Import keys from JWK
|
|
||||||
const publicKey = await window.crypto.subtle.importKey(
|
if (!keyData || !keyData.publicKey || !keyData.privateKey) {
|
||||||
'jwk',
|
console.log('No stored keypair found');
|
||||||
keyData.publicKey,
|
|
||||||
{ name: 'RSA-OAEP', hash: 'SHA-256' },
|
|
||||||
false,
|
|
||||||
['encrypt']
|
|
||||||
);
|
|
||||||
|
|
||||||
const privateKey = await window.crypto.subtle.importKey(
|
|
||||||
'jwk',
|
|
||||||
keyData.privateKey,
|
|
||||||
{ name: 'RSA-OAEP', hash: 'SHA-256' },
|
|
||||||
false,
|
|
||||||
['decrypt']
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('RSA keypair loaded from storage');
|
|
||||||
return { publicKey, privateKey };
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load keypair:', error);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Import keys from JWK - CRITICAL: Make public key extractable for export
|
||||||
|
const publicKey = await window.crypto.subtle.importKey(
|
||||||
|
'jwk',
|
||||||
|
keyData.publicKey,
|
||||||
|
{ name: 'RSA-OAEP', hash: 'SHA-256' },
|
||||||
|
true, // Make extractable so we can export it later
|
||||||
|
['encrypt']
|
||||||
|
);
|
||||||
|
|
||||||
|
const privateKey = await window.crypto.subtle.importKey(
|
||||||
|
'jwk',
|
||||||
|
keyData.privateKey,
|
||||||
|
{ name: 'RSA-OAEP', hash: 'SHA-256' },
|
||||||
|
false, // Private key doesn't need to be extractable for our use case
|
||||||
|
['decrypt']
|
||||||
|
);
|
||||||
|
|
||||||
|
return { publicKey, privateKey };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load keypair:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Also fix the generateKeyPair function to ensure consistent extractable settings
|
||||||
|
|
||||||
|
|
||||||
async storeSessionKey(roomId, sessionKey) {
|
async storeSessionKey(roomId, sessionKey) {
|
||||||
try {
|
try {
|
||||||
const keyData = await window.crypto.subtle.exportKey('raw', sessionKey);
|
const keyData = await window.crypto.subtle.exportKey('raw', sessionKey);
|
||||||
@@ -140,23 +211,39 @@ const keyStorage = {
|
|||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isRunningInTauri && tauriStore) {
|
let stored = false;
|
||||||
|
|
||||||
|
if (isRunningInTauri) {
|
||||||
try {
|
try {
|
||||||
await tauriStore.set(`session_key_${roomId}`, sessionData);
|
const store = await this.initTauriStore();
|
||||||
await tauriStore.save();
|
if (store) {
|
||||||
console.log('Session key stored for room:', roomId);
|
await store.set(`session_key_${roomId}`, sessionData);
|
||||||
|
console.log('✅ Session key stored in Tauri store for room:', roomId);
|
||||||
|
|
||||||
|
// Verify storage worked
|
||||||
|
const verification = await store.get(`session_key_${roomId}`);
|
||||||
|
if (verification) {
|
||||||
|
stored = true;
|
||||||
|
} else {
|
||||||
|
throw new Error('Session key storage verification failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (storeError) {
|
} catch (storeError) {
|
||||||
console.warn('Failed to store session key in Tauri store:', storeError);
|
console.warn('❌ Failed to store session key in Tauri store:', storeError);
|
||||||
this._sessionKeys = this._sessionKeys || {};
|
|
||||||
this._sessionKeys[roomId] = sessionData;
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if (!stored) {
|
||||||
|
// Fallback to memory
|
||||||
this._sessionKeys = this._sessionKeys || {};
|
this._sessionKeys = this._sessionKeys || {};
|
||||||
this._sessionKeys[roomId] = sessionData;
|
this._sessionKeys[roomId] = sessionData;
|
||||||
console.log('Session key stored in memory for room:', roomId);
|
console.log('📝 Session key stored in memory for room:', roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return stored;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to store session key:', error);
|
console.error('❌ Critical error storing session key:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -164,21 +251,31 @@ const keyStorage = {
|
|||||||
try {
|
try {
|
||||||
let sessionData;
|
let sessionData;
|
||||||
|
|
||||||
if (isRunningInTauri && tauriStore) {
|
if (isRunningInTauri) {
|
||||||
try {
|
try {
|
||||||
sessionData = await tauriStore.get(`session_key_${roomId}`);
|
const store = await this.initTauriStore();
|
||||||
|
if (store) {
|
||||||
|
sessionData = await store.get(`session_key_${roomId}`);
|
||||||
|
if (sessionData) {
|
||||||
|
console.log('✅ Session key loaded from Tauri store for room:', roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (storeError) {
|
} catch (storeError) {
|
||||||
console.warn('Failed to load session key from Tauri store:', storeError);
|
console.warn('⚠️ Failed to load session key from Tauri store:', storeError);
|
||||||
this._sessionKeys = this._sessionKeys || {};
|
|
||||||
sessionData = this._sessionKeys[roomId];
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
// Fallback to memory if Tauri store failed
|
||||||
|
if (!sessionData) {
|
||||||
this._sessionKeys = this._sessionKeys || {};
|
this._sessionKeys = this._sessionKeys || {};
|
||||||
sessionData = this._sessionKeys[roomId];
|
sessionData = this._sessionKeys[roomId];
|
||||||
|
if (sessionData) {
|
||||||
|
console.log('📝 Session key loaded from memory for room:', roomId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sessionData || !sessionData.key) {
|
if (!sessionData || !sessionData.key) {
|
||||||
console.log('No stored session key found for room:', roomId);
|
console.log('❌ No stored session key found for room:', roomId);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,23 +284,130 @@ const keyStorage = {
|
|||||||
'raw',
|
'raw',
|
||||||
keyData,
|
keyData,
|
||||||
{ name: 'AES-GCM' },
|
{ name: 'AES-GCM' },
|
||||||
false,
|
true, // Make extractable so we can re-store if needed
|
||||||
['encrypt', 'decrypt']
|
['encrypt', 'decrypt']
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('Session key loaded for room:', roomId);
|
console.log('✅ Session key successfully imported for room:', roomId);
|
||||||
return sessionKey;
|
return sessionKey;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load session key:', error);
|
console.error('❌ Failed to load session key for room', roomId, ':', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async clearSessionKey(roomId) {
|
||||||
|
try {
|
||||||
|
if (isRunningInTauri) {
|
||||||
|
try {
|
||||||
|
const store = await this.initTauriStore();
|
||||||
|
if (store) {
|
||||||
|
await store.delete(`session_key_${roomId}`);
|
||||||
|
console.log('🗑️ Session key cleared from Tauri store for room:', roomId);
|
||||||
|
}
|
||||||
|
} catch (storeError) {
|
||||||
|
console.warn('Failed to clear from Tauri store:', storeError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._sessionKeys && this._sessionKeys[roomId]) {
|
||||||
|
delete this._sessionKeys[roomId];
|
||||||
|
console.log('🗑️ Session key cleared from memory for room:', roomId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to clear session key for room', roomId, ':', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async forceSave() {
|
||||||
|
// Modern Tauri stores auto-save, this is mainly for compatibility
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
_memoryKeys: null,
|
_memoryKeys: null,
|
||||||
_sessionKeys: {}
|
_sessionKeys: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function generateKeyPair() {
|
||||||
|
try {
|
||||||
|
console.log('=== Starting key generation process ===');
|
||||||
|
updateStatus('Loading or generating encryption keys...', false);
|
||||||
|
|
||||||
|
// Try to load existing keypair first
|
||||||
|
console.log('Attempting to load stored keypair...');
|
||||||
|
const storedKeyPair = await keyStorage.loadKeyPair();
|
||||||
|
|
||||||
|
if (storedKeyPair) {
|
||||||
|
keyPair = storedKeyPair;
|
||||||
|
console.log("✅ Using stored RSA keypair");
|
||||||
|
} else {
|
||||||
|
console.log("🔧 Generating new RSA keypair...");
|
||||||
|
|
||||||
|
keyPair = await window.crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: "RSA-OAEP",
|
||||||
|
modulusLength: 2048,
|
||||||
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
|
hash: "SHA-256"
|
||||||
|
},
|
||||||
|
true, // Make extractable for storage AND export
|
||||||
|
["encrypt", "decrypt"]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("✅ New RSA keypair generated");
|
||||||
|
|
||||||
|
// Store the new keypair
|
||||||
|
console.log("💾 Storing new keypair...");
|
||||||
|
await keyStorage.storeKeyPair(keyPair);
|
||||||
|
console.log("✅ Keypair stored successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the keypair
|
||||||
|
if (!keyPair || !keyPair.publicKey || !keyPair.privateKey) {
|
||||||
|
throw new Error('Invalid keypair generated/loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that public key is extractable
|
||||||
|
try {
|
||||||
|
await window.crypto.subtle.exportKey("spki", keyPair.publicKey);
|
||||||
|
console.log("✅ Public key export test passed");
|
||||||
|
} catch (exportError) {
|
||||||
|
console.error("❌ Public key is not extractable:", exportError);
|
||||||
|
throw new Error('Public key must be extractable for protocol to work');
|
||||||
|
}
|
||||||
|
|
||||||
|
keysReady = true;
|
||||||
|
updateStatus("Ready to join a room", false);
|
||||||
|
console.log("✅ Key pair ready - can now join rooms");
|
||||||
|
console.log('=== Key generation process complete ===');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Failed to generate/load key pair:", error);
|
||||||
|
updateStatus("Error: Failed to set up encryption keys", true);
|
||||||
|
|
||||||
|
// Try to generate a fallback keypair
|
||||||
|
try {
|
||||||
|
console.log("🔄 Attempting fallback key generation...");
|
||||||
|
keyPair = await window.crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: "RSA-OAEP",
|
||||||
|
modulusLength: 2048,
|
||||||
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
|
hash: "SHA-256"
|
||||||
|
},
|
||||||
|
true, // Must be extractable for export
|
||||||
|
["encrypt", "decrypt"]
|
||||||
|
);
|
||||||
|
keysReady = true;
|
||||||
|
updateStatus("Ready to join a room (temporary keys)", false);
|
||||||
|
console.log("✅ Fallback keypair generated successfully");
|
||||||
|
} catch (fallbackError) {
|
||||||
|
console.error("❌ Fallback key generation also failed:", fallbackError);
|
||||||
|
throw new SecureChatError("Key generation failed completely", "KEY_GEN_ERROR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Security utilities
|
// Security utilities
|
||||||
const SecurityUtils = {
|
const SecurityUtils = {
|
||||||
sanitizeInput: function(input) {
|
sanitizeInput: function(input) {
|
||||||
@@ -307,94 +511,37 @@ function clearSensitiveData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateKeyPair() {
|
|
||||||
try {
|
|
||||||
console.log('=== Starting key generation process ===');
|
|
||||||
updateStatus('Loading or generating encryption keys...', false);
|
|
||||||
|
|
||||||
// Try to load existing keypair first
|
|
||||||
console.log('Attempting to load stored keypair...');
|
|
||||||
const storedKeyPair = await keyStorage.loadKeyPair();
|
|
||||||
|
|
||||||
if (storedKeyPair) {
|
|
||||||
keyPair = storedKeyPair;
|
|
||||||
console.log("✅ Using stored RSA keypair");
|
|
||||||
} else {
|
|
||||||
console.log("🔧 Generating new RSA keypair...");
|
|
||||||
|
|
||||||
keyPair = await window.crypto.subtle.generateKey(
|
|
||||||
{
|
|
||||||
name: "RSA-OAEP",
|
|
||||||
modulusLength: 2048,
|
|
||||||
publicExponent: new Uint8Array([1, 0, 1]),
|
|
||||||
hash: "SHA-256"
|
|
||||||
},
|
|
||||||
true, // Make extractable for storage
|
|
||||||
["encrypt", "decrypt"]
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("✅ New RSA keypair generated");
|
|
||||||
|
|
||||||
// Store the new keypair
|
|
||||||
console.log("💾 Storing new keypair...");
|
|
||||||
await keyStorage.storeKeyPair(keyPair);
|
|
||||||
console.log("✅ Keypair stored successfully");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the keypair
|
|
||||||
if (!keyPair || !keyPair.publicKey || !keyPair.privateKey) {
|
|
||||||
throw new Error('Invalid keypair generated/loaded');
|
|
||||||
}
|
|
||||||
|
|
||||||
keysReady = true;
|
|
||||||
updateStatus("Ready to join a room", false);
|
|
||||||
console.log("✅ Key pair ready - can now join rooms");
|
|
||||||
console.log('=== Key generation process complete ===');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Failed to generate/load key pair:", error);
|
|
||||||
updateStatus("Error: Failed to set up encryption keys", true);
|
|
||||||
|
|
||||||
// Try to generate a fallback keypair without storage
|
|
||||||
try {
|
|
||||||
console.log("🔄 Attempting fallback key generation...");
|
|
||||||
keyPair = await window.crypto.subtle.generateKey(
|
|
||||||
{
|
|
||||||
name: "RSA-OAEP",
|
|
||||||
modulusLength: 2048,
|
|
||||||
publicExponent: new Uint8Array([1, 0, 1]),
|
|
||||||
hash: "SHA-256"
|
|
||||||
},
|
|
||||||
false, // Not extractable for fallback
|
|
||||||
["encrypt", "decrypt"]
|
|
||||||
);
|
|
||||||
keysReady = true;
|
|
||||||
updateStatus("Ready to join a room (temporary keys)", false);
|
|
||||||
console.log("✅ Fallback keypair generated successfully");
|
|
||||||
} catch (fallbackError) {
|
|
||||||
console.error("❌ Fallback key generation also failed:", fallbackError);
|
|
||||||
throw new SecureChatError("Key generation failed completely", "KEY_GEN_ERROR");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
// Enhanced generateSessionKey with guaranteed storage
|
||||||
async function generateSessionKey() {
|
async function generateSessionKey() {
|
||||||
try {
|
try {
|
||||||
|
console.log("🔧 Generating new session key...");
|
||||||
sessionKey = await window.crypto.subtle.generateKey(
|
sessionKey = await window.crypto.subtle.generateKey(
|
||||||
{ name: "AES-GCM", length: 256 },
|
{ name: "AES-GCM", length: 256 },
|
||||||
true,
|
true, // Make extractable for storage
|
||||||
["encrypt", "decrypt"]
|
["encrypt", "decrypt"]
|
||||||
);
|
);
|
||||||
console.log("Session key generated");
|
|
||||||
|
|
||||||
// Store session key for this room
|
if (!sessionKey) {
|
||||||
|
throw new Error('Session key generation returned null/undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ Session key generated");
|
||||||
|
|
||||||
|
// CRITICAL: Immediately store the session key
|
||||||
if (currentRoom) {
|
if (currentRoom) {
|
||||||
|
console.log("💾 Storing newly generated session key...");
|
||||||
await keyStorage.storeSessionKey(currentRoom, sessionKey);
|
await keyStorage.storeSessionKey(currentRoom, sessionKey);
|
||||||
|
await keyStorage.forceSave();
|
||||||
|
console.log("✅ Session key stored and saved successfully");
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ No current room - session key not stored");
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessionKey;
|
return sessionKey;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to generate session key:", error);
|
console.error("❌ Failed to generate session key:", error);
|
||||||
throw new SecureChatError("Session key generation failed", "SESSION_KEY_ERROR");
|
throw new SecureChatError("Session key generation failed", "SESSION_KEY_ERROR");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -487,20 +634,22 @@ async function encryptSessionKey(sessionKey, publicKey) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced decryptSessionKey function
|
||||||
async function decryptSessionKey(encryptedKey) {
|
async function decryptSessionKey(encryptedKey) {
|
||||||
try {
|
try {
|
||||||
console.log("Decrypting session key...");
|
console.log("🔓 Decrypting session key...");
|
||||||
|
|
||||||
const decryptedSessionKey = await unwrapAesKeyWithRsa(keyPair.privateKey, encryptedKey);
|
const decryptedSessionKey = await unwrapAesKeyWithRsa(keyPair.privateKey, encryptedKey);
|
||||||
|
|
||||||
// Store the decrypted session key
|
if (!decryptedSessionKey) {
|
||||||
if (currentRoom) {
|
throw new Error('Failed to unwrap session key - got null/undefined result');
|
||||||
await keyStorage.storeSessionKey(currentRoom, decryptedSessionKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("✅ Session key decrypted successfully");
|
||||||
return decryptedSessionKey;
|
return decryptedSessionKey;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to decrypt session key:", error);
|
console.error("❌ Failed to decrypt session key:", error);
|
||||||
throw new SecureChatError("Session key decryption failed", "SESSION_KEY_DECRYPT_ERROR");
|
throw new SecureChatError("Session key decryption failed", "SESSION_KEY_DECRYPT_ERROR");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -615,6 +764,7 @@ function setupSocketListeners() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Enhanced room_joined handler with better key loading
|
||||||
socket.on('room_joined', async (data) => {
|
socket.on('room_joined', async (data) => {
|
||||||
try {
|
try {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
@@ -632,11 +782,16 @@ function setupSocketListeners() {
|
|||||||
currentDisplayName = data.display_name;
|
currentDisplayName = data.display_name;
|
||||||
roomUsers = data.user_keys || {};
|
roomUsers = data.user_keys || {};
|
||||||
|
|
||||||
// Try to load existing session key for this room
|
// ENHANCED: Try to load existing session key for this room FIRST
|
||||||
|
console.log("🔍 Checking for existing session key for room:", currentRoom);
|
||||||
const storedSessionKey = await keyStorage.loadSessionKey(currentRoom);
|
const storedSessionKey = await keyStorage.loadSessionKey(currentRoom);
|
||||||
if (storedSessionKey && !sessionKey) {
|
|
||||||
|
if (storedSessionKey) {
|
||||||
sessionKey = storedSessionKey;
|
sessionKey = storedSessionKey;
|
||||||
console.log("Loaded stored session key for room:", currentRoom);
|
console.log("✅ Using stored session key for room:", currentRoom);
|
||||||
|
} else {
|
||||||
|
console.log("❌ No stored session key found for room:", currentRoom);
|
||||||
|
sessionKey = null; // Explicitly clear
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switch to chat screen
|
// Switch to chat screen
|
||||||
@@ -652,25 +807,49 @@ function setupSocketListeners() {
|
|||||||
|
|
||||||
// Handle session key based on user status
|
// Handle session key based on user status
|
||||||
if (data.is_first_user) {
|
if (data.is_first_user) {
|
||||||
console.log("First user - generating session key");
|
console.log("👑 First user - generating new session key");
|
||||||
await generateSessionKey();
|
await generateSessionKey();
|
||||||
|
// Key is stored in generateSessionKey()
|
||||||
updateStatus("You're the first user! Session key generated. Others will receive it when they join.", false);
|
updateStatus("You're the first user! Session key generated. Others will receive it when they join.", false);
|
||||||
updateInputState();
|
updateInputState();
|
||||||
} else if (sessionKey) {
|
} else if (sessionKey) {
|
||||||
console.log("Using existing/stored session key");
|
console.log("✅ Using existing/stored session key");
|
||||||
updateStatus("Session key available. You can chat.", false);
|
updateStatus("Session key available. You can chat.", false);
|
||||||
updateInputState();
|
updateInputState();
|
||||||
} else {
|
} else {
|
||||||
console.log("Requesting session key from existing users");
|
console.log("❓ Need session key - requesting from existing users");
|
||||||
updateStatus("Requesting session key from other users...", false);
|
updateStatus("Requesting session key from other users...", false);
|
||||||
|
|
||||||
// Explicitly request session key
|
// Explicitly request session key with retry logic
|
||||||
const publicKeyString = await exportPublicKey(keyPair.publicKey);
|
const publicKeyString = await exportPublicKey(keyPair.publicKey);
|
||||||
socket.emit('request_session_key', {
|
socket.emit('request_session_key', {
|
||||||
room_id: currentRoom,
|
room_id: currentRoom,
|
||||||
public_key: publicKeyString
|
public_key: publicKeyString
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set up retry mechanism for session key request
|
||||||
|
let retryCount = 0;
|
||||||
|
const maxRetries = 3;
|
||||||
|
const retryInterval = setInterval(() => {
|
||||||
|
if (sessionKey) {
|
||||||
|
clearInterval(retryInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
retryCount++;
|
||||||
|
if (retryCount <= maxRetries) {
|
||||||
|
console.log(`🔄 Retrying session key request (${retryCount}/${maxRetries})`);
|
||||||
|
socket.emit('request_session_key', {
|
||||||
|
room_id: currentRoom,
|
||||||
|
public_key: publicKeyString
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
clearInterval(retryInterval);
|
||||||
|
console.log("❌ Max session key request retries reached");
|
||||||
|
updateStatus("Failed to obtain session key. Try leaving and rejoining the room.", true);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
updateInputState();
|
updateInputState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,10 +862,10 @@ function setupSocketListeners() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[room_joined] sessionKey:", !!sessionKey, "isFirstUser:", data.is_first_user);
|
console.log(`🏠 Room joined successfully. SessionKey: ${!!sessionKey}, FirstUser: ${data.is_first_user}`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error processing room_joined:", error);
|
console.error("❌ Error processing room_joined:", error);
|
||||||
updateStatus("Error joining room", true);
|
updateStatus("Error joining room", true);
|
||||||
updateInputState();
|
updateInputState();
|
||||||
}
|
}
|
||||||
@@ -751,34 +930,61 @@ function setupSocketListeners() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// This is the critical event that was likely not firing properly
|
// Enhanced session_key_received handler with better storage
|
||||||
socket.on('session_key_received', async (data) => {
|
socket.on('session_key_received', async (data) => {
|
||||||
try {
|
try {
|
||||||
console.log("session_key_received event fired:", data);
|
console.log("🔑 session_key_received event fired for room:", currentRoom);
|
||||||
|
|
||||||
if (!data || !data.encrypted_key) {
|
if (!data || !data.encrypted_key) {
|
||||||
console.log("Invalid session key data received");
|
console.log("❌ Invalid session key data received");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sessionKey) {
|
if (!sessionKey) {
|
||||||
try {
|
try {
|
||||||
console.log("Attempting to decrypt received session key");
|
console.log("🔓 Decrypting received session key...");
|
||||||
sessionKey = await decryptSessionKey(data.encrypted_key);
|
const decryptedKey = await decryptSessionKey(data.encrypted_key);
|
||||||
|
|
||||||
console.log("Session key decrypted successfully!");
|
if (!decryptedKey) {
|
||||||
|
throw new Error('Decryption returned null/undefined key');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the session key
|
||||||
|
sessionKey = decryptedKey;
|
||||||
|
|
||||||
|
// CRITICAL: Explicitly store the session key immediately
|
||||||
|
console.log("💾 Storing decrypted session key...");
|
||||||
|
await keyStorage.storeSessionKey(currentRoom, sessionKey);
|
||||||
|
|
||||||
|
// Modern Tauri store auto-saves
|
||||||
|
await keyStorage.forceSave();
|
||||||
|
|
||||||
|
console.log("✅ Session key decrypted, stored, and saved successfully!");
|
||||||
updateStatus("Session key received! You can now chat securely.", false);
|
updateStatus("Session key received! You can now chat securely.", false);
|
||||||
updateInputState();
|
updateInputState();
|
||||||
addSystemMessage("🔑 Session key received - you can now chat!");
|
addSystemMessage("🔑 Session key received - you can now chat!");
|
||||||
|
|
||||||
|
// Verify storage by attempting to reload
|
||||||
|
if (isRunningInTauri) {
|
||||||
|
console.log("🔍 Verifying session key storage...");
|
||||||
|
const verificationKey = await keyStorage.loadSessionKey(currentRoom);
|
||||||
|
if (verificationKey) {
|
||||||
|
console.log("✅ Session key storage verification successful");
|
||||||
|
} else {
|
||||||
|
console.error("❌ Session key storage verification failed");
|
||||||
|
updateStatus("Warning: Session key may not be saved", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to decrypt received session key:", error);
|
console.error("❌ Failed to process received session key:", error);
|
||||||
updateStatus("Failed to decrypt session key - please try rejoining", true);
|
updateStatus("Failed to decrypt session key - please try rejoining", true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("Session key already exists, ignoring duplicate");
|
console.log("ℹ️ Session key already exists, ignoring duplicate");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to process session_key_received:", error);
|
console.error("❌ Critical error in session_key_received:", error);
|
||||||
updateStatus("Error: Failed to process session key", true);
|
updateStatus("Error: Failed to process session key", true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -948,8 +1154,8 @@ async function displayMessage(messageData) {
|
|||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit'
|
minute: '2-digit'
|
||||||
});
|
});
|
||||||
|
// if shit breaks for uuid display fix this
|
||||||
const displayName = messageData.display_name || `User-${messageData.sender_id?.substring(0, 8) || 'Unknown'}`;
|
const displayName = messageData.display_name || `<p style="font-family:SevenSegment;">User-${messageData.sender_id?.substring(0, 8) + '</p>' || 'Unknown'}`;
|
||||||
const avatarText = displayName.substring(0, 2).toUpperCase();
|
const avatarText = displayName.substring(0, 2).toUpperCase();
|
||||||
|
|
||||||
const messageGroup = document.createElement('div');
|
const messageGroup = document.createElement('div');
|
||||||
@@ -1182,6 +1388,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add periodic session key backup for Tauri
|
||||||
|
if (isRunningInTauri) {
|
||||||
|
// Save session keys periodically in case of unexpected shutdown
|
||||||
|
setInterval(async () => {
|
||||||
|
if (sessionKey && currentRoom) {
|
||||||
|
try {
|
||||||
|
await keyStorage.storeSessionKey(currentRoom, sessionKey);
|
||||||
|
console.log("🔄 Periodic session key backup completed");
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("⚠️ Periodic session key backup failed:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 30000); // Every 30 seconds
|
||||||
|
|
||||||
|
// Note: Modern Tauri store auto-saves on app close
|
||||||
|
window.addEventListener('beforeunload', async () => {
|
||||||
|
console.log("🔄 App closing - Tauri store will auto-save");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Periodic connection health check
|
// Periodic connection health check
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (socket && isConnected) {
|
if (socket && isConnected) {
|
||||||
@@ -1217,7 +1443,7 @@ window.SecureChat = {
|
|||||||
hasKeyPair: !!keyPair,
|
hasKeyPair: !!keyPair,
|
||||||
keysReady,
|
keysReady,
|
||||||
isConnected,
|
isConnected,
|
||||||
isTauri
|
isTauri: isRunningInTauri
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1247,6 +1473,67 @@ if (isRunningInTauri) {
|
|||||||
return !!loaded;
|
return !!loaded;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
},
|
||||||
|
// Test session key persistence
|
||||||
|
testSessionKeyPersistence: async () => {
|
||||||
|
if (!sessionKey || !currentRoom) {
|
||||||
|
console.log("❌ No session key or room to test");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🧪 Testing session key persistence...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Store current session key
|
||||||
|
await keyStorage.storeSessionKey(currentRoom, sessionKey);
|
||||||
|
await keyStorage.forceSave();
|
||||||
|
|
||||||
|
// Try to load it back
|
||||||
|
const loaded = await keyStorage.loadSessionKey(currentRoom);
|
||||||
|
|
||||||
|
if (loaded) {
|
||||||
|
console.log("✅ Session key persistence test: SUCCESS");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.log("❌ Session key persistence test: FAILED");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Session key persistence test error:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Force session key request
|
||||||
|
forceRequestSessionKey: async () => {
|
||||||
|
if (!currentRoom || !keyPair) {
|
||||||
|
console.log("❌ Cannot request session key - missing room or keypair");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🔄 Forcing session key request...");
|
||||||
|
const publicKeyString = await exportPublicKey(keyPair.publicKey);
|
||||||
|
socket.emit('request_session_key', {
|
||||||
|
room_id: currentRoom,
|
||||||
|
public_key: publicKeyString
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Manual session key storage
|
||||||
|
forceStoreSessionKey: async () => {
|
||||||
|
if (!sessionKey || !currentRoom) {
|
||||||
|
console.log("❌ No session key or room to store");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await keyStorage.storeSessionKey(currentRoom, sessionKey);
|
||||||
|
console.log("✅ Manual session key storage completed");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Manual session key storage failed:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
737
src/rooms.js
Normal file
@@ -0,0 +1,737 @@
|
|||||||
|
let publicRoomsData = [];
|
||||||
|
let roomsRefreshInterval = null;
|
||||||
|
let currentSortBy = 'activity';
|
||||||
|
let currentMinUsers = 0;
|
||||||
|
let isSubscribedToRooms = false;
|
||||||
|
let isRoomsBrowserOpen = false;
|
||||||
|
let retryAttempts = 0;
|
||||||
|
const MAX_RETRY_ATTEMPTS = 3;
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const ROOMS_CONFIG = {
|
||||||
|
refreshInterval: 30000, // 30 seconds
|
||||||
|
maxRetries: 3,
|
||||||
|
retryDelay: 2000, // 2 seconds
|
||||||
|
socketTimeout: 25000, // 25 seconds to wait for socket
|
||||||
|
fallbackToHttp: true,
|
||||||
|
autoRefresh: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show the public rooms browser
|
||||||
|
function showPublicRoomsBrowser() {
|
||||||
|
console.log('Opening public rooms browser');
|
||||||
|
const browserElement = document.getElementById('publicRoomsBrowser');
|
||||||
|
if (!browserElement) {
|
||||||
|
console.error('Public rooms browser element not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
browserElement.style.display = 'flex';
|
||||||
|
isRoomsBrowserOpen = true;
|
||||||
|
retryAttempts = 0;
|
||||||
|
|
||||||
|
// Reset scroll position
|
||||||
|
const browserContent = document.getElementById('browserContent');
|
||||||
|
if (browserContent) {
|
||||||
|
browserContent.scrollTop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load rooms and subscribe to updates
|
||||||
|
loadPublicRoomsWebSocket();
|
||||||
|
subscribeToPublicRooms();
|
||||||
|
|
||||||
|
// Set up auto-refresh if enabled
|
||||||
|
if (ROOMS_CONFIG.autoRefresh && !roomsRefreshInterval) {
|
||||||
|
roomsRefreshInterval = setInterval(() => {
|
||||||
|
if (isRoomsBrowserOpen) {
|
||||||
|
console.log('Auto-refreshing public rooms');
|
||||||
|
loadPublicRoomsWebSocket();
|
||||||
|
}
|
||||||
|
}, ROOMS_CONFIG.refreshInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the public rooms browser
|
||||||
|
function closePublicRoomsBrowser() {
|
||||||
|
console.log('Closing public rooms browser');
|
||||||
|
const browserElement = document.getElementById('publicRoomsBrowser');
|
||||||
|
if (browserElement) {
|
||||||
|
browserElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
isRoomsBrowserOpen = false;
|
||||||
|
unsubscribeFromPublicRooms();
|
||||||
|
|
||||||
|
// Clear auto-refresh
|
||||||
|
if (roomsRefreshInterval) {
|
||||||
|
clearInterval(roomsRefreshInterval);
|
||||||
|
roomsRefreshInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to live public rooms updates
|
||||||
|
function subscribeToPublicRooms() {
|
||||||
|
if (isSocketAvailable() && !isSubscribedToRooms) {
|
||||||
|
try {
|
||||||
|
socket.emit('subscribe_public_rooms');
|
||||||
|
isSubscribedToRooms = true;
|
||||||
|
console.log('Subscribed to public rooms updates');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error subscribing to public rooms:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe from public rooms updates
|
||||||
|
function unsubscribeFromPublicRooms() {
|
||||||
|
if (isSocketAvailable() && isSubscribedToRooms) {
|
||||||
|
try {
|
||||||
|
socket.emit('unsubscribe_public_rooms');
|
||||||
|
isSubscribedToRooms = false;
|
||||||
|
console.log('Unsubscribed from public rooms updates');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error unsubscribing from public rooms:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if socket is available and connected
|
||||||
|
function isSocketAvailable() {
|
||||||
|
return typeof socket !== 'undefined' &&
|
||||||
|
socket !== null &&
|
||||||
|
socket.connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load public rooms via WebSocket with fallback
|
||||||
|
function loadPublicRoomsWebSocket() {
|
||||||
|
if (!isRoomsBrowserOpen) {
|
||||||
|
console.log('Rooms browser not open, skipping load');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
showLoadingState();
|
||||||
|
updateFiltersFromUI();
|
||||||
|
|
||||||
|
if (isSocketAvailable()) {
|
||||||
|
console.log('Loading rooms via WebSocket');
|
||||||
|
socket.emit('request_public_rooms', {
|
||||||
|
sort: currentSortBy,
|
||||||
|
min_users: currentMinUsers,
|
||||||
|
limit: 50
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set a timeout for WebSocket response
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.getElementById('roomsLoading')?.style.display !== 'none') {
|
||||||
|
console.warn('WebSocket timeout, falling back to HTTP');
|
||||||
|
loadPublicRoomsHTTP();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
} else {
|
||||||
|
console.log('WebSocket not available, using HTTP fallback');
|
||||||
|
loadPublicRoomsHTTP();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error requesting public rooms via WebSocket:', error);
|
||||||
|
loadPublicRoomsHTTP();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update filters from UI elements
|
||||||
|
function updateFiltersFromUI() {
|
||||||
|
const sortSelect = document.getElementById('roomsSortSelect');
|
||||||
|
const minUsersFilter = document.getElementById('minUsersFilter');
|
||||||
|
|
||||||
|
currentSortBy = sortSelect?.value || 'activity';
|
||||||
|
currentMinUsers = parseInt(minUsersFilter?.value || '0') || 0;
|
||||||
|
|
||||||
|
console.log(`Updated filters: sort=${currentSortBy}, minUsers=${currentMinUsers}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced HTTP fallback with retry logic
|
||||||
|
async function loadPublicRoomsHTTP() {
|
||||||
|
if (!isRoomsBrowserOpen) {
|
||||||
|
console.log('Rooms browser not open, skipping HTTP load');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
showLoadingState();
|
||||||
|
updateFiltersFromUI();
|
||||||
|
|
||||||
|
console.log(`Loading rooms via HTTP (attempt ${retryAttempts + 1}/${MAX_RETRY_ATTEMPTS})`);
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/rooms/public?sort=${encodeURIComponent(currentSortBy)}&min_users=${currentMinUsers}&limit=50`,
|
||||||
|
{ signal: controller.signal }
|
||||||
|
);
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data || !Array.isArray(data.rooms)) {
|
||||||
|
throw new Error('Invalid response format');
|
||||||
|
}
|
||||||
|
|
||||||
|
publicRoomsData = data.rooms;
|
||||||
|
console.log(`Loaded ${publicRoomsData.length} rooms via HTTP`);
|
||||||
|
|
||||||
|
// Load stats separately
|
||||||
|
await loadRoomStats();
|
||||||
|
|
||||||
|
displayRooms();
|
||||||
|
retryAttempts = 0; // Reset on success
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading public rooms via HTTP:', error);
|
||||||
|
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
console.error('Request timed out');
|
||||||
|
}
|
||||||
|
|
||||||
|
retryAttempts++;
|
||||||
|
|
||||||
|
if (retryAttempts < MAX_RETRY_ATTEMPTS) {
|
||||||
|
console.log(`Retrying in ${ROOMS_CONFIG.retryDelay}ms...`);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isRoomsBrowserOpen) {
|
||||||
|
loadPublicRoomsHTTP();
|
||||||
|
}
|
||||||
|
}, ROOMS_CONFIG.retryDelay);
|
||||||
|
} else {
|
||||||
|
showErrorState();
|
||||||
|
retryAttempts = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load room statistics
|
||||||
|
async function loadRoomStats() {
|
||||||
|
try {
|
||||||
|
const statsResponse = await fetch('/api/rooms/stats');
|
||||||
|
if (statsResponse.ok) {
|
||||||
|
const stats = await statsResponse.json();
|
||||||
|
updateStatsDisplay(stats);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load room stats:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh public rooms with user feedback
|
||||||
|
function refreshPublicRooms() {
|
||||||
|
console.log('Manual refresh requested');
|
||||||
|
retryAttempts = 0; // Reset retry counter on manual refresh
|
||||||
|
|
||||||
|
// Show brief loading indicator
|
||||||
|
const refreshBtn = document.getElementById('refreshPublicRoomsBtn');
|
||||||
|
const originalText = refreshBtn?.textContent;
|
||||||
|
|
||||||
|
if (refreshBtn) {
|
||||||
|
refreshBtn.textContent = 'Refreshing...';
|
||||||
|
refreshBtn.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPublicRoomsWebSocket();
|
||||||
|
|
||||||
|
// Reset button after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
if (refreshBtn) {
|
||||||
|
refreshBtn.textContent = originalText || 'Refresh';
|
||||||
|
refreshBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update stats display with enhanced formatting
|
||||||
|
function updateStatsDisplay(stats) {
|
||||||
|
const statsElement = document.getElementById('roomsStats');
|
||||||
|
if (!statsElement || !stats) return;
|
||||||
|
|
||||||
|
const publicRooms = stats.public_rooms || 0;
|
||||||
|
const totalUsers = stats.total_users || 0;
|
||||||
|
const lastUpdated = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
|
statsElement.innerHTML = `
|
||||||
|
<span>${publicRooms} public room${publicRooms !== 1 ? 's' : ''}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>${totalUsers} user${totalUsers !== 1 ? 's' : ''} online</span>
|
||||||
|
<span style="color: #666; font-size: 0.85em;">• Updated ${lastUpdated}</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced loading state
|
||||||
|
function showLoadingState() {
|
||||||
|
const loadingElement = document.getElementById('roomsLoading');
|
||||||
|
const listElement = document.getElementById('roomsList');
|
||||||
|
const emptyElement = document.getElementById('roomsEmpty');
|
||||||
|
|
||||||
|
if (loadingElement) {
|
||||||
|
loadingElement.style.display = 'flex';
|
||||||
|
loadingElement.innerHTML = `
|
||||||
|
<div style="display: flex; align-items: center; gap: 1rem; color: #888;">
|
||||||
|
<div class="loading-spinner" style="
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid #333;
|
||||||
|
border-top: 2px solid #00ff88;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
"></div>
|
||||||
|
<span>Loading public rooms...</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (listElement) listElement.style.display = 'none';
|
||||||
|
if (emptyElement) emptyElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced error state with retry options
|
||||||
|
function showErrorState() {
|
||||||
|
const loadingElement = document.getElementById('roomsLoading');
|
||||||
|
const listElement = document.getElementById('roomsList');
|
||||||
|
const emptyElement = document.getElementById('roomsEmpty');
|
||||||
|
|
||||||
|
if (loadingElement) loadingElement.style.display = 'none';
|
||||||
|
if (listElement) listElement.style.display = 'none';
|
||||||
|
|
||||||
|
if (emptyElement) {
|
||||||
|
emptyElement.style.display = 'flex';
|
||||||
|
emptyElement.innerHTML = `
|
||||||
|
<div style="text-align: center; max-width: 400px;">
|
||||||
|
<div style="font-size: 3rem; margin-bottom: 1rem;">⚠️</div>
|
||||||
|
<h3 style="margin: 0 0 0.5rem 0; color: #ffffff;">Failed to Load Rooms</h3>
|
||||||
|
<p style="margin: 0 0 1.5rem 0; color: #888; line-height: 1.5;">
|
||||||
|
Unable to connect to the server. This could be due to network issues or server maintenance.
|
||||||
|
</p>
|
||||||
|
<div style="display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap;">
|
||||||
|
<button class="btn" onclick="refreshPublicRooms()">Try Again</button>
|
||||||
|
<button class="btn btn-secondary" onclick="loadPublicRoomsHTTP()">Force HTTP</button>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top: 1rem; color: #666; font-size: 0.85em;">
|
||||||
|
Attempted ${retryAttempts}/${MAX_RETRY_ATTEMPTS} retries
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced room display with better error handling
|
||||||
|
function displayRooms() {
|
||||||
|
const roomsList = document.getElementById('roomsList');
|
||||||
|
const roomsLoading = document.getElementById('roomsLoading');
|
||||||
|
const roomsEmpty = document.getElementById('roomsEmpty');
|
||||||
|
|
||||||
|
if (roomsLoading) roomsLoading.style.display = 'none';
|
||||||
|
|
||||||
|
if (!Array.isArray(publicRoomsData) || publicRoomsData.length === 0) {
|
||||||
|
if (roomsEmpty) {
|
||||||
|
roomsEmpty.style.display = 'flex';
|
||||||
|
roomsEmpty.innerHTML = `
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 2.5rem; margin-bottom: 1rem;">🏠</div>
|
||||||
|
<h3 style="margin: 0 0 0.5rem 0; color: #ffffff;">No Public Rooms Found</h3>
|
||||||
|
<p style="margin: 0 0 1rem 0; color: #888;">
|
||||||
|
${currentMinUsers > 0 ? `Try reducing the minimum users filter (currently ${currentMinUsers})` : 'Check back later or create your own room'}
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-secondary" onclick="refreshPublicRooms()">Refresh</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (roomsList) roomsList.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roomsEmpty) roomsEmpty.style.display = 'none';
|
||||||
|
if (roomsList) {
|
||||||
|
roomsList.style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const roomsHtml = publicRoomsData.map(room => createRoomCard(room)).join('');
|
||||||
|
roomsList.innerHTML = roomsHtml;
|
||||||
|
console.log(`Displayed ${publicRoomsData.length} rooms`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering rooms:', error);
|
||||||
|
showErrorState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced room card with better data handling
|
||||||
|
function createRoomCard(room) {
|
||||||
|
if (!room || typeof room !== 'object') {
|
||||||
|
console.warn('Invalid room data:', room);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomId = sanitizeText(room.room_id || 'unknown');
|
||||||
|
const title = sanitizeText(room.title || room.room_id || 'Unnamed Room');
|
||||||
|
const description = sanitizeText(room.description || 'No description available');
|
||||||
|
const userCount = Math.max(0, parseInt(room.user_count) || 0);
|
||||||
|
const messageCount = Math.max(0, parseInt(room.message_count) || 0);
|
||||||
|
const minutesAgo = Math.max(0, parseInt(room.minutes_since_activity) || 0);
|
||||||
|
|
||||||
|
const activityClass = getActivityClass(minutesAgo);
|
||||||
|
const timeAgo = formatTimeAgo(minutesAgo);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="room-card"
|
||||||
|
onclick="joinPublicRoom('${escapeHtml(roomId)}')"
|
||||||
|
style="
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background: #2a2a2a;
|
||||||
|
"
|
||||||
|
onmouseover="this.style.background='#333'; this.style.borderColor='#555';"
|
||||||
|
onmouseout="this.style.background='#2a2a2a'; this.style.borderColor='#333';">
|
||||||
|
|
||||||
|
<div class="room-title" style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
">
|
||||||
|
${title}
|
||||||
|
<span class="activity-indicator ${activityClass}" style="
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="room-description" style="
|
||||||
|
color: #b0b0b0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
">${description}</div>
|
||||||
|
|
||||||
|
<div class="room-stats" style="
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
">
|
||||||
|
<div class="room-stat" style="display: flex; align-items: center; gap: 0.25rem;">
|
||||||
|
<span>👥</span>
|
||||||
|
<span>${userCount} user${userCount !== 1 ? 's' : ''}</span>
|
||||||
|
</div>
|
||||||
|
<div class="room-stat" style="display: flex; align-items: center; gap: 0.25rem;">
|
||||||
|
<span>💬</span>
|
||||||
|
<span>${messageCount} message${messageCount !== 1 ? 's' : ''}</span>
|
||||||
|
</div>
|
||||||
|
<div class="room-stat" style="display: flex; align-items: center; gap: 0.25rem;">
|
||||||
|
<span>🕐</span>
|
||||||
|
<span>Active ${timeAgo}</span>
|
||||||
|
</div>
|
||||||
|
<div class="room-stat" style="display: flex; align-items: center; gap: 0.25rem;">
|
||||||
|
<span>🆔</span>
|
||||||
|
<span style="font-family: monospace; font-size: 0.8em;">${roomId}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced activity classification
|
||||||
|
function getActivityClass(minutesAgo) {
|
||||||
|
if (minutesAgo <= 5) return 'activity-active';
|
||||||
|
if (minutesAgo <= 30) return 'activity-recent';
|
||||||
|
if (minutesAgo <= 120) return 'activity-moderate';
|
||||||
|
return 'activity-old';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced time formatting
|
||||||
|
function formatTimeAgo(minutes) {
|
||||||
|
if (minutes < 1) return 'just now';
|
||||||
|
if (minutes < 60) return `${Math.floor(minutes)}m ago`;
|
||||||
|
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
if (hours < 24) return `${hours}h ago`;
|
||||||
|
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
if (days < 7) return `${days}d ago`;
|
||||||
|
|
||||||
|
const weeks = Math.floor(days / 7);
|
||||||
|
return `${weeks}w ago`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced text sanitization
|
||||||
|
function sanitizeText(text) {
|
||||||
|
if (typeof text !== 'string') return '';
|
||||||
|
return text.trim().substring(0, 200); // Limit length and trim
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced HTML escaping
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = String(text);
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced room joining with validation
|
||||||
|
function joinPublicRoom(roomId) {
|
||||||
|
if (!roomId || typeof roomId !== 'string') {
|
||||||
|
console.error('Invalid room ID provided:', roomId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate room ID format
|
||||||
|
const roomIdPattern = /^[a-zA-Z0-9\-_]{1,32}$/;
|
||||||
|
if (!roomIdPattern.test(roomId)) {
|
||||||
|
console.error('Invalid room ID format:', roomId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Joining public room: ${roomId}`);
|
||||||
|
|
||||||
|
// Close the browser
|
||||||
|
closePublicRoomsBrowser();
|
||||||
|
|
||||||
|
// Fill in the room input
|
||||||
|
const roomInput = document.getElementById('roomInput');
|
||||||
|
if (roomInput) {
|
||||||
|
roomInput.value = roomId;
|
||||||
|
roomInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear password field for public rooms
|
||||||
|
const roomPasswordInput = document.getElementById('roomPasswordInput');
|
||||||
|
if (roomPasswordInput) {
|
||||||
|
roomPasswordInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger join room functionality with delay to ensure UI updates
|
||||||
|
setTimeout(() => {
|
||||||
|
const joinButton = document.getElementById('joinRoomBtn');
|
||||||
|
if (joinButton) {
|
||||||
|
joinButton.click();
|
||||||
|
} else {
|
||||||
|
console.error('Join button not found');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced WebSocket handlers
|
||||||
|
function setupPublicRoomsWebSocketHandlers() {
|
||||||
|
if (!isSocketAvailable()) {
|
||||||
|
console.log('Socket not available, WebSocket handlers not attached');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle public rooms data response
|
||||||
|
socket.on('public_rooms_data', function(data) {
|
||||||
|
console.log('Received public rooms data via WebSocket:', data);
|
||||||
|
|
||||||
|
if (data && Array.isArray(data.rooms)) {
|
||||||
|
publicRoomsData = data.rooms;
|
||||||
|
} else {
|
||||||
|
console.warn('Invalid public rooms data format');
|
||||||
|
publicRoomsData = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data.stats) {
|
||||||
|
updateStatsDisplay(data.stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRoomsBrowserOpen) {
|
||||||
|
displayRooms();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle live updates
|
||||||
|
socket.on('public_rooms_updated', function(data) {
|
||||||
|
console.log('Received live public rooms update:', data);
|
||||||
|
|
||||||
|
if (isRoomsBrowserOpen && isSubscribedToRooms) {
|
||||||
|
if (data && Array.isArray(data.rooms)) {
|
||||||
|
publicRoomsData = data.rooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data.stats) {
|
||||||
|
updateStatsDisplay(data.stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
displayRooms();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle WebSocket errors
|
||||||
|
socket.on('public_rooms_error', function(data) {
|
||||||
|
console.error('Public rooms WebSocket error:', data);
|
||||||
|
if (isRoomsBrowserOpen) {
|
||||||
|
showErrorState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle connection events
|
||||||
|
socket.on('connect', function() {
|
||||||
|
console.log('Socket connected, resubscribing if browser is open');
|
||||||
|
if (isRoomsBrowserOpen && !isSubscribedToRooms) {
|
||||||
|
subscribeToPublicRooms();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', function() {
|
||||||
|
console.log('Socket disconnected');
|
||||||
|
isSubscribedToRooms = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Enhanced public rooms WebSocket handlers attached');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced socket initialization
|
||||||
|
function waitForSocketAndSetupHandlers(retryCount = 0) {
|
||||||
|
if (isSocketAvailable()) {
|
||||||
|
setupPublicRoomsWebSocketHandlers();
|
||||||
|
console.log('WebSocket handlers setup successfully');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryCount > 100) { // 100 * 250ms = 25 seconds max wait
|
||||||
|
console.warn('Socket failed to initialize after 25 seconds, using HTTP-only mode');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryCount % 20 === 0) { // Log every 5 seconds
|
||||||
|
console.log(`Waiting for socket... (attempt ${retryCount + 1})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => waitForSocketAndSetupHandlers(retryCount + 1), 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced event listeners with better error handling
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('Setting up public rooms browser event listeners');
|
||||||
|
|
||||||
|
// Browse button (this was missing!)
|
||||||
|
const browseButton = document.getElementById('browsePublicRoomsBtn');
|
||||||
|
if (browseButton) {
|
||||||
|
browseButton.addEventListener('click', showPublicRoomsBrowser);
|
||||||
|
console.log('Browse public rooms button listener attached');
|
||||||
|
} else {
|
||||||
|
console.warn('Browse public rooms button not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
const closeButton = document.getElementById('closePublicRoomsBrowserBtn');
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.addEventListener('click', closePublicRoomsBrowser);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort selector
|
||||||
|
const sortSelect = document.getElementById('roomsSortSelect');
|
||||||
|
if (sortSelect) {
|
||||||
|
sortSelect.addEventListener('change', refreshPublicRooms);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min users filter
|
||||||
|
const minUsersFilter = document.getElementById('minUsersFilter');
|
||||||
|
if (minUsersFilter) {
|
||||||
|
minUsersFilter.addEventListener('change', refreshPublicRooms);
|
||||||
|
minUsersFilter.addEventListener('input', debounce(refreshPublicRooms, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh button
|
||||||
|
const refreshButton = document.getElementById('refreshPublicRoomsBtn');
|
||||||
|
if (refreshButton) {
|
||||||
|
refreshButton.addEventListener('click', refreshPublicRooms);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backdrop click handler
|
||||||
|
const browserElement = document.getElementById('publicRoomsBrowser');
|
||||||
|
if (browserElement) {
|
||||||
|
browserElement.addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) {
|
||||||
|
closePublicRoomsBrowser();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (!isRoomsBrowserOpen) return;
|
||||||
|
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
closePublicRoomsBrowser();
|
||||||
|
} else if (e.key === 'F5' || (e.ctrlKey && e.key === 'r')) {
|
||||||
|
e.preventDefault();
|
||||||
|
refreshPublicRooms();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start WebSocket setup
|
||||||
|
waitForSocketAndSetupHandlers();
|
||||||
|
|
||||||
|
console.log('Public rooms browser setup complete');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Utility: Debounce function
|
||||||
|
function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func.apply(this, args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup on page unload
|
||||||
|
window.addEventListener('beforeunload', function() {
|
||||||
|
if (isSubscribedToRooms) {
|
||||||
|
unsubscribeFromPublicRooms();
|
||||||
|
}
|
||||||
|
if (roomsRefreshInterval) {
|
||||||
|
clearInterval(roomsRefreshInterval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add CSS for activity indicators if not already present
|
||||||
|
if (!document.getElementById('roomsActivityStyles')) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'roomsActivityStyles';
|
||||||
|
style.textContent = `
|
||||||
|
.activity-active {
|
||||||
|
background-color: #00ff88 !important;
|
||||||
|
box-shadow: 0 0 6px rgba(0, 255, 136, 0.6);
|
||||||
|
}
|
||||||
|
.activity-recent {
|
||||||
|
background-color: #ffaa00 !important;
|
||||||
|
}
|
||||||
|
.activity-moderate {
|
||||||
|
background-color: #888 !important;
|
||||||
|
}
|
||||||
|
.activity-old {
|
||||||
|
background-color: #444 !important;
|
||||||
|
}
|
||||||
|
.room-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
262
src/rss.js
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
async function loadRSS() {
|
||||||
|
const container = document.getElementById("rss-feed");
|
||||||
|
const showLegacyContainer = document.getElementById("show-legacy-container");
|
||||||
|
const showLegacyBtn = document.getElementById("show-legacy-btn");
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
console.error("RSS feed container not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
container.innerHTML = "<p>Loading release notes...</p>";
|
||||||
|
|
||||||
|
const rssUrl = "https://rattatwinko.servecounterstrike.com/gitea/rattatwinko/bytechat-desktop/releases.rss";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try direct fetch first
|
||||||
|
let response;
|
||||||
|
let xmlText;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await fetch(rssUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/rss+xml, application/xml, text/xml, */*',
|
||||||
|
'Cache-Control': 'no-cache'
|
||||||
|
},
|
||||||
|
mode: 'cors'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
xmlText = await response.text();
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (corsError) {
|
||||||
|
console.warn("Direct fetch failed, trying proxy...", corsError);
|
||||||
|
|
||||||
|
// Try CORS proxy
|
||||||
|
const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(rssUrl)}`;
|
||||||
|
const proxyResponse = await fetch(proxyUrl);
|
||||||
|
|
||||||
|
if (!proxyResponse.ok) {
|
||||||
|
throw new Error(`Proxy failed: ${proxyResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyData = await proxyResponse.json();
|
||||||
|
if (!proxyData.contents) {
|
||||||
|
throw new Error("No content from proxy");
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlText = proxyData.contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the XML
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const xml = parser.parseFromString(xmlText, "application/xml");
|
||||||
|
|
||||||
|
// Check for parsing errors
|
||||||
|
const parserError = xml.querySelector("parsererror");
|
||||||
|
if (parserError) {
|
||||||
|
throw new Error("XML parsing failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = parseRSSItems(xml);
|
||||||
|
displayRSSItems(items, container, showLegacyContainer, showLegacyBtn);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load RSS:", err);
|
||||||
|
|
||||||
|
// Show error with retry and direct link
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="padding: 1rem; background: rgba(255, 100, 100, 0.1); border: 1px solid rgba(255, 100, 100, 0.3); border-radius: 6px; text-align: center;">
|
||||||
|
<p style="margin: 0 0 0.5rem 0;"><strong>Unable to load release notes</strong></p>
|
||||||
|
<p style="font-size: 0.9em; margin: 0.5rem 0; color: #ccc;">
|
||||||
|
Network issue detected.
|
||||||
|
</p>
|
||||||
|
<div style="display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; margin-top: 1rem;">
|
||||||
|
<button onclick="loadRSS()" style="padding: 0.4rem 0.8rem; background: #00ff88; color: black; border: none; border-radius: 4px; cursor: pointer; font-weight: 500;">
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
<a href="${rssUrl}" target="_blank" rel="noopener" style="padding: 0.4rem 0.8rem; background: #333; color: #00ff88; text-decoration: none; border-radius: 4px; display: inline-block;">
|
||||||
|
View Direct
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRSSItems(xml) {
|
||||||
|
console.log("Parsing RSS XML...");
|
||||||
|
|
||||||
|
const items = Array.from(xml.querySelectorAll("item")).map(item => {
|
||||||
|
const title = item.querySelector("title")?.textContent?.trim() || "Untitled Release";
|
||||||
|
const link = item.querySelector("link")?.textContent?.trim() || "#";
|
||||||
|
const pubDate = item.querySelector("pubDate")?.textContent?.trim() || "";
|
||||||
|
const author = item.querySelector("author")?.textContent?.trim() || "Unknown";
|
||||||
|
const description = item.querySelector("description")?.textContent?.trim() || "";
|
||||||
|
|
||||||
|
// Extract content from CDATA if available
|
||||||
|
const contentEncoded = item.querySelector("content\\:encoded");
|
||||||
|
let content = "";
|
||||||
|
if (contentEncoded) {
|
||||||
|
content = contentEncoded.textContent.trim();
|
||||||
|
// Remove HTML tags for a clean preview
|
||||||
|
content = content.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
|
// Limit to first 150 characters
|
||||||
|
if (content.length > 150) {
|
||||||
|
content = content.substring(0, 150) + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Parsed item:", { title, pubDate, author });
|
||||||
|
return { title, link, pubDate, author, description, content };
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${items.length} releases`);
|
||||||
|
|
||||||
|
// Sort by date (newest first)
|
||||||
|
return items.sort((a, b) => {
|
||||||
|
if (!a.pubDate || !b.pubDate) return 0;
|
||||||
|
return new Date(b.pubDate) - new Date(a.pubDate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayRSSItems(items, container, showLegacyContainer, showLegacyBtn) {
|
||||||
|
if (!items.length) {
|
||||||
|
container.innerHTML = "<p style='text-align: center; color: #888;'>No releases found.</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Displaying items:", items);
|
||||||
|
|
||||||
|
const visibleItems = 2;
|
||||||
|
const hasMoreItems = items.length > visibleItems;
|
||||||
|
|
||||||
|
let html = "<div class='release-list' style='display: flex; flex-direction: column; gap: 1rem;'>";
|
||||||
|
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
const isHidden = index >= visibleItems;
|
||||||
|
|
||||||
|
// Format the date nicely
|
||||||
|
let formattedDate = "";
|
||||||
|
if (item.pubDate) {
|
||||||
|
try {
|
||||||
|
const date = new Date(item.pubDate);
|
||||||
|
formattedDate = date.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
formattedDate = item.pubDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="release-item ${isHidden ? 'legacy-release' : ''}"
|
||||||
|
style="
|
||||||
|
${isHidden ? 'display: none;' : ''}
|
||||||
|
padding: 1rem;
|
||||||
|
background: linear-gradient(135deg, rgba(0, 255, 136, 0.05) 0%, rgba(0, 255, 136, 0.02) 100%);
|
||||||
|
border: 1px solid rgba(0, 255, 136, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #00ff88;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
"
|
||||||
|
onmouseover="this.style.background='linear-gradient(135deg, rgba(0, 255, 136, 0.08) 0%, rgba(0, 255, 136, 0.04) 100%)'; this.style.transform='translateY(-1px)'"
|
||||||
|
onmouseout="this.style.background='linear-gradient(135deg, rgba(0, 255, 136, 0.05) 0%, rgba(0, 255, 136, 0.02) 100%)'; this.style.transform='translateY(0px)'">
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem; flex-wrap: wrap; gap: 0.5rem;">
|
||||||
|
<h4 style="margin: 0; color: #00ff88; font-size: 1.1rem; font-weight: 600;">
|
||||||
|
<a href="${item.link}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
style="color: inherit; text-decoration: none;"
|
||||||
|
onmouseover="this.style.textDecoration='underline'"
|
||||||
|
onmouseout="this.style.textDecoration='none'">
|
||||||
|
${item.title}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
${formattedDate ? `
|
||||||
|
<span style="color: #888; font-size: 0.85rem; white-space: nowrap;">
|
||||||
|
${formattedDate}
|
||||||
|
</span>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${item.author !== 'Unknown' ? `
|
||||||
|
<div style="color: #666; font-size: 0.8rem; margin-bottom: 0.5rem;">
|
||||||
|
by ${item.author}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${item.content ? `
|
||||||
|
<div style="color: #ccc; font-size: 0.9rem; line-height: 1.4; margin-top: 0.5rem;">
|
||||||
|
${item.content}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += "</div>";
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
// Setup legacy releases toggle
|
||||||
|
if (hasMoreItems && showLegacyContainer && showLegacyBtn) {
|
||||||
|
showLegacyContainer.style.display = 'block';
|
||||||
|
|
||||||
|
// Remove existing listeners by cloning
|
||||||
|
const newBtn = showLegacyBtn.cloneNode(true);
|
||||||
|
showLegacyBtn.parentNode.replaceChild(newBtn, showLegacyBtn);
|
||||||
|
|
||||||
|
let showingLegacy = false;
|
||||||
|
newBtn.addEventListener('click', function() {
|
||||||
|
showingLegacy = !showingLegacy;
|
||||||
|
const legacyItems = container.querySelectorAll('.legacy-release');
|
||||||
|
|
||||||
|
legacyItems.forEach(item => {
|
||||||
|
if (showingLegacy) {
|
||||||
|
item.style.display = 'block';
|
||||||
|
item.style.animation = 'fadeIn 0.3s ease-in';
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newBtn.textContent = showingLegacy ?
|
||||||
|
'Hide Legacy Releases' : `Show ${items.length - visibleItems} More Releases`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update button text to show count
|
||||||
|
newBtn.textContent = `Show ${items.length - visibleItems} More Releases`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("RSS items displayed successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some CSS for animations
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
// Initialize when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', loadRSS);
|
||||||
|
} else {
|
||||||
|
// DOM is already loaded
|
||||||
|
loadRSS();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make loadRSS available globally for retry button
|
||||||
|
window.loadRSS = loadRSS;
|
||||||
@@ -10,15 +10,25 @@ html, body {
|
|||||||
overflow: hidden; /* prevents double scrollbars */
|
overflow: hidden; /* prevents double scrollbars */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: SevenSegment;
|
||||||
|
src: url(assets/7Segment.ttf);
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||||
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%);
|
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
padding-left: env(safe-area-inset-left);
|
||||||
|
padding-right: env(safe-area-inset-right);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main container */
|
/* Main container */
|
||||||
|
/*
|
||||||
.chat-container {
|
.chat-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -26,6 +36,7 @@ body {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
.chat-header {
|
.chat-header {
|
||||||
@@ -57,7 +68,6 @@ body {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Welcome screen */
|
|
||||||
.welcome-screen {
|
.welcome-screen {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -66,8 +76,11 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
min-height: 100vh; /* Ensure it takes full viewport height */
|
||||||
|
box-sizing: border-box; /* Include padding in height calculation */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.welcome-title {
|
.welcome-title {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|||||||