Compare commits
No commits in common. "b620393eae3612fb7891a35a9829961a5558bd1f" and "5a032a45d9c8c360caa78be3f5749170c81ddeba" have entirely different histories.
b620393eae
...
5a032a45d9
@ -1,49 +1,134 @@
|
|||||||
# Forge Architecture
|
# Forge Architecture & Data Flow Diagram
|
||||||
|
|
||||||
## Runtime Flow
|
## 🏗️ **System Architecture Overview**
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph ForgeSystem [FORGE SYSTEM]
|
||||||
|
subgraph Clients [Clients #40;Read-Only#41;]
|
||||||
|
ClientA[CLIENT A]
|
||||||
|
ClientB[CLIENT B]
|
||||||
|
ClientN[CLIENT N]
|
||||||
|
|
||||||
|
subgraph OptimisticCache [Optimistic Cache]
|
||||||
|
ActorObj[Actor Object<br/>- loadout<br/>- position<br/>- stats]
|
||||||
|
end
|
||||||
|
|
||||||
|
ClientA --- OptimisticCache
|
||||||
|
ClientB --- OptimisticCache
|
||||||
|
ClientN --- OptimisticCache
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Server [ArmA 3 SERVER #40;Hot Cache#41;]
|
||||||
|
Registry["GVAR(Registry)<br/>In-Memory HashMap<br/>UID -> {loadout, position, stats...}"]
|
||||||
|
SessionMgmt[Session Management<br/>- Token Generation<br/>- UID Resolution<br/>- Player State]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Rust [EXTENSION #40;Cold Storage#41;]
|
||||||
|
ConnPool["Connection Pool<br/>(bb8-redis)<br/>2-10 connections"]
|
||||||
|
RedisOps[Redis Operations<br/>- actor_get/set/update<br/>- Async I/O]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Redis [DATABASE #40;Saved to Disc#41;]
|
||||||
|
ActorDataStore[Actor Data Store<br/>actor:UID -> JSON]
|
||||||
|
Modules[Additional Modules<br/>garage, locker, bank, org]
|
||||||
|
end
|
||||||
|
|
||||||
|
Clients -->|Event Driven<br/>#40;CBA A3 Events#41;| Server
|
||||||
|
Server -->|Extension Calls<br/>#40;Rust FFI#41;| Rust
|
||||||
|
Rust -->|Redis Protocol<br/>#40;bb8-redis#41;| Redis
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 **Data Flow Sequence**
|
||||||
|
|
||||||
|
### **1. Player Connection & Initial Data Load**
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client
|
||||||
|
participant Server as Server (Hot Cache)
|
||||||
|
participant Extension as Extension (Cold Storage)
|
||||||
|
participant Redis as Redis (Database)
|
||||||
|
|
||||||
|
Note over Client, Redis: 1. Player Connection & Initial Data Load
|
||||||
|
|
||||||
|
Client->>Server: 1. Connect
|
||||||
|
Client->>Server: 2. Request Actor Data
|
||||||
|
Server->>Server: 3. Check Cache (Cache Miss)
|
||||||
|
Server->>Extension: 4. Extension Call
|
||||||
|
Extension->>Redis: 5. Redis Query
|
||||||
|
Redis-->>Extension: 6. JSON Data
|
||||||
|
Extension-->>Server: 7. Actor Data
|
||||||
|
Server->>Server: 8. Store in Hot Cache
|
||||||
|
Server-->>Client: 9. Secure Response
|
||||||
|
Client->>Client: 10. Update Local Cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Subsequent Data Access (Cache Hit)**
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client
|
||||||
|
participant Server as Server (Hot Cache)
|
||||||
|
participant Extension as Extension (Cold Storage)
|
||||||
|
participant Redis as Redis (Database)
|
||||||
|
|
||||||
|
Note over Client, Redis: 2. Subsequent Data Access (Cache Hit)
|
||||||
|
|
||||||
|
Client->>Server: 1. Request Actor Data
|
||||||
|
Server->>Server: 2. Check Cache (Cache Hit!)
|
||||||
|
Server-->>Client: 3. Instant Response
|
||||||
|
Client->>Client: 4. Update Local Cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### **3. Data Update (Write-Through)**
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client
|
||||||
|
participant Server as Server (Hot Cache)
|
||||||
|
participant Extension as Extension (Cold Storage)
|
||||||
|
participant Redis as Redis (Database)
|
||||||
|
|
||||||
|
Note over Client, Redis: 3. Data Update (Write-Through)
|
||||||
|
|
||||||
|
Client->>Server: 1. Action (Move, etc)
|
||||||
|
Server->>Server: 2. Validate & Update Cache
|
||||||
|
Server->>Extension: 3. Persist to Database
|
||||||
|
Extension->>Redis: 4. Redis Update
|
||||||
|
Redis-->>Extension: 5. Confirmation
|
||||||
|
Extension-->>Server: 6. Success
|
||||||
|
Server-->>Client: 7. Sync to All Clients
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 **Performance Characteristics**
|
||||||
|
|
||||||
|
### **Access Times**
|
||||||
|
|
||||||
|
- **Hot Cache (Server)**: `< 1ms` (HashMap lookup)
|
||||||
|
- **Cold Storage (Redis)**: `1-5ms` (Network + Redis)
|
||||||
|
- **Client Cache**: `< 0.1ms` (Local object access)
|
||||||
|
|
||||||
|
### **Cache Hit Ratios**
|
||||||
|
|
||||||
|
- **Hot Cache**: `~95%` (Active players)
|
||||||
|
- **Cold Storage**: `~5%` (New connections, cache misses)
|
||||||
|
|
||||||
|
### **Memory Usage**
|
||||||
|
|
||||||
|
- **Server Registry**: `~1KB per active player`
|
||||||
|
- **Client Cache**: `~500B per player object`
|
||||||
|
- **Redis**: `~2KB per player (persistent)`
|
||||||
|
|
||||||
|
## 🔒 **Security & Session Management**
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
Client[Arma Client Addons] --> Server[Arma Server Addons]
|
subgraph SessionMgmt [SERVER-SIDE #40;Session MGT#41;]
|
||||||
Server --> Bridge[Extension Bridge]
|
Conn[Player Connection] --> Token[Session Token Generation<br/>#40;Generated on server#41;]
|
||||||
Bridge --> Extension[Rust arma-rs Extension]
|
Token --> UID[UID Resolution<br/>#40;Steam UID mapping#41;]
|
||||||
Extension --> Services[Service Layer]
|
UID --> State[Player State Tracking<br/>#40;Tracked in Registry#41;]
|
||||||
Services --> Repositories[Repository Traits]
|
State --> Access[Data Access Authorized<br/>#40;Authorized via session#41;]
|
||||||
Repositories --> Surreal[(SurrealDB)]
|
end
|
||||||
```
|
|
||||||
|
|
||||||
## Persistence Startup
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Arma as Arma Server
|
|
||||||
participant Ext as Forge Extension
|
|
||||||
participant Db as SurrealDB
|
|
||||||
|
|
||||||
Arma->>Ext: init
|
|
||||||
Ext->>Db: connect
|
|
||||||
Ext->>Db: apply schema modules
|
|
||||||
Db-->>Ext: ready
|
|
||||||
Arma->>Ext: status
|
|
||||||
Ext-->>Arma: connected
|
|
||||||
```
|
|
||||||
|
|
||||||
## Data Access
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant SQF as SQF Addon
|
|
||||||
participant Ext as Extension Command
|
|
||||||
participant Service as Service
|
|
||||||
participant Repo as Repository
|
|
||||||
participant Db as SurrealDB
|
|
||||||
|
|
||||||
SQF->>Ext: domain command
|
|
||||||
Ext->>Service: validate and execute
|
|
||||||
Service->>Repo: repository call
|
|
||||||
Repo->>Db: query/upsert/delete
|
|
||||||
Db-->>Repo: result
|
|
||||||
Repo-->>Service: domain model
|
|
||||||
Service-->>Ext: response
|
|
||||||
Ext-->>SQF: serialized result
|
|
||||||
```
|
```
|
||||||
|
|||||||
@ -12,6 +12,7 @@ resolver = "3"
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
arma-rs = { version = "1.11.15", features = ["chrono", "serde_json", "uuid"] }
|
arma-rs = { version = "1.11.15", features = ["chrono", "serde_json", "uuid"] }
|
||||||
chrono = "0.4.42"
|
chrono = "0.4.42"
|
||||||
|
redis = "1.0.0-rc.1"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
tokio = { version = "1.47.1", features = ["full"] }
|
tokio = { version = "1.47.1", features = ["full"] }
|
||||||
|
|||||||
345
README.md
@ -1,54 +1,313 @@
|
|||||||
# Forge
|
# Forge Framework
|
||||||
|
|
||||||
Forge is a framework for Arma 3 persistent game servers. It combines SQF
|
**Forge** is a high-performance, production-ready framework for Arma 3 persistent game servers, built with Rust and Redis for optimal performance and reliability.
|
||||||
addons, a Rust `arma-rs` extension, shared service crates, and web-based client
|
|
||||||
interfaces for player data, organizations, banking, garages, lockers, phones,
|
|
||||||
CAD, stores, and task workflows.
|
|
||||||
|
|
||||||
## Storage
|
## Overview
|
||||||
|
|
||||||
Durable persistence is backed by SurrealDB. The server extension loads schema
|
Forge provides a complete solution for managing persistent player data, organizations, and game state in Arma 3 multiplayer environments. It combines the performance of Rust with the flexibility of Redis to deliver sub-millisecond response times while maintaining data consistency across server restarts.
|
||||||
modules at startup and routes domain repositories through the SurrealDB client.
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- **🚀 High Performance**: Sub-millisecond data access through intelligent caching
|
||||||
|
- **🔒 Data Integrity**: Strict validation and type safety at every layer
|
||||||
|
- **🏗️ Clean Architecture**: Layered design following SOLID principles
|
||||||
|
- **📦 Modular Design**: Easy to extend with new entities and features
|
||||||
|
- **🔄 Real-time Sync**: Automatic state synchronization across all clients
|
||||||
|
- **💾 Persistent Storage**: Redis-backed storage with automatic failover
|
||||||
|
- **🧪 Testable**: Mock-friendly architecture for comprehensive testing
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Forge follows a **layered architecture** pattern:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
Extension[Extension Layer<br/>ArmA 3 Interface <---> Rust]
|
||||||
|
Services[Services Layer<br/>#40;Business Logic#41;]
|
||||||
|
Repositories[Repositories Layer<br/>#40;Data Persistence#41;]
|
||||||
|
Models[Models Layer<br/>#40;Data Structures & Validation#41;]
|
||||||
|
|
||||||
|
Extension --> Services
|
||||||
|
Services --> Repositories
|
||||||
|
Repositories --> Models
|
||||||
|
```
|
||||||
|
|
||||||
|
**Communication Flow**:
|
||||||
|
|
||||||
|
- **Clients** → Use events (`CBA_Events`) to communicate with server
|
||||||
|
- **Server** → Calls Rust extension via `callExtension`
|
||||||
|
- **Extension** → Manages Redis connection pool and data operations
|
||||||
|
|
||||||
|
For detailed architecture information, see [Diagram](Architecture_Diagram.md).
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
forge/
|
||||||
|
├── arma/
|
||||||
|
│ ├── client/ # Client-side SQF mod
|
||||||
|
│ │ ├── addons/
|
||||||
|
│ │ │ ├── main/ # Core initialization & config
|
||||||
|
│ │ │ ├── common/ # Shared utilities & helpers
|
||||||
|
│ │ │ ├── actor/ # Actor/player UI, class & events
|
||||||
|
│ │ │ ├── org/ # Organization UI, class & events
|
||||||
|
│ │ │ └── bank/ # Banking UI, class & events
|
||||||
|
│ │ ├── include/ # Header files
|
||||||
|
│ │ └── tools/ # Build tools
|
||||||
|
│ ├── server/
|
||||||
|
│ │ ├── addons/
|
||||||
|
│ │ │ ├── main/ # Core initialization & config
|
||||||
|
│ │ │ ├── common/ # Shared utilities & helpers
|
||||||
|
│ │ │ ├── actor/ # Actor/player Registry, Store & events
|
||||||
|
│ │ │ └── org/ # Organization Registry, Store & events
|
||||||
|
│ │ ├── include/ # Header files
|
||||||
|
│ │ ├── tools/ # Build tools
|
||||||
|
│ │ └── extension/ # Rust extension (Arma 3 interface)
|
||||||
|
│ │ ├── src/
|
||||||
|
│ │ │ ├── actor.rs # Actor/player commands
|
||||||
|
│ │ │ ├── org.rs # Organization commands
|
||||||
|
│ │ │ ├── redis/ # Redis operations module
|
||||||
|
│ │ │ └── adapters/ # Repository adapters
|
||||||
|
│ │ └── README.md
|
||||||
|
├── lib/
|
||||||
|
│ ├── models/ # Data structures & validation
|
||||||
|
│ ├── repositories/ # Data persistence layer
|
||||||
|
│ ├── services/ # Business logic layer
|
||||||
|
│ ├── shared/ # Common utilities & traits
|
||||||
|
│ └── README.md
|
||||||
|
└── FORGE_Architecture_Diagram.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Rust 1.70+ with `cargo`
|
||||||
|
- Redis 6.0+
|
||||||
|
- HEMTT
|
||||||
|
|
||||||
|
1. Clone the repository from Gitea
|
||||||
|
2. Install HEMTT
|
||||||
|
The latest version of HEMTT can be installed by running:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
winget install hemtt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Coding Guidelines
|
||||||
|
|
||||||
|
This mod follows the same coding guidelines as the ACE3 mod, which can be found [here](https://ace3.acemod.org/wiki/development/coding-guidelines).
|
||||||
|
|
||||||
|
### Building the Extension
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build for release
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# The compiled extension will be at:
|
||||||
|
# target/release/forge_server.dll (Windows)
|
||||||
|
# target/release/forge_server.so (Linux)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Create `@forge_server/config.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[surreal]
|
[redis]
|
||||||
endpoint = "127.0.0.1:8000"
|
host = "127.0.0.1"
|
||||||
namespace = "forge"
|
port = 6379
|
||||||
database = "main"
|
password = "" # Optional
|
||||||
username = "root"
|
max_connections = 10
|
||||||
password = "root"
|
min_connections = 2
|
||||||
connect_timeout_ms = 5000
|
idle_timeout = 300
|
||||||
```
|
```
|
||||||
|
|
||||||
## Workspace
|
### SQF Usage
|
||||||
|
|
||||||
```text
|
|
||||||
arma/
|
|
||||||
client/ Client-side addons and browser UIs
|
|
||||||
server/ Server-side addons and extension crate
|
|
||||||
bin/
|
|
||||||
icom/ Interprocess communication helper
|
|
||||||
lib/
|
|
||||||
models/ Shared domain models
|
|
||||||
repositories/ Repository traits and in-memory test stores
|
|
||||||
services/ Domain business logic
|
|
||||||
shared/ Cross-crate helpers
|
|
||||||
tools/ Web UI build tooling
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Commands
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cargo test
|
|
||||||
npm run build:webui
|
|
||||||
.\build-arma.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
## Extension Status
|
|
||||||
|
|
||||||
```sqf
|
```sqf
|
||||||
"forge_server" callExtension ["status", []];
|
// Create an actor
|
||||||
"forge_server" callExtension ["surreal:status", []];
|
private _data = createHashMapFromArray [
|
||||||
|
["name", "John Doe"],
|
||||||
|
["bank", 1000],
|
||||||
|
["level", 1]
|
||||||
|
];
|
||||||
|
private _result = "forge_server" callExtension ["actor:create", [getPlayerUID player, toJSON _data]];
|
||||||
|
|
||||||
|
// Get actor data
|
||||||
|
private _result = "forge_server" callExtension ["actor:get", [getPlayerUID player]];
|
||||||
|
private _actorData = fromJSON (_result select 0);
|
||||||
|
|
||||||
|
// Update actor
|
||||||
|
private _update = createHashMapFromArray [["bank", 1500]];
|
||||||
|
"forge_server" callExtension ["actor:update", [getPlayerUID player, toJSON _update]];
|
||||||
```
|
```
|
||||||
|
|
||||||
Both commands report the persistence connection state.
|
## Core Modules
|
||||||
|
|
||||||
|
### Models
|
||||||
|
|
||||||
|
Defines strict data structures with built-in validation:
|
||||||
|
|
||||||
|
- `Actor`: Player data (stats, inventory, position)
|
||||||
|
- `Org`: Organization/clan data (members, roles, metadata)
|
||||||
|
|
||||||
|
[Documentation](lib/models/README.md)
|
||||||
|
|
||||||
|
### Repositories
|
||||||
|
|
||||||
|
Manages data persistence with Redis:
|
||||||
|
|
||||||
|
- Hash-based storage for structured data
|
||||||
|
- Set-based storage for collections
|
||||||
|
- Generic over Redis client implementations
|
||||||
|
|
||||||
|
[Documentation](lib/repositories/README.md)
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
|
Implements business logic and orchestration:
|
||||||
|
|
||||||
|
- Get-or-create patterns
|
||||||
|
- Data validation and transformation
|
||||||
|
- Complex workflows
|
||||||
|
|
||||||
|
[Documentation](lib/services/README.md)
|
||||||
|
|
||||||
|
### Extension
|
||||||
|
|
||||||
|
Arma 3 interface layer:
|
||||||
|
|
||||||
|
- Command routing and parsing
|
||||||
|
- Session management
|
||||||
|
- Error handling and logging
|
||||||
|
|
||||||
|
[Documentation](arma/server/extension/README.md)
|
||||||
|
|
||||||
|
### Client Mod
|
||||||
|
|
||||||
|
Client-side SQF addon that provides:
|
||||||
|
|
||||||
|
- **UI Components**: Player interfaces for inventory, organizations, banking
|
||||||
|
- **Event Handlers**: CBA event listeners for server communication
|
||||||
|
- **Optimistic Caching**: Local data caching for instant UI updates
|
||||||
|
- **State Management**: Client-side state synchronization
|
||||||
|
- **Input Validation**: Client-side validation before server requests
|
||||||
|
|
||||||
|
The client mod communicates with the server using **CBA Events**, ensuring:
|
||||||
|
|
||||||
|
- No direct extension calls from clients (security)
|
||||||
|
- Event-driven architecture for scalability
|
||||||
|
- Automatic state synchronization across all clients
|
||||||
|
- Reduced server load through client-side caching
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
### Actor Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
| -------------- | -------------------------- |
|
||||||
|
| `actor:get` | Retrieve actor data by UID |
|
||||||
|
| `actor:create` | Create a new actor |
|
||||||
|
| `actor:update` | Update actor fields |
|
||||||
|
| `actor:exists` | Check if actor exists |
|
||||||
|
| `actor:delete` | Delete actor data |
|
||||||
|
|
||||||
|
### Organization Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
| ------------------- | ------------------------------- |
|
||||||
|
| `org:get` | Retrieve organization data |
|
||||||
|
| `org:create` | Create a new organization |
|
||||||
|
| `org:update` | Update organization fields |
|
||||||
|
| `org:exists` | Check if organization exists |
|
||||||
|
| `org:delete` | Delete organization |
|
||||||
|
| `org:add_member` | Add member to organization |
|
||||||
|
| `org:remove_member` | Remove member from organization |
|
||||||
|
| `org:get_members` | Get all organization members |
|
||||||
|
|
||||||
|
### Redis Operations
|
||||||
|
|
||||||
|
Direct Redis operations for advanced use cases:
|
||||||
|
|
||||||
|
- **Common**: Key-value operations (set, get, incr, decr, del)
|
||||||
|
- **Hash**: Structured data (hset, hget, hgetall, hdel)
|
||||||
|
- **List**: Ordered collections (lpush, rpush, lrange, lpop, rpop)
|
||||||
|
- **Set**: Unique collections (sadd, smembers, srem, sismember)
|
||||||
|
|
||||||
|
[Documentation](arma/server/extension/src/redis/README.md)
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Hot Cache (Server)**: < 1ms (HashMap lookup)
|
||||||
|
- **Cold Storage (Redis)**: 1-5ms (Network + Redis query)
|
||||||
|
- **Cache Hit Ratio**: ~95% for active players
|
||||||
|
- **Memory Usage**: ~1KB per active player (server), ~2KB per player (Redis)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We welcome contributions! Please see the contributing guides for each layer:
|
||||||
|
|
||||||
|
- [Extension Contributing Guide](arma/server/extension/README.md#contributing)
|
||||||
|
- [Services Contributing Guide](lib/services/README.md#contributing)
|
||||||
|
- [Repositories Contributing Guide](lib/repositories/README.md#contributing)
|
||||||
|
- [Models Contributing Guide](lib/models/README.md#contributing)
|
||||||
|
- [Library Contributing Guide](lib/README.md#contributing)
|
||||||
|
- [Adapter Contributing Guide](arma/server/extension/src/adapters/#contributing)
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
|
||||||
|
1. **Define Model**: Create data structure with validation
|
||||||
|
2. **Create Repository**: Implement persistence layer
|
||||||
|
3. **Build Service**: Add business logic
|
||||||
|
4. **Expose in Extension**: Create SQF-callable commands
|
||||||
|
5. **Test**: Verify each layer independently
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
All commands return consistent error messages:
|
||||||
|
|
||||||
|
```sqf
|
||||||
|
private _result = "forge_server" callExtension ["actor:get", ["invalid_uid"]];
|
||||||
|
private _response = _result select 0;
|
||||||
|
|
||||||
|
if (_response find "Error:" == 0) then {
|
||||||
|
diag_log format ["Operation failed: %1", _response];
|
||||||
|
} else {
|
||||||
|
private _data = fromJSON _response;
|
||||||
|
// Use data
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
Logs are automatically created in `@forge_server/logs/`:
|
||||||
|
|
||||||
|
- `actor.log` - Actor operations
|
||||||
|
- `org.log` - Organization operations
|
||||||
|
- `redis.log` - Redis connection and operations
|
||||||
|
- `debug.log` - General debug information
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
View the License [here](LICENSE.md).
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **Issues**: [Gitea Issues](https://gitea.innovativedevsolutions.org/IDSolutions/forge/issues)
|
||||||
|
- **Documentation**: See individual module READMEs
|
||||||
|
- **Architecture**: [Diagram](Architecture_Diagram.md)
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- [ ] Admin system
|
||||||
|
- [ ] Arsenal system
|
||||||
|
- [ ] Banking system
|
||||||
|
- [ ] Economy system
|
||||||
|
- [ ] Garage system
|
||||||
|
- [ ] Locker system
|
||||||
|
- [ ] Mission template
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Built using **Rust**, **Redis**, and **Arma 3**
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* File: fnc_handleUIEvents.sqf
|
* File: fnc_handleUIEvents.sqf
|
||||||
* Author: IDSolutions
|
* Author: IDSolutions
|
||||||
* Date: 2026-01-28
|
* Date: 2026-01-28
|
||||||
* Last Update: 2026-04-06
|
* Last Update: 2026-03-28
|
||||||
* Public: No
|
* Public: No
|
||||||
*
|
*
|
||||||
* Description:
|
* Description:
|
||||||
@ -41,7 +41,7 @@ switch (_event) do {
|
|||||||
case "actor::open::vgarage": { [] spawn EFUNC(garage,openVG); };
|
case "actor::open::vgarage": { [] spawn EFUNC(garage,openVG); };
|
||||||
case "actor::open::org": { [] spawn EFUNC(org,openUI); };
|
case "actor::open::org": { [] spawn EFUNC(org,openUI); };
|
||||||
case "actor::open::vlocker": { [FORGE_Locker_Box, player, false] spawn AFUNC(arsenal,openBox) };
|
case "actor::open::vlocker": { [FORGE_Locker_Box, player, false] spawn AFUNC(arsenal,openBox) };
|
||||||
case "actor::open::phone": { [] spawn EFUNC(phone,openUI); };
|
case "actor::open::phone": { hint "Phone interaction is not yet implemented."; };
|
||||||
case "actor::open::iplayer": { hint "Player interaction is not yet implemented." };
|
case "actor::open::iplayer": { hint "Player interaction is not yet implemented." };
|
||||||
case "actor::open::store": { [] spawn EFUNC(store,openUI); };
|
case "actor::open::store": { [] spawn EFUNC(store,openUI); };
|
||||||
default { hint format ["Unhandled UI event: %1", _event]; };
|
default { hint format ["Unhandled UI event: %1", _event]; };
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
forge\forge_client\addons\phone
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
class Extended_PreStart_EventHandlers {
|
|
||||||
class ADDON {
|
|
||||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
class Extended_PreInit_EventHandlers {
|
|
||||||
class ADDON {
|
|
||||||
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
|
|
||||||
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_preInitClient));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
class Extended_PostInit_EventHandlers {
|
|
||||||
class ADDON {
|
|
||||||
init = QUOTE(call COMPILE_SCRIPT(XEH_postInit));
|
|
||||||
clientInit = QUOTE(call COMPILE_SCRIPT(XEH_postInitClient));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
forge_client_phone
|
|
||||||
===================
|
|
||||||
|
|
||||||
This addon provides the phone user interface and functionality for the in-game phone system. It handles all phone-related features including the UI display, interactions, and core phone operations.
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
PREP(handleUIEvents);
|
|
||||||
PREP(initClass);
|
|
||||||
PREP(openUI);
|
|
||||||
@ -1 +0,0 @@
|
|||||||
#include "script_component.hpp"
|
|
||||||
@ -1,340 +0,0 @@
|
|||||||
#include "script_component.hpp"
|
|
||||||
|
|
||||||
[{
|
|
||||||
GETVAR(player,FORGE_isLoaded,false)
|
|
||||||
}, {
|
|
||||||
[QGVAR(initPhone), []] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(waitUntilAndExecute);
|
|
||||||
|
|
||||||
if (isNil QGVAR(PhoneClass)) then { [] call FUNC(initClass); };
|
|
||||||
|
|
||||||
[QGVAR(initPhone), {
|
|
||||||
GVAR(PhoneClass) call ["init", []];
|
|
||||||
|
|
||||||
["forge_server_phone_requestInitPhone", [getPlayerUID player, createHashMap]] call CFUNC(serverEvent);
|
|
||||||
["forge_server_phone_requestRefreshContacts", [getPlayerUID player, player]] call CFUNC(serverEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseSyncPhone), {
|
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
GVAR(PhoneClass) call ["sync", [_data]];
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
// Contact Management Response Events
|
|
||||||
[QGVAR(responseAddContact), {
|
|
||||||
params [["_success", false, [false]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["success", "Contact Added", "Contact added successfully", 3000]] call CFUNC(localEvent);
|
|
||||||
[QGVAR(refreshUI), []] call CFUNC(localEvent);
|
|
||||||
} else {
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["danger", "Contact Error", "Failed to add contact", 4000]] call CFUNC(localEvent);
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseAddContactByPhone), {
|
|
||||||
params [["_success", false, [false]], ["_phoneNumber", "", [""]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["success", "Contact Added", format ["Contact with phone %1 added successfully", _phoneNumber], 3000]] call CFUNC(localEvent);
|
|
||||||
[QGVAR(refreshUI), []] call CFUNC(localEvent);
|
|
||||||
} else {
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["warning", "Contact Not Found", format ["Player with phone %1 not found", _phoneNumber], 4000]] call CFUNC(localEvent);
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseAddContactByEmail), {
|
|
||||||
params [["_success", false, [false]], ["_email", "", [""]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["success", "Contact Added", format ["Contact with email %1 added successfully", _email], 3000]] call CFUNC(localEvent);
|
|
||||||
[QGVAR(refreshUI), []] call CFUNC(localEvent);
|
|
||||||
} else {
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["warning", "Contact Not Found", format ["Player with email %1 not found", _email], 4000]] call CFUNC(localEvent);
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseRemoveContact), {
|
|
||||||
params [["_success", false, [false]], ["_contactUid", "", [""]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["success", "Contact Removed", "Contact removed successfully", 3000]] call CFUNC(localEvent);
|
|
||||||
[QGVAR(refreshUI), []] call CFUNC(localEvent);
|
|
||||||
} else {
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["danger", "Contact Error", "Failed to remove contact", 4000]] call CFUNC(localEvent);
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseRefreshContacts), {
|
|
||||||
params [["_contacts", [], [[]]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Contacts refreshed: %1 contacts", count _contacts];
|
|
||||||
|
|
||||||
[QGVAR(updateContacts), [_contacts]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseGetContacts), {
|
|
||||||
params [["_contactUids", [], [[]]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Got contact UIDs: %1", _contactUids];
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
// Messaging Response Events
|
|
||||||
[QGVAR(responseMessageSent), {
|
|
||||||
params [["_messageObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Message sent: %1", _messageObj];
|
|
||||||
|
|
||||||
[QGVAR(updateMessageSent), [_messageObj]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseMessageReceived), {
|
|
||||||
params [["_messageObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _fromUid = _messageObj get "from";
|
|
||||||
private _message = _messageObj get "message";
|
|
||||||
private _contacts = player getVariable ["FORGE_Contacts", []];
|
|
||||||
private _senderName = "Unknown";
|
|
||||||
|
|
||||||
{
|
|
||||||
if ((_x get "uid") isEqualTo _fromUid) exitWith {
|
|
||||||
_senderName = _x get "name";
|
|
||||||
};
|
|
||||||
} forEach _contacts;
|
|
||||||
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["info", "New Message", format ["From %1", _senderName], 4000]] call CFUNC(localEvent);
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Message received from %1: %2", _fromUid, _message];
|
|
||||||
|
|
||||||
[QGVAR(updateMessageReceived), [_messageObj]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseSendMessage), {
|
|
||||||
params [["_success", false, [false]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["success", "Message Sent", "Message sent successfully", 2000]] call CFUNC(localEvent);
|
|
||||||
} else {
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["danger", "Message Failed", "Failed to send message", 4000]] call CFUNC(localEvent);
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseGetMessages), {
|
|
||||||
params [["_messages", [], [[]]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Got %1 messages", count _messages];
|
|
||||||
|
|
||||||
[QGVAR(updateMessages), [_messages]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseGetMessageThread), {
|
|
||||||
params [["_messages", [], [[]]], ["_otherUid", "", [""]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Got message thread with %1: %2 messages", _otherUid, count _messages];
|
|
||||||
|
|
||||||
[QGVAR(updateMessageThread), [_messages, _otherUid]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseMarkMessageRead), {
|
|
||||||
params [["_success", false, [false]], ["_messageId", "", [""]]];
|
|
||||||
|
|
||||||
if (_success) then { diag_log format ["[FORGE:Client:Phone] Message %1 marked as read", _messageId]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseMessageRead), {
|
|
||||||
params [["_messageId", "", [""]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Message %1 marked as read", _messageId];
|
|
||||||
|
|
||||||
[QGVAR(updateMessageRead), [_messageId]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseDeleteMessage), {
|
|
||||||
params [["_success", false, [false]], ["_messageId", "", [""]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Message %1 deleted", _messageId];
|
|
||||||
[QGVAR(updateMessageDeleted), [_messageId]] call CFUNC(localEvent);
|
|
||||||
} else {
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["danger", "Message Delete Failed", "Failed to delete message", 4000]] call CFUNC(localEvent);
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
// Email Response Events
|
|
||||||
[QGVAR(responseEmailSent), {
|
|
||||||
params [["_emailObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Email sent: %1", _emailObj];
|
|
||||||
|
|
||||||
[QGVAR(updateEmailSent), [_emailObj]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseEmailReceived), {
|
|
||||||
params [["_emailObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _fromUid = _emailObj get "from";
|
|
||||||
private _subject = _emailObj get "subject";
|
|
||||||
private _contacts = player getVariable ["FORGE_Contacts", []];
|
|
||||||
private _senderName = "Unknown";
|
|
||||||
|
|
||||||
{
|
|
||||||
if ((_x get "uid") isEqualTo _fromUid) exitWith {
|
|
||||||
_senderName = _x get "name";
|
|
||||||
};
|
|
||||||
} forEach _contacts;
|
|
||||||
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["info", "New Email", format ["From %1: %2", _senderName, _subject], 4000]] call CFUNC(localEvent);
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Email received from %1: %2", _fromUid, _subject];
|
|
||||||
|
|
||||||
[QGVAR(updateEmailReceived), [_emailObj]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseSendEmail), {
|
|
||||||
params [["_success", false, [false]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["success", "Email Sent", "Email sent successfully", 2000]] call CFUNC(localEvent);
|
|
||||||
} else {
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["danger", "Email Failed", "Failed to send email", 4000]] call CFUNC(localEvent);
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseGetEmails), {
|
|
||||||
params [["_emails", [], [[]]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Got %1 emails", count _emails];
|
|
||||||
|
|
||||||
[QGVAR(updateEmails), [_emails]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseMarkEmailRead), {
|
|
||||||
params [["_success", false, [false]], ["_emailId", "", [""]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Email %1 marked as read", _emailId];
|
|
||||||
[QGVAR(updateEmailRead), [_emailId]] call CFUNC(localEvent);
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseEmailRead), {
|
|
||||||
params [["_emailId", "", [""]]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Email %1 marked as read", _emailId];
|
|
||||||
|
|
||||||
[QGVAR(updateEmailRead), [_emailId]] call CFUNC(localEvent);
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(responseDeleteEmail), {
|
|
||||||
params [["_success", false, [false]], ["_emailId", "", [""]]];
|
|
||||||
|
|
||||||
if (_success) then {
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Email %1 deleted", _emailId];
|
|
||||||
[QGVAR(updateEmailDeleted), [_emailId]] call CFUNC(localEvent);
|
|
||||||
} else {
|
|
||||||
[QEGVAR(notifications,recieveNotification), ["danger", "Email Delete Failed", "Failed to delete email", 4000]] call CFUNC(localEvent);
|
|
||||||
};
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
// Cleanup Response Events
|
|
||||||
[QGVAR(responseRemovePhone), {
|
|
||||||
params [["_success", false, [false]]];
|
|
||||||
|
|
||||||
if (_success) then { diag_log "[FORGE:Client:Phone] Phone data removed successfully"; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
// UI Update Events (for internal use)
|
|
||||||
[QGVAR(refreshUI), {
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", "refreshContacts()"]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateContacts), {
|
|
||||||
params [["_contacts", [], [[]]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateContacts(%1)", (toJSON _contacts)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateMessageSent), {
|
|
||||||
params [["_messageObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateMessageSent(%1)", (toJSON _messageObj)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateMessageReceived), {
|
|
||||||
params [["_messageObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateMessageReceived(%1)", (toJSON _messageObj)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateMessages), {
|
|
||||||
params [["_messages", [], [[]]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateMessages(%1)", (toJSON _messages)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateMessageThread), {
|
|
||||||
params [["_messages", [], [[]]], ["_otherUid", "", [""]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateMessageThread(%1, %2)", (toJSON _messages), (toJSON _otherUid)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateMessageDeleted), {
|
|
||||||
params [["_messageId", "", [""]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateMessageDeleted(%1)", (toJSON _messageId)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateEmailSent), {
|
|
||||||
params [["_emailObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateEmailSent(%1)", (toJSON _emailObj)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateEmailReceived), {
|
|
||||||
params [["_emailObj", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateEmailReceived(%1)", (toJSON _emailObj)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateEmails), {
|
|
||||||
params [["_emails", [], [[]]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateEmails(%1)", (toJSON _emails)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateEmailRead), {
|
|
||||||
params [["_emailId", "", [""]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateEmailRead(%1)", (toJSON _emailId)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
|
|
||||||
[QGVAR(updateEmailDeleted), {
|
|
||||||
params [["_emailId", "", [""]]];
|
|
||||||
|
|
||||||
private _control = (uiNamespace getVariable ["RscPhone", displayNull]) displayCtrl 1001;
|
|
||||||
|
|
||||||
if (!isNull _control) then { _control ctrlWebBrowserAction ["ExecJS", format ["updateEmailDeleted(%1)", (toJSON _emailId)]]; };
|
|
||||||
}] call CFUNC(addEventHandler);
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
#include "script_component.hpp"
|
|
||||||
|
|
||||||
PREP_RECOMPILE_START;
|
|
||||||
#include "XEH_PREP.hpp"
|
|
||||||
PREP_RECOMPILE_END;
|
|
||||||
|
|
||||||
private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
|
|
||||||
|
|
||||||
#include "initKeybinds.inc.sqf"
|
|
||||||
@ -1 +0,0 @@
|
|||||||
#include "script_component.hpp"
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
#include "script_component.hpp"
|
|
||||||
#include "XEH_PREP.hpp"
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
#include "script_component.hpp"
|
|
||||||
|
|
||||||
class CfgPatches {
|
|
||||||
class ADDON {
|
|
||||||
author = AUTHOR;
|
|
||||||
authors[] = {"J. Schmidt"};
|
|
||||||
url = ECSTRING(main,url);
|
|
||||||
name = COMPONENT_NAME;
|
|
||||||
requiredVersion = REQUIRED_VERSION;
|
|
||||||
requiredAddons[] = {
|
|
||||||
"forge_client_main"
|
|
||||||
};
|
|
||||||
units[] = {};
|
|
||||||
weapons[] = {};
|
|
||||||
VERSION_CONFIG;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
#include "CfgEventHandlers.hpp"
|
|
||||||
#include "ui\RscCommon.hpp"
|
|
||||||
#include "ui\RscPhone.hpp"
|
|
||||||
@ -1,351 +0,0 @@
|
|||||||
#include "..\script_component.hpp"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Author: IDSolutions
|
|
||||||
* Handles UI events.
|
|
||||||
*
|
|
||||||
* Arguments:
|
|
||||||
* None
|
|
||||||
*
|
|
||||||
* Return Value:
|
|
||||||
* None
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* [] call forge_client_phone_fnc_handleUIEvents;
|
|
||||||
*
|
|
||||||
* Public: No
|
|
||||||
*/
|
|
||||||
|
|
||||||
params ["_control", "_isConfirmDialog", "_message"];
|
|
||||||
|
|
||||||
private _alert = fromJSON _message;
|
|
||||||
private _event = _alert get "event";
|
|
||||||
private _data = _alert get "data";
|
|
||||||
|
|
||||||
// diag_log format ["[FORGE:Client:Phone] Handling UI event: %1 with data: %2", _event, _data];
|
|
||||||
|
|
||||||
switch (_event) do {
|
|
||||||
case "phone::get::player": {
|
|
||||||
private _uid = getPlayerUID player;
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", format ["setPlayerUid(%1)", (toJSON _uid)]];
|
|
||||||
};
|
|
||||||
case "phone::get::theme": {
|
|
||||||
private _isDark = profileNamespace getVariable ["FORGE_Phone_isDark", true];
|
|
||||||
private _theme = ["light", "dark"] select (_isDark);
|
|
||||||
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", format ["setTheme(%1)", (toJSON _theme)]];
|
|
||||||
};
|
|
||||||
case "phone::get::contacts": {
|
|
||||||
private _contacts = player getVariable ["FORGE_Contacts", []];
|
|
||||||
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", format ["loadContacts(%1)", (toJSON _contacts)]];
|
|
||||||
["forge_server_phone_requestRefreshContacts", [getPlayerUID player, player]] call CFUNC(serverEvent);
|
|
||||||
};
|
|
||||||
case "phone::set::theme": {
|
|
||||||
private _isDark = _data get "isDark";
|
|
||||||
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_isDark", _isDark];
|
|
||||||
};
|
|
||||||
case "phone::add::contact": {
|
|
||||||
private _contactPhone = _data get "phone";
|
|
||||||
|
|
||||||
if (_contactPhone isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestAddContactByPhone", [getPlayerUID player, _contactPhone, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No phone number provided for contact addition";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::add::contact::by::phone": {
|
|
||||||
private _phoneNumber = _data get "phone";
|
|
||||||
|
|
||||||
if (_phoneNumber isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestAddContactByPhone", [getPlayerUID player, _phoneNumber, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No phone number provided";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::add::contact::by::email": {
|
|
||||||
private _email = _data get "email";
|
|
||||||
|
|
||||||
if (_email isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestAddContactByEmail", [getPlayerUID player, _email, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No email provided";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::remove::contact": {
|
|
||||||
private _contactUid = _data get "uid";
|
|
||||||
|
|
||||||
if (_contactUid isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestRemoveContact", [getPlayerUID player, _contactUid, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No contact UID provided for removal";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::refresh::contacts": {
|
|
||||||
["forge_server_phone_requestRefreshContacts", [getPlayerUID player, player]] call CFUNC(serverEvent);
|
|
||||||
};
|
|
||||||
case "phone::send::message": {
|
|
||||||
private _contactName = _data get "contactName";
|
|
||||||
private _messageData = _data get "message";
|
|
||||||
private _messageText = _messageData get "text";
|
|
||||||
private _toUid = _data get "toUid";
|
|
||||||
|
|
||||||
if (_toUid isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestSendMessage", [getPlayerUID player, _toUid, _messageText, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log format ["[FORGE:Client:Phone] No recipient UID provided for message to %1", _contactName];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::get::messages": {
|
|
||||||
["forge_server_phone_requestGetMessages", [getPlayerUID player, player]] call CFUNC(serverEvent);
|
|
||||||
};
|
|
||||||
case "phone::get::message::thread": {
|
|
||||||
private _otherUid = _data get "otherUid";
|
|
||||||
|
|
||||||
if (_otherUid isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestGetMessageThread", [getPlayerUID player, _otherUid, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No other UID provided for message thread";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::mark::message::read": {
|
|
||||||
private _messageId = _data get "messageId";
|
|
||||||
|
|
||||||
if (_messageId isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestMarkMessageRead", [getPlayerUID player, _messageId, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No message ID provided for mark read";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::delete::message": {
|
|
||||||
private _messageId = _data get "messageId";
|
|
||||||
|
|
||||||
if (_messageId isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestDeleteMessage", [getPlayerUID player, _messageId, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No message ID provided for delete";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::send::email": {
|
|
||||||
private _toUid = _data get "toUid";
|
|
||||||
private _subject = _data get "subject";
|
|
||||||
private _body = _data get "body";
|
|
||||||
if (_subject isEqualTo "") then { _subject = "No subject"; };
|
|
||||||
|
|
||||||
if (_toUid isNotEqualTo "" && _body isNotEqualTo "") then {
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Sending email to %1 subject length %2 body length %3", _toUid, count _subject, count _body];
|
|
||||||
["forge_server_phone_requestSendEmail", [getPlayerUID player, _toUid, _subject, _body, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] Missing required email parameters";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::get::emails": {
|
|
||||||
["forge_server_phone_requestGetEmails", [getPlayerUID player, player]] call CFUNC(serverEvent);
|
|
||||||
};
|
|
||||||
case "phone::mark::email::read": {
|
|
||||||
private _emailId = _data get "emailId";
|
|
||||||
|
|
||||||
if (_emailId isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestMarkEmailRead", [getPlayerUID player, _emailId, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No email ID provided for mark read";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::delete::email": {
|
|
||||||
private _emailId = _data get "emailId";
|
|
||||||
|
|
||||||
if (_emailId isNotEqualTo "") then {
|
|
||||||
["forge_server_phone_requestDeleteEmail", [getPlayerUID player, _emailId, player]] call CFUNC(serverEvent);
|
|
||||||
} else {
|
|
||||||
diag_log "[FORGE:Client:Phone] No email ID provided for delete";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::get::notes": {
|
|
||||||
private _notes = GVAR(PhoneClass) call ["getAllNotes", []];
|
|
||||||
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", format ["loadNotes(%1)", (toJSON _notes)]];
|
|
||||||
};
|
|
||||||
case "phone::save::note": {
|
|
||||||
private _success = GVAR(PhoneClass) call ["addNote", [_data]];
|
|
||||||
_success
|
|
||||||
};
|
|
||||||
case "phone::delete::note": {
|
|
||||||
private _noteId = _data get "id";
|
|
||||||
|
|
||||||
private _success = GVAR(PhoneClass) call ["deleteNote", [_noteId]];
|
|
||||||
_success
|
|
||||||
};
|
|
||||||
case "phone::get::events": {
|
|
||||||
private _events = profileNamespace getVariable ["FORGE_Phone_Events", []];
|
|
||||||
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", format ["loadCalendarEvents(%1)", (toJSON _events)]];
|
|
||||||
};
|
|
||||||
case "phone::save::event": {
|
|
||||||
private _eventId = _data get "id";
|
|
||||||
private _eventTitle = _data get "title";
|
|
||||||
|
|
||||||
private _events = profileNamespace getVariable ["FORGE_Phone_Events", []];
|
|
||||||
private _existingIndex = -1;
|
|
||||||
{
|
|
||||||
private _existingId = _x get "id";
|
|
||||||
if (_existingId isEqualTo _eventId) then {
|
|
||||||
_existingIndex = _forEachIndex;
|
|
||||||
};
|
|
||||||
} forEach _events;
|
|
||||||
|
|
||||||
if (_existingIndex >= 0) then {
|
|
||||||
_events set [_existingIndex, _data];
|
|
||||||
diag_log format ["[PHONE] Updated event: %1 [ID: %2]", _eventTitle, _eventId];
|
|
||||||
} else {
|
|
||||||
_events pushBack _data;
|
|
||||||
diag_log format ["[PHONE] Added new event: %1 [ID: %2]", _eventTitle, _eventId];
|
|
||||||
};
|
|
||||||
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Events", _events];
|
|
||||||
diag_log format ["[PHONE] Saved events to profile. Total events: %1", count _events];
|
|
||||||
};
|
|
||||||
case "phone::delete::event": {
|
|
||||||
private _eventId = _data get "id";
|
|
||||||
private _events = profileNamespace getVariable ["FORGE_Phone_Events", []];
|
|
||||||
|
|
||||||
private _newEvents = [];
|
|
||||||
private _deleted = false;
|
|
||||||
{
|
|
||||||
private _existingId = _x get "id";
|
|
||||||
if (_existingId isEqualTo _eventId) then {
|
|
||||||
_deleted = true;
|
|
||||||
} else {
|
|
||||||
_newEvents pushBack _x;
|
|
||||||
};
|
|
||||||
} forEach _events;
|
|
||||||
|
|
||||||
if (_deleted) then {
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Events", _newEvents];
|
|
||||||
diag_log format ["[PHONE] Deleted calendar event [ID: %1]. Remaining events: %2", _eventId, count _newEvents];
|
|
||||||
} else {
|
|
||||||
diag_log format ["[PHONE] Calendar event not found for deletion [ID: %1]", _eventId];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::get::clocks": {
|
|
||||||
private _worldClocks = profileNamespace getVariable ["FORGE_Phone_WorldClocks", []];
|
|
||||||
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", format ["loadWorldClocks(%1)", (toJSON _worldClocks)]];
|
|
||||||
};
|
|
||||||
case "phone::save::clock": {
|
|
||||||
private _clockId = _data get "id";
|
|
||||||
private _timezone = _data get "timezone";
|
|
||||||
private _city = _data get "city";
|
|
||||||
|
|
||||||
private _worldClocks = profileNamespace getVariable ["FORGE_Phone_WorldClocks", []];
|
|
||||||
private _clockExists = false;
|
|
||||||
{
|
|
||||||
private _existingId = _x get "id";
|
|
||||||
private _existingTimezone = _x get "timezone";
|
|
||||||
if (_existingId isEqualTo _clockId || _existingTimezone isEqualTo _timezone) then {
|
|
||||||
_clockExists = true;
|
|
||||||
};
|
|
||||||
} forEach _worldClocks;
|
|
||||||
|
|
||||||
if (!_clockExists) then {
|
|
||||||
_worldClocks pushBack _data;
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_WorldClocks", _worldClocks];
|
|
||||||
|
|
||||||
diag_log format ["[PHONE] Added world clock: %1 (%2) [ID: %3]. Total clocks: %4", _city, _timezone, _clockId, count _worldClocks];
|
|
||||||
} else {
|
|
||||||
diag_log format ["[PHONE] World clock already exists: %1 (%2) [ID: %3]. Skipping duplicate.", _city, _timezone, _clockId];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::delete::clock": {
|
|
||||||
private _clockId = _data get "id";
|
|
||||||
|
|
||||||
private _worldClocks = profileNamespace getVariable ["FORGE_Phone_WorldClocks", []];
|
|
||||||
private _newClocks = [];
|
|
||||||
private _deleted = false;
|
|
||||||
{
|
|
||||||
private _existingId = _x get "id";
|
|
||||||
if (_existingId isEqualTo _clockId) then {
|
|
||||||
_deleted = true;
|
|
||||||
} else {
|
|
||||||
_newClocks pushBack _x;
|
|
||||||
};
|
|
||||||
} forEach _worldClocks;
|
|
||||||
|
|
||||||
if (_deleted) then {
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_WorldClocks", _newClocks];
|
|
||||||
diag_log format ["[PHONE] Deleted world clock [ID: %1]. Remaining clocks: %2", _clockId, count _newClocks];
|
|
||||||
} else {
|
|
||||||
diag_log format ["[PHONE] World clock not found for deletion [ID: %1]", _clockId];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::get::alarms": {
|
|
||||||
private _alarms = profileNamespace getVariable ["FORGE_Phone_Alarms", []];
|
|
||||||
|
|
||||||
_control ctrlWebBrowserAction ["ExecJS", format ["loadAlarms(%1)", (toJSON _alarms)]];
|
|
||||||
};
|
|
||||||
case "phone::save::alarm": {
|
|
||||||
private _alarmId = _data get "id";
|
|
||||||
private _alarmTime = _data get "time";
|
|
||||||
private _alarmLabel = _data get "label";
|
|
||||||
|
|
||||||
private _alarms = profileNamespace getVariable ["FORGE_Phone_Alarms", []];
|
|
||||||
private _existingIndex = -1;
|
|
||||||
{
|
|
||||||
private _existingId = _x get "id";
|
|
||||||
if (_existingId isEqualTo _alarmId) then {
|
|
||||||
_existingIndex = _forEachIndex;
|
|
||||||
};
|
|
||||||
} forEach _alarms;
|
|
||||||
|
|
||||||
if (_existingIndex >= 0) then {
|
|
||||||
_alarms set [_existingIndex, _data];
|
|
||||||
diag_log format ["[PHONE] Updated alarm: %1 at %2 [ID: %3]", _alarmLabel, _alarmTime, _alarmId];
|
|
||||||
} else {
|
|
||||||
_alarms pushBack _data;
|
|
||||||
diag_log format ["[PHONE] Added new alarm: %1 at %2 [ID: %3]", _alarmLabel, _alarmTime, _alarmId];
|
|
||||||
};
|
|
||||||
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Alarms", _alarms];
|
|
||||||
diag_log format ["[PHONE] Saved alarms to profile. Total alarms: %1", count _alarms];
|
|
||||||
};
|
|
||||||
case "phone::delete::alarm": {
|
|
||||||
private _alarmId = _data get "id";
|
|
||||||
|
|
||||||
private _alarms = profileNamespace getVariable ["FORGE_Phone_Alarms", []];
|
|
||||||
private _newAlarms = [];
|
|
||||||
private _deleted = false;
|
|
||||||
{
|
|
||||||
private _existingId = _x get "id";
|
|
||||||
if (_existingId isEqualTo _alarmId) then {
|
|
||||||
_deleted = true;
|
|
||||||
} else {
|
|
||||||
_newAlarms pushBack _x;
|
|
||||||
};
|
|
||||||
} forEach _alarms;
|
|
||||||
|
|
||||||
if (_deleted) then {
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Alarms", _newAlarms];
|
|
||||||
diag_log format ["[PHONE] Deleted alarm [ID: %1]. Remaining alarms: %2", _alarmId, count _newAlarms];
|
|
||||||
} else {
|
|
||||||
diag_log format ["[PHONE] Alarm not found for deletion [ID: %1]", _alarmId];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
case "phone::toggle::alarm": {
|
|
||||||
private _alarmId = _data get "id";
|
|
||||||
|
|
||||||
private _alarms = profileNamespace getVariable ["FORGE_Phone_Alarms", []];
|
|
||||||
{
|
|
||||||
private _existingId = _x get "id";
|
|
||||||
if (_existingId isEqualTo _alarmId) then {
|
|
||||||
private _currentEnabled = _x get "enabled";
|
|
||||||
_x set ["enabled", !_currentEnabled];
|
|
||||||
diag_log format ["[PHONE] Toggled alarm [ID: %1] to %2", _alarmId, !_currentEnabled];
|
|
||||||
};
|
|
||||||
} forEach _alarms;
|
|
||||||
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Alarms", _alarms];
|
|
||||||
};
|
|
||||||
default { hint format ["Unhandled phone event: %1", _event]; };
|
|
||||||
};
|
|
||||||
|
|
||||||
true;
|
|
||||||
@ -1,293 +0,0 @@
|
|||||||
#include "..\script_component.hpp"
|
|
||||||
|
|
||||||
#pragma hemtt ignore_variables ["_self"]
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Author: IDSolutions
|
|
||||||
* Initialize unified phone class
|
|
||||||
*
|
|
||||||
* Arguments:
|
|
||||||
* N/A
|
|
||||||
*
|
|
||||||
* Return Value:
|
|
||||||
* N/A
|
|
||||||
*
|
|
||||||
* Examples:
|
|
||||||
* [] call forge_client_phone_fnc_initClass
|
|
||||||
*
|
|
||||||
* Public: Yes
|
|
||||||
*/
|
|
||||||
|
|
||||||
// TODO: Perform comprehensive review and edit of phone class implementation
|
|
||||||
// Then integrate this class to replace current phone handling logic
|
|
||||||
// Key areas to address:
|
|
||||||
// - Verify all phone data structures and methods
|
|
||||||
// - Ensure proper data persistence
|
|
||||||
// - Implement robust error handling
|
|
||||||
// - Replace direct UI manipulation with class-based approach
|
|
||||||
GVAR(PhoneClass) = createHashMapObject [[
|
|
||||||
["#type", "IPhoneClass"],
|
|
||||||
["#create", {
|
|
||||||
_self set ["uid", getPlayerUID player];
|
|
||||||
_self set ["notes", createHashMap];
|
|
||||||
_self set ["events", []];
|
|
||||||
_self set ["settings", createHashMap];
|
|
||||||
_self set ["isLoaded", false];
|
|
||||||
_self set ["lastSave", time];
|
|
||||||
|
|
||||||
// Initialize default settings
|
|
||||||
private _settings = createHashMap;
|
|
||||||
_settings set ["theme", "light"];
|
|
||||||
_settings set ["notifications", true];
|
|
||||||
_settings set ["sound", true];
|
|
||||||
_settings set ["vibration", true];
|
|
||||||
_self set ["settings", _settings];
|
|
||||||
}],
|
|
||||||
["init", {
|
|
||||||
// Contacts/messages/emails are server-owned. Keep only local utility-app
|
|
||||||
// state in profileNamespace until those apps are migrated.
|
|
||||||
private _savedNotes = profileNamespace getVariable ["FORGE_Phone_Notes", createHashMap];
|
|
||||||
private _savedEvents = profileNamespace getVariable ["FORGE_Phone_Events", []];
|
|
||||||
private _savedSettings = profileNamespace getVariable ["FORGE_Phone_Settings", createHashMap];
|
|
||||||
|
|
||||||
_self set ["notes", _savedNotes];
|
|
||||||
_self set ["events", _savedEvents];
|
|
||||||
|
|
||||||
// Merge saved settings with defaults
|
|
||||||
private _defaultSettings = _self get "settings";
|
|
||||||
{
|
|
||||||
_defaultSettings set [_x, _y];
|
|
||||||
} forEach _savedSettings;
|
|
||||||
|
|
||||||
_self set ["settings", _defaultSettings];
|
|
||||||
_self set ["isLoaded", true];
|
|
||||||
|
|
||||||
systemChat format ["Phone loaded for %1", (name player)];
|
|
||||||
diag_log "[FORGE:Client:Phone] Phone Class Initialized!";
|
|
||||||
}],
|
|
||||||
["_padString", {
|
|
||||||
params [["_number", 0, [0]], ["_length", 0, [0]]];
|
|
||||||
|
|
||||||
private _str = str _number;
|
|
||||||
|
|
||||||
while { (_str select [(_length - 1), 1]) == "" } do { _str = "0" + _str };
|
|
||||||
_str
|
|
||||||
}],
|
|
||||||
["save", {
|
|
||||||
params [["_sync", false, [false]]];
|
|
||||||
|
|
||||||
// Save local-only phone app state to profile.
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Notes", _self get "notes"];
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Events", _self get "events"];
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Settings", _self get "settings"];
|
|
||||||
|
|
||||||
if (_sync) then { saveProfileNamespace; };
|
|
||||||
|
|
||||||
_self set ["lastSave", time];
|
|
||||||
}],
|
|
||||||
["sync", {
|
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
if (_data isEqualTo createHashMap) exitWith { diag_log "[FORGE:Client:Phone] Empty data received for sync, skipping."; };
|
|
||||||
}],
|
|
||||||
["get", {
|
|
||||||
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
|
|
||||||
|
|
||||||
private _settings = _self get "settings";
|
|
||||||
_settings getOrDefault [_key, _default];
|
|
||||||
}],
|
|
||||||
["addNote", {
|
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
if (_data isEqualTo createHashMap) exitWith { false };
|
|
||||||
|
|
||||||
private _noteId = _data get "id";
|
|
||||||
private _notes = _self get "notes";
|
|
||||||
|
|
||||||
_notes set [_noteId, _data];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Added note [ID: %1]", _noteId];
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["updateNote", {
|
|
||||||
params [["_data", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _noteId = _data get "id";
|
|
||||||
if (isNil "_noteId" || _noteId == "") exitWith { false };
|
|
||||||
|
|
||||||
private _notes = _self get "notes";
|
|
||||||
if !(_noteId in _notes) exitWith { false };
|
|
||||||
|
|
||||||
_notes set [_noteId, _data];
|
|
||||||
_self set ["notes", _notes];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Updated note [ID: %1]", _noteId];
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["deleteNote", {
|
|
||||||
params [["_noteId", "", [""]]];
|
|
||||||
|
|
||||||
if (_noteId == "") exitWith { false };
|
|
||||||
|
|
||||||
private _notes = _self get "notes";
|
|
||||||
if (!(_noteId in _notes)) exitWith { false };
|
|
||||||
|
|
||||||
_notes deleteAt _noteId;
|
|
||||||
_self set ["notes", _notes];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Deleted note [ID: %1]", _noteId];
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["getNote", {
|
|
||||||
params [["_noteId", "", [""]], ["_default", nil]];
|
|
||||||
|
|
||||||
private _notes = _self get "notes";
|
|
||||||
_notes getOrDefault [_noteId, _default];
|
|
||||||
}],
|
|
||||||
["getAllNotes", {
|
|
||||||
private _notes = _self get "notes";
|
|
||||||
private _notesArray = [];
|
|
||||||
|
|
||||||
{
|
|
||||||
_notesArray pushBack _y;
|
|
||||||
} forEach _notes;
|
|
||||||
|
|
||||||
_notesArray
|
|
||||||
}],
|
|
||||||
["setSetting", {
|
|
||||||
params [["_key", "", [""]], ["_value", nil]];
|
|
||||||
|
|
||||||
if (_key == "") exitWith { false };
|
|
||||||
|
|
||||||
private _settings = _self get "settings";
|
|
||||||
_settings set [_key, _value];
|
|
||||||
_self set ["settings", _settings];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["getSetting", {
|
|
||||||
params [["_key", "", [""]], ["_default", nil]];
|
|
||||||
|
|
||||||
private _settings = _self get "settings";
|
|
||||||
_settings getOrDefault [_key, _default];
|
|
||||||
}],
|
|
||||||
["getAllSettings", {
|
|
||||||
_self get "settings";
|
|
||||||
}],
|
|
||||||
["addEvent", {
|
|
||||||
params [["_eventData", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
if (_eventData isEqualTo createHashMap) exitWith { false };
|
|
||||||
|
|
||||||
private _eventId = _eventData get "id";
|
|
||||||
if (isNil "_eventId" || _eventId == "") exitWith { false };
|
|
||||||
|
|
||||||
private _events = _self get "events";
|
|
||||||
|
|
||||||
// Check if event already exists
|
|
||||||
private _existingIndex = _events findIf {(_x get "id") isEqualTo _eventId};
|
|
||||||
|
|
||||||
if (_existingIndex >= 0) then {
|
|
||||||
// Update existing event
|
|
||||||
_events set [_existingIndex, _eventData];
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Updated event [ID: %1]", _eventId];
|
|
||||||
} else {
|
|
||||||
// Add new event
|
|
||||||
_events pushBack _eventData;
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Added event [ID: %1]", _eventId];
|
|
||||||
};
|
|
||||||
|
|
||||||
_self set ["events", _events];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["updateEvent", {
|
|
||||||
params [["_eventData", createHashMap, [createHashMap]]];
|
|
||||||
|
|
||||||
private _eventId = _eventData get "id";
|
|
||||||
if (isNil "_eventId" || _eventId == "") exitWith { false };
|
|
||||||
|
|
||||||
private _events = _self get "events";
|
|
||||||
private _existingIndex = _events findIf {(_x get "id") isEqualTo _eventId};
|
|
||||||
|
|
||||||
if (_existingIndex < 0) exitWith { false };
|
|
||||||
|
|
||||||
_events set [_existingIndex, _eventData];
|
|
||||||
_self set ["events", _events];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Updated event [ID: %1]", _eventId];
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["deleteEvent", {
|
|
||||||
params [["_eventId", "", [""]]];
|
|
||||||
|
|
||||||
if (_eventId == "") exitWith { false };
|
|
||||||
|
|
||||||
private _events = _self get "events";
|
|
||||||
private _existingIndex = _events findIf {(_x get "id") isEqualTo _eventId};
|
|
||||||
|
|
||||||
if (_existingIndex < 0) exitWith { false };
|
|
||||||
|
|
||||||
_events deleteAt _existingIndex;
|
|
||||||
_self set ["events", _events];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
|
|
||||||
diag_log format ["[FORGE:Client:Phone] Deleted event [ID: %1]", _eventId];
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["getEvent", {
|
|
||||||
params [["_eventId", "", [""]], ["_default", nil]];
|
|
||||||
|
|
||||||
private _events = _self get "events";
|
|
||||||
private _event = _events select {(_x get "id") isEqualTo _eventId};
|
|
||||||
|
|
||||||
if (_event isNotEqualTo []) then {
|
|
||||||
_event select 0
|
|
||||||
} else {
|
|
||||||
_default
|
|
||||||
};
|
|
||||||
}],
|
|
||||||
["getAllEvents", {
|
|
||||||
private _events = _self get "events";
|
|
||||||
_events
|
|
||||||
}],
|
|
||||||
["getEventsByDate", {
|
|
||||||
params [["_date", "", [""]]];
|
|
||||||
|
|
||||||
private _events = _self get "events";
|
|
||||||
private _dateEvents = _events select {
|
|
||||||
private _eventStartTime = _x get "startTime";
|
|
||||||
if (isNil "_eventStartTime") then { false } else {
|
|
||||||
// Extract date from ISO string (YYYY-MM-DD)
|
|
||||||
private _eventDate = (_eventStartTime splitString "T") select 0;
|
|
||||||
_eventDate isEqualTo _date
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
_dateEvents
|
|
||||||
}],
|
|
||||||
["clearAllEvents", {
|
|
||||||
_self set ["events", []];
|
|
||||||
_self call ["save", [true]];
|
|
||||||
diag_log "[FORGE:Client:Phone] Cleared all events";
|
|
||||||
true
|
|
||||||
}],
|
|
||||||
["getEventsForToday", {
|
|
||||||
private _currentTime = systemTimeUTC;
|
|
||||||
private _todayDate = format ["%1-%2-%3",
|
|
||||||
_currentTime select 0,
|
|
||||||
_self call ["_padString", [(_currentTime select 1), 2]],
|
|
||||||
_self call ["_padString", [(_currentTime select 2), 2]]
|
|
||||||
];
|
|
||||||
|
|
||||||
_self call ["getEventsByDate", [_todayDate]]
|
|
||||||
}]
|
|
||||||
]];
|
|
||||||
|
|
||||||
SETVAR(player,FORGE_PhoneClass,GVAR(PhoneClass));
|
|
||||||
GVAR(PhoneClass)
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
#include "..\script_component.hpp"
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Author: IDSolutions
|
|
||||||
* Open phone interface.
|
|
||||||
*
|
|
||||||
* Arguments:
|
|
||||||
* None
|
|
||||||
*
|
|
||||||
* Return Value:
|
|
||||||
* None
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* [] call forge_client_phone_fnc_openUI;
|
|
||||||
*
|
|
||||||
* Public: No
|
|
||||||
*/
|
|
||||||
|
|
||||||
private _display = (findDisplay 46) createDisplay "RscPhone";
|
|
||||||
private _ctrl = (_display displayCtrl 1001);
|
|
||||||
|
|
||||||
_ctrl ctrlAddEventHandler ["JSDialog", {
|
|
||||||
params ["_control", "_isConfirmDialog", "_message"];
|
|
||||||
|
|
||||||
[_control, _isConfirmDialog, _message] call FUNC(handleUIEvents);
|
|
||||||
}];
|
|
||||||
|
|
||||||
_ctrl ctrlWebBrowserAction ["LoadFile", QUOTE(PATHTOF(ui\_site\index.html))];
|
|
||||||
// _ctrl ctrlWebBrowserAction ["OpenDevConsole"];
|
|
||||||
|
|
||||||
true;
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
#include "\forge\forge_client\addons\main\data\hpp\defineDIKCodes.hpp"
|
|
||||||
|
|
||||||
[
|
|
||||||
_category, QGVAR(ForgePhone),
|
|
||||||
[LSTRING(phone), LSTRING(phoneTooltip)], {
|
|
||||||
[] call FUNC(openUI)
|
|
||||||
}, {}, [DIK_P, [false, false, false]]
|
|
||||||
] call CFUNC(addKeybind);
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
#define COMPONENT phone
|
|
||||||
#define COMPONENT_BEAUTIFIED Phone
|
|
||||||
#include "\forge\forge_client\addons\main\script_mod.hpp"
|
|
||||||
|
|
||||||
// #define DEBUG_MODE_FULL
|
|
||||||
// #define DISABLE_COMPILE_CACHE
|
|
||||||
// #define ENABLE_PERFORMANCE_COUNTERS
|
|
||||||
|
|
||||||
#include "\forge\forge_client\addons\main\script_macros.hpp"
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project name="FFE">
|
|
||||||
<Package name="Phone">
|
|
||||||
<Key ID="STR_forge_client_phone_displayName">
|
|
||||||
<English>Phone</English>
|
|
||||||
</Key>
|
|
||||||
<Key ID="STR_forge_client_phone_phone">
|
|
||||||
<English>Phone</English>
|
|
||||||
</Key>
|
|
||||||
<Key ID="STR_forge_client_phone_phoneTooltip">
|
|
||||||
<English>Open your phone</English>
|
|
||||||
</Key>
|
|
||||||
</Package>
|
|
||||||
</Project>
|
|
||||||
@ -1,265 +0,0 @@
|
|||||||
// Control types
|
|
||||||
#define CT_STATIC 0
|
|
||||||
#define CT_BUTTON 1
|
|
||||||
#define CT_EDIT 2
|
|
||||||
#define CT_SLIDER 3
|
|
||||||
#define CT_COMBO 4
|
|
||||||
#define CT_LISTBOX 5
|
|
||||||
#define CT_TOOLBOX 6
|
|
||||||
#define CT_CHECKBOXES 7
|
|
||||||
#define CT_PROGRESS 8
|
|
||||||
#define CT_HTML 9
|
|
||||||
#define CT_STATIC_SKEW 10
|
|
||||||
#define CT_ACTIVETEXT 11
|
|
||||||
#define CT_TREE 12
|
|
||||||
#define CT_STRUCTURED_TEXT 13
|
|
||||||
#define CT_CONTEXT_MENU 14
|
|
||||||
#define CT_CONTROLS_GROUP 15
|
|
||||||
#define CT_SHORTCUTBUTTON 16
|
|
||||||
#define CT_HITZONES 17
|
|
||||||
#define CT_XKEYDESC 40
|
|
||||||
#define CT_XBUTTON 41
|
|
||||||
#define CT_XLISTBOX 42
|
|
||||||
#define CT_XSLIDER 43
|
|
||||||
#define CT_XCOMBO 44
|
|
||||||
#define CT_ANIMATED_TEXTURE 45
|
|
||||||
#define CT_OBJECT 80
|
|
||||||
#define CT_OBJECT_ZOOM 81
|
|
||||||
#define CT_OBJECT_CONTAINER 82
|
|
||||||
#define CT_OBJECT_CONT_ANIM 83
|
|
||||||
#define CT_LINEBREAK 98
|
|
||||||
#define CT_USER 99
|
|
||||||
#define CT_MAP 100
|
|
||||||
#define CT_MAP_MAIN 101
|
|
||||||
#define CT_LISTNBOX 102
|
|
||||||
#define CT_ITEMSLOT 103
|
|
||||||
#define CT_CHECKBOX 77
|
|
||||||
|
|
||||||
// Static styles
|
|
||||||
#define ST_POS 0x0F
|
|
||||||
#define ST_HPOS 0x03
|
|
||||||
#define ST_VPOS 0x0C
|
|
||||||
#define ST_LEFT 0x00
|
|
||||||
#define ST_RIGHT 0x01
|
|
||||||
#define ST_CENTER 0x02
|
|
||||||
#define ST_DOWN 0x04
|
|
||||||
#define ST_UP 0x08
|
|
||||||
#define ST_VCENTER 0x0C
|
|
||||||
|
|
||||||
#define ST_TYPE 0xF0
|
|
||||||
#define ST_SINGLE 0x00
|
|
||||||
#define ST_MULTI 0x10
|
|
||||||
#define ST_TITLE_BAR 0x20
|
|
||||||
#define ST_PICTURE 0x30
|
|
||||||
#define ST_FRAME 0x40
|
|
||||||
#define ST_BACKGROUND 0x50
|
|
||||||
#define ST_GROUP_BOX 0x60
|
|
||||||
#define ST_GROUP_BOX2 0x70
|
|
||||||
#define ST_HUD_BACKGROUND 0x80
|
|
||||||
#define ST_TILE_PICTURE 0x90
|
|
||||||
#define ST_WITH_RECT 0xA0
|
|
||||||
#define ST_LINE 0xB0
|
|
||||||
#define ST_UPPERCASE 0xC0
|
|
||||||
#define ST_LOWERCASE 0xD0
|
|
||||||
|
|
||||||
#define ST_SHADOW 0x100
|
|
||||||
#define ST_NO_RECT 0x200
|
|
||||||
#define ST_KEEP_ASPECT_RATIO 0x800
|
|
||||||
|
|
||||||
// Slider styles
|
|
||||||
#define SL_DIR 0x400
|
|
||||||
#define SL_VERT 0
|
|
||||||
#define SL_HORZ 0x400
|
|
||||||
|
|
||||||
#define SL_TEXTURES 0x10
|
|
||||||
|
|
||||||
// progress bar
|
|
||||||
#define ST_VERTICAL 0x01
|
|
||||||
#define ST_HORIZONTAL 0
|
|
||||||
|
|
||||||
// Listbox styles
|
|
||||||
#define LB_TEXTURES 0x10
|
|
||||||
#define LB_MULTI 0x20
|
|
||||||
|
|
||||||
// Tree styles
|
|
||||||
#define TR_SHOWROOT 1
|
|
||||||
#define TR_AUTOCOLLAPSE 2
|
|
||||||
|
|
||||||
// Default text sizes
|
|
||||||
#define GUI_TEXT_SIZE_SMALL (GUI_GRID_H * 0.8)
|
|
||||||
#define GUI_TEXT_SIZE_MEDIUM (GUI_GRID_H * 1)
|
|
||||||
#define GUI_TEXT_SIZE_LARGE (GUI_GRID_H * 1.2)
|
|
||||||
|
|
||||||
// Pixel grid
|
|
||||||
#define pixelScale 0.50
|
|
||||||
#define GRID_W (pixelW * pixelGrid * pixelScale)
|
|
||||||
#define GRID_H (pixelH * pixelGrid * pixelScale)
|
|
||||||
|
|
||||||
class ScrollBar;
|
|
||||||
class RscObject;
|
|
||||||
class RscText;
|
|
||||||
class RscTextSmall;
|
|
||||||
class RscTitle;
|
|
||||||
class RscProgress;
|
|
||||||
class RscProgressNotFreeze;
|
|
||||||
class RscPicture;
|
|
||||||
class RscLadderPicture;
|
|
||||||
class RscPictureKeepAspect;
|
|
||||||
class RscHTML;
|
|
||||||
class RscButton;
|
|
||||||
class RscShortcutButton;
|
|
||||||
class RscButtonSmall;
|
|
||||||
class RscEdit;
|
|
||||||
class RscCombo;
|
|
||||||
class RscListBox;
|
|
||||||
class RscListNBox;
|
|
||||||
class RscXListBox;
|
|
||||||
class RscTree;
|
|
||||||
class RscSlider;
|
|
||||||
class RscSliderH;
|
|
||||||
class RscXSliderH;
|
|
||||||
class RscActiveText;
|
|
||||||
class RscStructuredText;
|
|
||||||
class RscControlsGroup;
|
|
||||||
class RscToolbox;
|
|
||||||
class RscMapControl;
|
|
||||||
class RscCheckBox;
|
|
||||||
class RscFrame;
|
|
||||||
class ctrlDefault;
|
|
||||||
class ctrlControlsGroup;
|
|
||||||
class ctrlDefaultText;
|
|
||||||
class ctrlDefaultButton;
|
|
||||||
class RscBackgroundStripeTop;
|
|
||||||
class RscBackgroundStripeBottom;
|
|
||||||
class RscIGText;
|
|
||||||
class RscIGProgress;
|
|
||||||
class RscListBoxKeys;
|
|
||||||
class RscControlsGroupNoScrollbars;
|
|
||||||
class RscControlsGroupNoHScrollbars;
|
|
||||||
class RscControlsGroupNoVScrollbars;
|
|
||||||
class RscLine;
|
|
||||||
class RscActivePicture;
|
|
||||||
class RscButtonTextOnly;
|
|
||||||
class RscShortcutButtonMain;
|
|
||||||
class RscButtonEditor;
|
|
||||||
class RscIGUIShortcutButton;
|
|
||||||
class RscGearShortcutButton;
|
|
||||||
class RscButtonMenu;
|
|
||||||
class RscButtonMenuOK;
|
|
||||||
class RscButtonMenuCancel;
|
|
||||||
class RscButtonMenuSteam;
|
|
||||||
class RscLoadingText;
|
|
||||||
class RscIGUIListBox;
|
|
||||||
class RscIGUIListNBox;
|
|
||||||
class RscBackground;
|
|
||||||
class RscBackgroundGUI;
|
|
||||||
class RscBackgroundGUILeft;
|
|
||||||
class RscBackgroundGUIRight;
|
|
||||||
class RscBackgroundGUIBottom;
|
|
||||||
class RscBackgroundGUITop;
|
|
||||||
class RscBackgroundGUIDark;
|
|
||||||
class RscBackgroundLogo;
|
|
||||||
class RscMapControlEmpty;
|
|
||||||
class RscVignette;
|
|
||||||
class CA_Mainback;
|
|
||||||
class CA_Back;
|
|
||||||
class CA_Title_Back;
|
|
||||||
class CA_Black_Back;
|
|
||||||
class CA_Title;
|
|
||||||
class CA_Logo;
|
|
||||||
class CA_Logo_Small;
|
|
||||||
class CA_RscButton;
|
|
||||||
class CA_RscButton_dialog;
|
|
||||||
class CA_Ok;
|
|
||||||
class CA_Ok_image;
|
|
||||||
class CA_Ok_image2;
|
|
||||||
class CA_Ok_text;
|
|
||||||
class ctrlCheckbox;
|
|
||||||
class ctrlCheckboxBaseline;
|
|
||||||
class ctrlStatic;
|
|
||||||
class ctrlControlsGroupNoScrollbars;
|
|
||||||
class ctrlStructuredText;
|
|
||||||
class RscTextMulti;
|
|
||||||
class RscTreeSearch;
|
|
||||||
class RscVideo;
|
|
||||||
class RscVideoKeepAspect;
|
|
||||||
class RscActivePictureKeepAspect;
|
|
||||||
class RscEditMulti;
|
|
||||||
class RscMapSignalBackground;
|
|
||||||
class RscMapSignalPicture;
|
|
||||||
class RscMapSignalText;
|
|
||||||
class RscColorPicker;
|
|
||||||
class RscInterlacingScreen;
|
|
||||||
class RscFeedback;
|
|
||||||
class RscTrafficLight;
|
|
||||||
class RscButtonSearch;
|
|
||||||
class RscIGUIText;
|
|
||||||
class RscOpticsText;
|
|
||||||
class RscOpticsValue;
|
|
||||||
class RscIGUIValue;
|
|
||||||
class RscButtonMenuMain;
|
|
||||||
class RscButtonTestCentered;
|
|
||||||
class RscDisplaySingleMission_ChallengeOverviewGroup;
|
|
||||||
class RscDisplayDebriefing_RscTextMultiline;
|
|
||||||
class RscDisplayDebriefing_ListGroup;
|
|
||||||
class RscButtonArsenal;
|
|
||||||
class RscTextNoShadow;
|
|
||||||
class RscButtonNoColor;
|
|
||||||
class RscToolboxButton;
|
|
||||||
class ctrlStaticPicture;
|
|
||||||
class ctrlStaticPictureKeepAspect;
|
|
||||||
class ctrlStaticPictureTile;
|
|
||||||
class ctrlStaticFrame;
|
|
||||||
class ctrlStaticLine;
|
|
||||||
class ctrlStaticMulti;
|
|
||||||
class ctrlStaticBackground;
|
|
||||||
class ctrlStaticOverlay;
|
|
||||||
class ctrlStaticTitle;
|
|
||||||
class ctrlStaticFooter;
|
|
||||||
class ctrlStaticBackgroundDisable;
|
|
||||||
class ctrlStaticBackgroundDisableTiles;
|
|
||||||
class ctrlButton;
|
|
||||||
class ctrlButtonPicture;
|
|
||||||
class ctrlButtonPictureKeepAspect;
|
|
||||||
class ctrlButtonOK;
|
|
||||||
class ctrlButtonCancel;
|
|
||||||
class ctrlButtonClose;
|
|
||||||
class ctrlButtonToolbar;
|
|
||||||
class ctrlButtonSearch;
|
|
||||||
class ctrlButtonExpandAll;
|
|
||||||
class ctrlButtonCollapseAll;
|
|
||||||
class ctrlButtonFilter;
|
|
||||||
class ctrlEdit;
|
|
||||||
class ctrlEditMulti;
|
|
||||||
class ctrlSliderV;
|
|
||||||
class ctrlSliderH;
|
|
||||||
class ctrlCombo;
|
|
||||||
class ctrlComboToolbar;
|
|
||||||
class ctrlListbox;
|
|
||||||
class ctrlToolbox;
|
|
||||||
class ctrlToolboxPicture;
|
|
||||||
class ctrlToolboxPictureKeepAspect;
|
|
||||||
class ctrlCheckboxes;
|
|
||||||
class ctrlCheckboxesCheckbox;
|
|
||||||
class ctrlProgress;
|
|
||||||
class ctrlHTML;
|
|
||||||
class ctrlActiveText;
|
|
||||||
class ctrlActivePicture;
|
|
||||||
class ctrlActivePictureKeepAspect;
|
|
||||||
class ctrlTree;
|
|
||||||
class ctrlControlsGroupNoHScrollbars;
|
|
||||||
class ctrlControlsGroupNoVScrollbars;
|
|
||||||
class ctrlShortcutButton;
|
|
||||||
class ctrlShortcutButtonOK;
|
|
||||||
class ctrlShortcutButtonCancel;
|
|
||||||
class ctrlShortcutButtonSteam;
|
|
||||||
class ctrlXListbox;
|
|
||||||
class ctrlXSliderV;
|
|
||||||
class ctrlXSliderH;
|
|
||||||
class ctrlMenu;
|
|
||||||
class ctrlMenuStrip;
|
|
||||||
class ctrlMap;
|
|
||||||
class ctrlMapEmpty;
|
|
||||||
class ctrlMapMain;
|
|
||||||
class ctrlListNBox;
|
|
||||||
class ctrlCheckboxToolbar;
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
class RscPhone {
|
|
||||||
idd = 1000;
|
|
||||||
movingEnable = 1;
|
|
||||||
enableSimulation = 1;
|
|
||||||
duration = 1e011;
|
|
||||||
fadeIn = 0;
|
|
||||||
fadeOut = 0;
|
|
||||||
onLoad = "uiNamespace setVariable ['RscPhone', _this select 0]";
|
|
||||||
|
|
||||||
class controlsBackground {};
|
|
||||||
class controls {
|
|
||||||
class Background: RscText {
|
|
||||||
type = 106;
|
|
||||||
idc = 1001;
|
|
||||||
x = "safezoneX + (safezoneW * 0.4125)";
|
|
||||||
y = "safezoneY + (safezoneH * 0.1)";
|
|
||||||
w = "safezoneW * 1";
|
|
||||||
h = "safezoneH * 1";
|
|
||||||
colorBackground[] = {0, 0, 0, 0};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
# Phone UI Framework
|
|
||||||
|
|
||||||
A lightweight, component-based framework for building phone-like user interfaces in the browser. This framework provides a React-like development experience without external dependencies, making it perfect for creating mobile-first web applications.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Component-based architecture (React-like API)
|
|
||||||
- Virtual DOM-like rendering system
|
|
||||||
- Built-in global and local state management
|
|
||||||
- Modular, maintainable CSS structure
|
|
||||||
- Mobile-first, accessible design (ARIA roles/labels)
|
|
||||||
- No external dependencies
|
|
||||||
- Easy production bundling (JS & CSS)
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
1. Clone the repository
|
|
||||||
2. **On Windows, run the provided script to build and start the local server:**
|
|
||||||
```powershell
|
|
||||||
./start.ps1
|
|
||||||
```
|
|
||||||
This will automatically build the JS and CSS bundles and open the app in your browser at [http://localhost:8000](http://localhost:8000).
|
|
||||||
|
|
||||||
3. **On Linux/macOS, run the provided shell script:**
|
|
||||||
```sh
|
|
||||||
chmod +x start.sh
|
|
||||||
./start.sh
|
|
||||||
```
|
|
||||||
This will automatically build the JS and CSS bundles and open the app in your browser at [http://localhost:8000](http://localhost:8000).
|
|
||||||
|
|
||||||
4. If you prefer, you can run the build manually with `node tools/concat-all.js` and start a local server (e.g., `python3 -m http.server`).
|
|
||||||
|
|
||||||
> **Note:** The app will not work unless you run the build script. Always re-run the build script if you add, remove, or change any JS or CSS files.
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
├── index.html # Main entry point
|
|
||||||
├── dist/ # Production bundles (auto-generated)
|
|
||||||
│ ├── app.bundle.js
|
|
||||||
│ └── app.bundle.css
|
|
||||||
├── styles/ # CSS files
|
|
||||||
│ ├── base.css
|
|
||||||
│ ├── main.css
|
|
||||||
│ └── components/ # Component-specific styles
|
|
||||||
├── js/ # JavaScript files
|
|
||||||
│ ├── core/ # Core framework (Component, StateManager)
|
|
||||||
│ ├── components/ # Shared UI components
|
|
||||||
│ ├── apps/ # App modules (phone, messages, contacts, settings)
|
|
||||||
│ ├── utils/ # Utility functions (scriptLoader, helpers)
|
|
||||||
│ ├── app.js # Main app integration/root
|
|
||||||
│ └── main.js # App initialization
|
|
||||||
├── tools/ # Build and utility scripts
|
|
||||||
│ ├── concat-js.js
|
|
||||||
│ ├── concat-css.js
|
|
||||||
│ └── concat-all.js
|
|
||||||
├── start.ps1 # Windows script to build and start local server
|
|
||||||
├── start.sh # Linux/macOS script to build and start local server
|
|
||||||
└── images/ # Image assets
|
|
||||||
```
|
|
||||||
|
|
||||||
## App Structure
|
|
||||||
|
|
||||||
- **Main App (`App` class in `js/app.js`)**: Handles app switching, global modals, and integration.
|
|
||||||
- **Apps (`js/apps/`)**: Each app (Phone, Messages, Contacts, Settings) has its own entry point (`index.js`) and components.
|
|
||||||
- **Components (`js/components/` and app subfolders)**: Reusable UI elements (NavigationBar, Modal, StatusBar, etc.).
|
|
||||||
- **State Management (`js/core/StateManager.js`)**: Global state via `globalState`, plus local state in components.
|
|
||||||
- **Utilities (`js/utils/`)**: Script loader, helpers, etc.
|
|
||||||
|
|
||||||
## Creating Components
|
|
||||||
|
|
||||||
Components are created by extending the base `Component` class:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
class MyComponent extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = { /* ... */ };
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
return this.createElement('div', { className: 'my-component' }, 'Hello World');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Component Lifecycle
|
|
||||||
- `constructor(props)`: Initialize component
|
|
||||||
- `render()`: Define component structure
|
|
||||||
- `componentDidMount()`: Called after mount
|
|
||||||
- `componentWillUnmount()`: Called before unmount
|
|
||||||
- `onStateChange(prevState, newState)`: On state change
|
|
||||||
|
|
||||||
### State Management
|
|
||||||
- Local: `this.setState({ ... })`
|
|
||||||
- Global: `globalState.setState({ ... })`, `globalState.subscribe(cb)`
|
|
||||||
|
|
||||||
## Creating Elements
|
|
||||||
|
|
||||||
Use `createElement` to create DOM elements:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
this.createElement('div', { className: 'container', onClick: ... }, 'Content');
|
|
||||||
```
|
|
||||||
|
|
||||||
## Styling
|
|
||||||
|
|
||||||
- Base styles: `base.css`, `main.css`
|
|
||||||
- Component styles: `styles/components/`
|
|
||||||
- For all environments, use the bundled `dist/app.bundle.css`
|
|
||||||
|
|
||||||
## Available Components
|
|
||||||
|
|
||||||
- `StatusBar`, `NavigationBar`, `Modal`, `HomeScreen`, `HomeIndicator`, `Header`, `SearchBar`
|
|
||||||
- App-specific: `ContactList`, `ContactItem`, `AddContactForm`, `MessagesList`, `MessageItem`, `ConversationView`, `Dialpad`, `Settings`
|
|
||||||
|
|
||||||
## Scripts
|
|
||||||
|
|
||||||
- `tools/concat-js.js`: Bundles all JS files into `dist/app.bundle.js`
|
|
||||||
- `tools/concat-css.js`: Bundles all CSS files into `dist/app.bundle.css`
|
|
||||||
- `tools/concat-all.js`: Bundles both JS and CSS (**required for all environments**)
|
|
||||||
- `start.ps1`: Builds and starts a local server on Windows
|
|
||||||
- `start.sh`: Builds and starts a local server on Linux/macOS
|
|
||||||
|
|
||||||
## How to Add a New App
|
|
||||||
|
|
||||||
1. Create a new folder in `js/apps/yourapp/` with an `index.js` and any components.
|
|
||||||
2. Add your app's entry point to the bundler scripts and (if needed) to the app switch logic in `js/app.js`.
|
|
||||||
3. Add styles in `styles/components/yourapp.css` and include in the CSS bundle list.
|
|
||||||
4. **Re-run the build script after any changes.**
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. Keep components small and focused
|
|
||||||
2. Use state management for global data
|
|
||||||
3. Follow the component lifecycle
|
|
||||||
4. Use modular CSS for styling
|
|
||||||
5. Handle cleanup in `componentWillUnmount`
|
|
||||||
6. Use ARIA roles/labels for accessibility
|
|
||||||
|
|
||||||
## Development & Production
|
|
||||||
|
|
||||||
- **Always run the build script (`node tools/concat-all.js`, `./start.ps1`, or `./start.sh`) before starting or deploying the app.**
|
|
||||||
- The app will not work unless all JS and CSS are bundled.
|
|
||||||
- If you encounter issues, re-run the build script to ensure all files are up to date.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create your feature branch
|
|
||||||
3. Commit your changes
|
|
||||||
4. Push to the branch
|
|
||||||
5. Create a Pull Request
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the MIT License.
|
|
||||||
2986
arma/client/addons/phone/ui/_site/dist/app.bundle.css
vendored
7928
arma/client/addons/phone/ui/_site/dist/app.bundle.js
vendored
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
@ -1,29 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<script>
|
|
||||||
Promise.all([
|
|
||||||
// Load CSS file
|
|
||||||
A3API.RequestFile("forge\\forge_client\\addons\\phone\\ui\\_site\\dist\\app.bundle.css"),
|
|
||||||
// Load JavaScript file
|
|
||||||
A3API.RequestFile("forge\\forge_client\\addons\\phone\\ui\\_site\\dist\\app.bundle.js")
|
|
||||||
]).then(([css, js]) => {
|
|
||||||
// Apply CSS
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = css;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
|
|
||||||
// Load and execute JavaScript
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.text = js;
|
|
||||||
document.head.appendChild(script);
|
|
||||||
|
|
||||||
// Initialize the phone interface
|
|
||||||
initializeApp();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,324 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Root application component and integration logic.
|
|
||||||
*
|
|
||||||
* The App class manages:
|
|
||||||
* - Switching between different app modules (home, phone, messages, contacts, settings)
|
|
||||||
* - Rendering the correct app UI based on global state
|
|
||||||
* - Handling global modals (e.g., call confirmation)
|
|
||||||
* - Integrating shared UI elements (status bar, home indicator, dynamic island)
|
|
||||||
*
|
|
||||||
* Each app module is initialized via its global function (e.g., window.initializePhoneApp) and mounted into the app container.
|
|
||||||
* The placeholder app view is shown for unimplemented apps.
|
|
||||||
*
|
|
||||||
* This is the main entry point for the phone UI framework.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @class App
|
|
||||||
* @extends Component
|
|
||||||
* @description The root component that manages app switching and integration
|
|
||||||
*/
|
|
||||||
class App extends Component {
|
|
||||||
/**
|
|
||||||
* @constructor
|
|
||||||
* Initializes state and subscribes to global state changes.
|
|
||||||
*/
|
|
||||||
constructor(props = {}) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
...globalState.getState(),
|
|
||||||
currentApp: 'home',
|
|
||||||
showAddContactForm: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this.unsubscribe = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe to global state changes after mounting
|
|
||||||
* @lifecycle
|
|
||||||
*/
|
|
||||||
componentDidMount() {
|
|
||||||
this.unsubscribe = globalState.subscribe((newState) => {
|
|
||||||
this.setState(newState);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up subscriptions before unmounting
|
|
||||||
* @lifecycle
|
|
||||||
*/
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.unsubscribe) {
|
|
||||||
this.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the current app based on app state
|
|
||||||
* @returns {HTMLElement} Current app view
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
renderCurrentApp() {
|
|
||||||
const { currentApp } = this.state;
|
|
||||||
const appContainer = this.createElement('div', { className: 'app-container' });
|
|
||||||
|
|
||||||
switch (currentApp) {
|
|
||||||
case 'clock':
|
|
||||||
window.initializeClockApp(appContainer);
|
|
||||||
break;
|
|
||||||
case 'calendar':
|
|
||||||
window.initializeCalendarApp(appContainer);
|
|
||||||
break;
|
|
||||||
case 'home':
|
|
||||||
return new HomeScreen();
|
|
||||||
case 'phone':
|
|
||||||
window.initializePhoneApp(appContainer);
|
|
||||||
break;
|
|
||||||
case 'messages':
|
|
||||||
window.initializeMessagesApp(appContainer);
|
|
||||||
break;
|
|
||||||
case 'mail':
|
|
||||||
window.initializeMailApp(appContainer);
|
|
||||||
break;
|
|
||||||
case 'notes':
|
|
||||||
window.initializeNotesApp(appContainer);
|
|
||||||
break;
|
|
||||||
case 'contacts':
|
|
||||||
window.initializeContactsApp(appContainer);
|
|
||||||
break;
|
|
||||||
case 'settings':
|
|
||||||
window.initializeSettingsApp(appContainer);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return this.renderPlaceholderApp(currentApp);
|
|
||||||
}
|
|
||||||
|
|
||||||
return appContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render a placeholder for unimplemented apps
|
|
||||||
* @param {string} appName - App name
|
|
||||||
* @returns {HTMLElement} Placeholder app view
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
renderPlaceholderApp(appName) {
|
|
||||||
const appIcons = {
|
|
||||||
calendar: '',
|
|
||||||
camera: '',
|
|
||||||
store: '',
|
|
||||||
mail: '',
|
|
||||||
icloud: '',
|
|
||||||
photos: '',
|
|
||||||
safari: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'app-container' },
|
|
||||||
new NavigationBar({ title: appName }),
|
|
||||||
this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'content' },
|
|
||||||
this.createElement(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '50px 20px',
|
|
||||||
color: '#6c757d',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
this.createElement('h2', { role: 'img', 'aria-label': appName }, appIcons[appName] || ''),
|
|
||||||
this.createElement('p', {}, `${appName} app coming soon!`)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the phone app UI, including status bar, main content, home indicator, and modals.
|
|
||||||
* @returns {HTMLElement} The rendered phone app
|
|
||||||
*/
|
|
||||||
render() {
|
|
||||||
const { currentApp, selectedContact, showModal, showDeleteModal, noteToDelete, eventToDelete } = this.state;
|
|
||||||
const openMessageThread = (contact) => {
|
|
||||||
if (!contact || contact.canMessage === false) return;
|
|
||||||
|
|
||||||
const contactId = contact.contactId || contact.uid || contact.id;
|
|
||||||
if (!contactId) return;
|
|
||||||
|
|
||||||
const { messages = [], rawMessages = [], currentUid = window.__playerUid } = globalState.getState();
|
|
||||||
const existingConversation = messages.find((message) => (message.contactId || message.id) === contactId);
|
|
||||||
const selectedRawMessages = rawMessages.filter((message) =>
|
|
||||||
message &&
|
|
||||||
(
|
|
||||||
(message.from === currentUid && message.to === contactId) ||
|
|
||||||
(message.from === contactId && message.to === currentUid)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const conversation = existingConversation || {
|
|
||||||
...contact,
|
|
||||||
id: contactId,
|
|
||||||
contactId,
|
|
||||||
contactName: contact.fullName || contact.name || contactId,
|
|
||||||
conversation: [],
|
|
||||||
hasConversation: false
|
|
||||||
};
|
|
||||||
|
|
||||||
globalState.setState({
|
|
||||||
currentApp: 'messages',
|
|
||||||
selectedContact: null,
|
|
||||||
showModal: false,
|
|
||||||
showMessageContactPicker: false,
|
|
||||||
selectedConversation: {
|
|
||||||
...conversation,
|
|
||||||
id: contactId,
|
|
||||||
contactId,
|
|
||||||
contactName: conversation.contactName || contact.fullName || contact.name || contactId,
|
|
||||||
conversation: conversation.conversation || []
|
|
||||||
},
|
|
||||||
selectedConversationRaw: {
|
|
||||||
otherUid: contactId,
|
|
||||||
messages: selectedRawMessages
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.createElement(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
className: 'phone-container',
|
|
||||||
role: 'application',
|
|
||||||
'aria-label': 'Phone interface',
|
|
||||||
},
|
|
||||||
this.createElement(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
className: 'phone-screen dynamic-island',
|
|
||||||
role: 'main',
|
|
||||||
},
|
|
||||||
// Dynamic Island content
|
|
||||||
this.createElement(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
className: 'dynamic-island-content',
|
|
||||||
'aria-hidden': 'true',
|
|
||||||
},
|
|
||||||
this.createElement('div', { className: 'speaker' }),
|
|
||||||
this.createElement('div', { className: 'camera' })
|
|
||||||
),
|
|
||||||
|
|
||||||
// Status bar
|
|
||||||
new StatusBar(),
|
|
||||||
|
|
||||||
// Main app content
|
|
||||||
this.renderCurrentApp(),
|
|
||||||
|
|
||||||
// Home indicator (except on home screen)
|
|
||||||
currentApp !== 'home' && new HomeIndicator(),
|
|
||||||
|
|
||||||
// Call modal
|
|
||||||
showModal && selectedContact && new Modal({
|
|
||||||
show: showModal,
|
|
||||||
title: selectedContact.canCall === false ? (selectedContact.fullName || selectedContact.name) : `Call ${selectedContact.fullName || selectedContact.name}?`,
|
|
||||||
confirmText: selectedContact.canCall === false ? 'Close' : 'Call',
|
|
||||||
cancelText: selectedContact.canCall === false ? 'Back' : 'Cancel',
|
|
||||||
hideCancel: true,
|
|
||||||
hideConfirm: selectedContact.canCall === false,
|
|
||||||
extraActions: selectedContact.canMessage === false || !(selectedContact.contactId || selectedContact.uid || selectedContact.id) ? [] : [{
|
|
||||||
text: 'Text',
|
|
||||||
ariaLabel: `Text ${selectedContact.fullName || selectedContact.name}`,
|
|
||||||
className: 'button secondary',
|
|
||||||
onClick: () => openMessageThread(selectedContact)
|
|
||||||
}],
|
|
||||||
onClose: () => globalState.setState({ showModal: false, selectedContact: null }),
|
|
||||||
onConfirm: () => {
|
|
||||||
if (selectedContact.canCall === false) {
|
|
||||||
globalState.setState({ showModal: false, selectedContact: null });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
globalState.setState({
|
|
||||||
phoneNumber: selectedContact.phone,
|
|
||||||
showModal: false,
|
|
||||||
selectedContact: null,
|
|
||||||
currentApp: 'phone'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
this.createElement(
|
|
||||||
'p',
|
|
||||||
{ role: 'alert' },
|
|
||||||
selectedContact.canCall === false
|
|
||||||
? `${selectedContact.fullName || selectedContact.name} is a command broadcast contact. Incoming messages and email are available, but direct calls are disabled.`
|
|
||||||
: `Do you want to call ${selectedContact.fullName || selectedContact.name} at ${selectedContact.phone}?`
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Delete note confirmation modal
|
|
||||||
showDeleteModal && noteToDelete && new Modal({
|
|
||||||
show: showDeleteModal,
|
|
||||||
title: `Delete "${noteToDelete.title}"?`,
|
|
||||||
confirmText: 'Delete',
|
|
||||||
cancelText: 'Cancel',
|
|
||||||
onClose: () => globalState.setState({ showDeleteModal: false, noteToDelete: null }),
|
|
||||||
onConfirm: () => {
|
|
||||||
// Find the onDelete handler from the notes editor and call it
|
|
||||||
const currentState = globalState.getState();
|
|
||||||
const currentNotes = currentState.notes || [];
|
|
||||||
const updatedNotes = currentNotes.filter(n => n.id !== noteToDelete.id);
|
|
||||||
|
|
||||||
globalState.setState({
|
|
||||||
notes: updatedNotes,
|
|
||||||
currentNote: null,
|
|
||||||
showNoteEditor: false,
|
|
||||||
showDeleteModal: false,
|
|
||||||
noteToDelete: null
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete from server
|
|
||||||
if (typeof deleteNote === 'function') {
|
|
||||||
deleteNote(noteToDelete.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Note deleted:', noteToDelete.id);
|
|
||||||
},
|
|
||||||
children: [this.createElement('p', { role: 'alert' }, `Are you sure you want to delete this note? This action cannot be undone.`)]
|
|
||||||
}),
|
|
||||||
|
|
||||||
showDeleteModal && eventToDelete && new Modal({
|
|
||||||
show: showDeleteModal,
|
|
||||||
title: `Delete "${eventToDelete.title}"?`,
|
|
||||||
confirmText: 'Delete',
|
|
||||||
cancelText: 'Cancel',
|
|
||||||
onClose: () => globalState.setState({ showDeleteModal: false, eventToDelete: null }),
|
|
||||||
onConfirm: () => {
|
|
||||||
// Find the onDelete handler from the events editor and call it
|
|
||||||
const currentState = globalState.getState();
|
|
||||||
const currentEvents = currentState.events || [];
|
|
||||||
const updatedEvents = currentEvents.filter(n => n.id !== eventToDelete.id);
|
|
||||||
|
|
||||||
globalState.setState({
|
|
||||||
events: updatedEvents,
|
|
||||||
currentEvent: null,
|
|
||||||
showEventEditor: false,
|
|
||||||
showDeleteModal: false,
|
|
||||||
eventToDelete: null
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete from server
|
|
||||||
if (typeof deleteCalendarEvent === 'function') {
|
|
||||||
deleteCalendarEvent(eventToDelete.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Event deleted:', eventToDelete.id);
|
|
||||||
},
|
|
||||||
children: [this.createElement('p', { role: 'alert' }, `Are you sure you want to delete this event? This action cannot be undone.`)]
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,191 +0,0 @@
|
|||||||
/**
|
|
||||||
* @format
|
|
||||||
* @fileoverview Calendar component for displaying and managing calendar events
|
|
||||||
*/
|
|
||||||
|
|
||||||
class Calendar extends Component {
|
|
||||||
constructor(props = {}) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
let selectedDate = props.selectedDate;
|
|
||||||
if (!(selectedDate instanceof Date) || isNaN(selectedDate.getTime())) {
|
|
||||||
selectedDate = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
currentDate: props.selectedDate || new Date(),
|
|
||||||
selectedDate: props.selectedDate || new Date(),
|
|
||||||
events: props.events || [],
|
|
||||||
};
|
|
||||||
|
|
||||||
this.onEventClick = props.onEventClick;
|
|
||||||
this.onDayClick = props.onDayClick;
|
|
||||||
|
|
||||||
this.handleDayClick = this.handleDayClick.bind(this);
|
|
||||||
this.handleEventClick = this.handleEventClick.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the component is first mounted to the DOM.
|
|
||||||
* Ensures the initial view is rendered.
|
|
||||||
*/
|
|
||||||
componentDidMount() {
|
|
||||||
this.render(); // Initial render after component is mounted
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the component's state or props change.
|
|
||||||
* Updates the component if necessary.
|
|
||||||
*/
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
// Re-render if selectedDate or events have changed significantly
|
|
||||||
if (
|
|
||||||
prevState.selectedDate.toDateString() !== this.state.selectedDate.toDateString() ||
|
|
||||||
JSON.stringify(prevState.events) !== JSON.stringify(this.state.events) ||
|
|
||||||
prevState.currentDate.toDateString() !== this.state.currentDate.toDateString()
|
|
||||||
) {
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { currentDate } = this.state;
|
|
||||||
const year = currentDate.getFullYear();
|
|
||||||
const month = currentDate.getMonth();
|
|
||||||
|
|
||||||
return this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'calendar-container' },
|
|
||||||
|
|
||||||
this.createElement('div', { className: 'calendar-header' }, this.createElement('div', { className: 'calendar-title' }, `${this.getMonthName(month)} ${year}`)),
|
|
||||||
|
|
||||||
this.createElement('div', { className: 'calendar-grid' }, this.renderWeekdays(), this.renderDays(year, month)),
|
|
||||||
|
|
||||||
this.createElement('div', { className: 'calendar-events' }, this.renderEvents())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderWeekdays() {
|
|
||||||
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
||||||
return weekdays.map((day) => this.createElement('div', { className: 'calendar-weekday' }, day));
|
|
||||||
}
|
|
||||||
|
|
||||||
renderDays(year, month) {
|
|
||||||
const firstDay = new Date(year, month, 1);
|
|
||||||
const lastDay = new Date(year, month + 1, 0);
|
|
||||||
const startingDay = firstDay.getDay();
|
|
||||||
const totalDays = lastDay.getDate();
|
|
||||||
|
|
||||||
let days = [];
|
|
||||||
|
|
||||||
// Previous month's days (empty placeholders or actual days if needed, currently empty for visual alignment)
|
|
||||||
for (let i = 0; i < startingDay; i++) {
|
|
||||||
days.push(this.createElement('div', { className: 'calendar-day other-month' }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current month's days
|
|
||||||
for (let day = 1; day <= totalDays; day++) {
|
|
||||||
const date = new Date(year, month, day);
|
|
||||||
const isToday = this.isToday(date);
|
|
||||||
const isSelected = this.isSelected(date);
|
|
||||||
const hasEvents = this.hasEvents(date);
|
|
||||||
|
|
||||||
let classes = ['calendar-day'];
|
|
||||||
if (isToday) classes.push('today');
|
|
||||||
if (isSelected) classes.push('selected');
|
|
||||||
if (hasEvents) classes.push('has-events');
|
|
||||||
|
|
||||||
days.push(
|
|
||||||
this.createElement(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
className: classes.join(' '),
|
|
||||||
'data-date': date.toISOString(),
|
|
||||||
onClick: () => this.handleDayClick(date),
|
|
||||||
},
|
|
||||||
day
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next month's days (empty placeholders for visual alignment)
|
|
||||||
const remainingCells = 42 - days.length; // 42 = 6 rows * 7 days
|
|
||||||
for (let i = 0; i < remainingCells; i++) {
|
|
||||||
days.push(this.createElement('div', { className: 'calendar-day other-month' }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return days;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderEvents() {
|
|
||||||
const events = this.getEventsForDate(this.state.selectedDate);
|
|
||||||
if (!events || events.length === 0) {
|
|
||||||
return this.createElement('div', { className: 'no-events' }, 'No events for this day');
|
|
||||||
}
|
|
||||||
|
|
||||||
return events.map((event) =>
|
|
||||||
this.createElement(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
className: 'event-item',
|
|
||||||
'data-event-id': event.id,
|
|
||||||
onClick: () => this.handleEventClick(event),
|
|
||||||
},
|
|
||||||
this.createElement('div', { className: 'event-dot' }),
|
|
||||||
this.createElement('div', { className: 'event-time' }, this.formatTime(event.startTime)),
|
|
||||||
this.createElement('div', { className: 'event-title' }, event.title)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDayClick(date) {
|
|
||||||
this.setState({ selectedDate: date });
|
|
||||||
|
|
||||||
if (this.onDayClick) {
|
|
||||||
this.onDayClick(date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEventClick(event) {
|
|
||||||
if (this.onEventClick) {
|
|
||||||
this.onEventClick(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getEventsForDate(date) {
|
|
||||||
const dateKey = this.getDateKey(date);
|
|
||||||
return this.state.events.filter((event) => {
|
|
||||||
const eventStartDate = new Date(event.startTime);
|
|
||||||
return this.getDateKey(eventStartDate) === dateKey;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
hasEvents(date) {
|
|
||||||
return this.getEventsForDate(date).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDateKey(date) {
|
|
||||||
return date.toISOString().split('T')[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
isToday(date) {
|
|
||||||
const today = new Date();
|
|
||||||
return date.toDateString() === today.toDateString();
|
|
||||||
}
|
|
||||||
|
|
||||||
isSelected(date) {
|
|
||||||
return date.toDateString() === this.state.selectedDate.toDateString();
|
|
||||||
}
|
|
||||||
|
|
||||||
getMonthName(month) {
|
|
||||||
return new Date(2000, month, 1).toLocaleString('default', { month: 'long' });
|
|
||||||
}
|
|
||||||
|
|
||||||
formatTime(time) {
|
|
||||||
return new Date(time).toLocaleTimeString('default', {
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,290 +0,0 @@
|
|||||||
/**
|
|
||||||
* @format
|
|
||||||
* @class EventEditor
|
|
||||||
* @extends Component
|
|
||||||
* @description A component for creating and editing calendar events.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class EventEditor extends Component {
|
|
||||||
/**
|
|
||||||
* @constructor
|
|
||||||
* @param {Object} props - Component properties
|
|
||||||
* @param {Object} [props.event] - Existing event to edit
|
|
||||||
* @param {Function} props.onSave - Callback when event is saved
|
|
||||||
* @param {Function} props.onCancel - Callback when editing is cancelled
|
|
||||||
* @param {Function} [props.onDelete] - Callback when event is deleted
|
|
||||||
*/
|
|
||||||
constructor(props = {}) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const existingEvent = props.event || {
|
|
||||||
title: '',
|
|
||||||
startTime: new Date(),
|
|
||||||
endTime: new Date(new Date().getTime() + 60 * 60 * 1000),
|
|
||||||
description: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
title: existingEvent.title || '',
|
|
||||||
startTime: this.formatDateTimeForInput(existingEvent.startTime),
|
|
||||||
endTime: this.formatDateTimeForInput(existingEvent.endTime),
|
|
||||||
description: existingEvent.description || '',
|
|
||||||
id: existingEvent.id || null,
|
|
||||||
isModified: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// References for DOM elements
|
|
||||||
this.titleInputRef = null;
|
|
||||||
this.startTimeInputRef = null;
|
|
||||||
this.endTimeInputRef = null;
|
|
||||||
this.descriptionInputRef = null;
|
|
||||||
|
|
||||||
// Bind methods
|
|
||||||
this.handleTitleChange = this.handleTitleChange.bind(this);
|
|
||||||
this.handleStartTimeChange = this.handleStartTimeChange.bind(this);
|
|
||||||
this.handleEndTimeChange = this.handleEndTimeChange.bind(this);
|
|
||||||
this.handleDescriptionChange = this.handleDescriptionChange.bind(this);
|
|
||||||
this.handleSave = this.handleSave.bind(this);
|
|
||||||
this.handleCancel = this.handleCancel.bind(this);
|
|
||||||
this.handleDelete = this.handleDelete.bind(this);
|
|
||||||
this.setTitleInputRef = this.setTitleInputRef.bind(this);
|
|
||||||
this.setStartTimeInputRef = this.setStartTimeInputRef.bind(this);
|
|
||||||
this.setEndTimeInputRef = this.setEndTimeInputRef.bind(this);
|
|
||||||
this.setDescriptionInputRef = this.setDescriptionInputRef.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component mounted - focus on title if new event
|
|
||||||
*/
|
|
||||||
componentDidMount() {
|
|
||||||
if (!this.state.id && this.titleInputRef) {
|
|
||||||
this.titleInputRef.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ref setter methods
|
|
||||||
setTitleInputRef(element) {
|
|
||||||
if (element) {
|
|
||||||
this.titleInputRef = element;
|
|
||||||
if (this.state.title && element.value !== this.state.title) {
|
|
||||||
element.value = this.state.title;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setStartTimeInputRef(element) {
|
|
||||||
if (element) {
|
|
||||||
this.startTimeInputRef = element;
|
|
||||||
if (this.state.startTime && element.value !== this.state.startTime) {
|
|
||||||
element.value = this.state.startTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setEndTimeInputRef(element) {
|
|
||||||
if (element) {
|
|
||||||
this.endTimeInputRef = element;
|
|
||||||
if (this.state.endTime && element.value !== this.state.endTime) {
|
|
||||||
element.value = this.state.endTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setDescriptionInputRef(element) {
|
|
||||||
if (element) {
|
|
||||||
this.descriptionInputRef = element;
|
|
||||||
if (this.state.description && element.value !== this.state.description) {
|
|
||||||
element.value = this.state.description;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input change handlers
|
|
||||||
handleTitleChange(e) {
|
|
||||||
this.state.title = e.target.value;
|
|
||||||
this.state.isModified = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleStartTimeChange(e) {
|
|
||||||
this.state.startTime = e.target.value;
|
|
||||||
this.state.isModified = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEndTimeChange(e) {
|
|
||||||
this.state.endTime = e.target.value;
|
|
||||||
this.state.isModified = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDescriptionChange(e) {
|
|
||||||
this.state.description = e.target.value;
|
|
||||||
this.state.isModified = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSave() {
|
|
||||||
const { title, startTime, endTime, description, id } = this.state;
|
|
||||||
|
|
||||||
// if (!title.trim() || !startTime || !endTime) {
|
|
||||||
// alert('Please fill in all required fields.');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
const savedEvent = {
|
|
||||||
id: id || generateId(),
|
|
||||||
title: title.trim(),
|
|
||||||
startTime: new Date(startTime),
|
|
||||||
endTime: new Date(endTime),
|
|
||||||
description: description.trim(),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isModified: false,
|
|
||||||
id: savedEvent.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.props.onSave) {
|
|
||||||
this.props.onSave(savedEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancel() {
|
|
||||||
if (this.props.onCancel) {
|
|
||||||
this.props.onCancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDelete() {
|
|
||||||
if (!this.state.id) {
|
|
||||||
console.warn('Cannot delete event: no ID present');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.props.onDelete) {
|
|
||||||
console.warn('Cannot delete event: no onDelete callback provided');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Show delete confirmation modal using global state
|
|
||||||
globalState.setState({
|
|
||||||
showDeleteModal: true,
|
|
||||||
eventToDelete: {
|
|
||||||
id: this.state.id,
|
|
||||||
title: this.state.title || 'Untitled',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error showing delete confirmation:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formatDateTimeForInput(date) {
|
|
||||||
// Make sure date is a valid Date object
|
|
||||||
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
|
||||||
// If it's a string that looks like a date, try to parse it
|
|
||||||
if (typeof date === 'string') {
|
|
||||||
date = new Date(date);
|
|
||||||
}
|
|
||||||
// If still not valid, return current time
|
|
||||||
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
|
||||||
date = new Date();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return date.toISOString().slice(0, 16); // Format: YYYY-MM-DDTHH:mm
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { title, startTime, endTime, description, id } = this.state;
|
|
||||||
|
|
||||||
return this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'event-editor' },
|
|
||||||
|
|
||||||
// Navigation bar
|
|
||||||
new NavigationBar({
|
|
||||||
title: id ? 'Edit Event' : 'New Event',
|
|
||||||
leftButton: {
|
|
||||||
element: 'button',
|
|
||||||
props: {
|
|
||||||
className: 'nav-button cancel-button',
|
|
||||||
onClick: this.handleCancel,
|
|
||||||
'aria-label': 'Cancel',
|
|
||||||
},
|
|
||||||
content: 'Cancel',
|
|
||||||
},
|
|
||||||
rightButton: {
|
|
||||||
element: 'button',
|
|
||||||
props: {
|
|
||||||
className: 'nav-button save-button',
|
|
||||||
onClick: this.handleSave,
|
|
||||||
'aria-label': 'Save event',
|
|
||||||
},
|
|
||||||
content: 'Save',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Editor content
|
|
||||||
this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'event-form' },
|
|
||||||
|
|
||||||
// Title input
|
|
||||||
this.createElement('input', {
|
|
||||||
type: 'text',
|
|
||||||
className: 'event-title-input',
|
|
||||||
placeholder: 'Event title...',
|
|
||||||
value: title,
|
|
||||||
onInput: this.handleTitleChange,
|
|
||||||
ref: this.setTitleInputRef,
|
|
||||||
required: true,
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Time inputs container
|
|
||||||
this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'time-container' },
|
|
||||||
|
|
||||||
// Start time input
|
|
||||||
this.createElement('input', {
|
|
||||||
type: 'datetime-local',
|
|
||||||
className: 'time-input',
|
|
||||||
value: startTime,
|
|
||||||
onInput: this.handleStartTimeChange,
|
|
||||||
ref: this.setStartTimeInputRef,
|
|
||||||
required: true,
|
|
||||||
}),
|
|
||||||
|
|
||||||
// End time input
|
|
||||||
this.createElement('input', {
|
|
||||||
type: 'datetime-local',
|
|
||||||
className: 'time-input',
|
|
||||||
value: endTime,
|
|
||||||
onInput: this.handleEndTimeChange,
|
|
||||||
ref: this.setEndTimeInputRef,
|
|
||||||
required: true,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
|
|
||||||
// Description textarea
|
|
||||||
this.createElement('textarea', {
|
|
||||||
className: 'event-description-input',
|
|
||||||
placeholder: 'Add description...',
|
|
||||||
value: description,
|
|
||||||
onInput: this.handleDescriptionChange,
|
|
||||||
ref: this.setDescriptionInputRef,
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Delete button (only for existing events)
|
|
||||||
id &&
|
|
||||||
this.createElement(
|
|
||||||
'button',
|
|
||||||
{
|
|
||||||
type: 'button',
|
|
||||||
className: 'delete-event-button',
|
|
||||||
onClick: this.handleDelete,
|
|
||||||
},
|
|
||||||
'Delete Event'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Main entry point for the Calendar application
|
|
||||||
*
|
|
||||||
* This module initializes the Calendar app UI, including:
|
|
||||||
* - Displaying the calendar view
|
|
||||||
* - Handling event creation, editing, and deletion via EventEditor
|
|
||||||
* - Managing event persistence via A3API
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes and mounts the Calendar application.
|
|
||||||
* @param {HTMLElement} container - The DOM element to mount the app into.
|
|
||||||
*/
|
|
||||||
function initializeCalendarApp(container) {
|
|
||||||
const { events = [], selectedDate = new Date(), showEventEditor = false, currentEvent = null } = globalState.getState();
|
|
||||||
const appContainer = document.createElement('div');
|
|
||||||
|
|
||||||
appContainer.className = 'app-container';
|
|
||||||
appContainer.setAttribute('role', 'main');
|
|
||||||
appContainer.setAttribute('aria-label', 'Calendar');
|
|
||||||
|
|
||||||
// Check if we're viewing/editing a specific event
|
|
||||||
if (showEventEditor || currentEvent) {
|
|
||||||
// Show event editor
|
|
||||||
const eventEditor = new EventEditor({
|
|
||||||
event: currentEvent,
|
|
||||||
onSave: (savedEvent) => {
|
|
||||||
const currentEvents = globalState.getState().events || [];
|
|
||||||
let updatedEvents;
|
|
||||||
|
|
||||||
if (savedEvent.id && currentEvents.find(e => e.id === savedEvent.id)) {
|
|
||||||
// Update existing event
|
|
||||||
updatedEvents = currentEvents.map(e => e.id === savedEvent.id ? savedEvent : e);
|
|
||||||
} else {
|
|
||||||
// Add new event
|
|
||||||
updatedEvents = [savedEvent, ...currentEvents];
|
|
||||||
}
|
|
||||||
|
|
||||||
globalState.setState({
|
|
||||||
events: updatedEvents,
|
|
||||||
currentEvent: null,
|
|
||||||
showEventEditor: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save to server
|
|
||||||
if (typeof saveCalendarEvent === 'function') {
|
|
||||||
saveCalendarEvent(savedEvent);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onCancel: () => {
|
|
||||||
globalState.setState({
|
|
||||||
currentEvent: null,
|
|
||||||
showEventEditor: false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onDelete: (eventId) => {
|
|
||||||
const currentEvents = globalState.getState().events || [];
|
|
||||||
const updatedEvents = currentEvents.filter(e => e.id !== eventId);
|
|
||||||
|
|
||||||
globalState.setState({
|
|
||||||
events: updatedEvents,
|
|
||||||
currentEvent: null,
|
|
||||||
showEventEditor: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete from server
|
|
||||||
if (typeof deleteCalendarEvent === 'function') {
|
|
||||||
deleteCalendarEvent(eventId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
eventEditor.mount(appContainer);
|
|
||||||
} else {
|
|
||||||
// Show calendar view
|
|
||||||
const navBar = new NavigationBar({
|
|
||||||
title: 'Calendar',
|
|
||||||
rightButton: {
|
|
||||||
element: 'button',
|
|
||||||
props: {
|
|
||||||
className: 'nav-button add-event-button',
|
|
||||||
onClick: () => {
|
|
||||||
globalState.setState({
|
|
||||||
showEventEditor: true,
|
|
||||||
currentEvent: null
|
|
||||||
});
|
|
||||||
},
|
|
||||||
'aria-label': 'Add Event'
|
|
||||||
},
|
|
||||||
content: '+'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
navBar.mount(appContainer);
|
|
||||||
|
|
||||||
const calendar = new Calendar({
|
|
||||||
selectedDate: selectedDate,
|
|
||||||
events: events,
|
|
||||||
onDayClick: (date) => {
|
|
||||||
globalState.setState({
|
|
||||||
selectedDate: date,
|
|
||||||
currentEvent: null,
|
|
||||||
showEventEditor: false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onEventClick: (event) => {
|
|
||||||
globalState.setState({
|
|
||||||
currentEvent: event,
|
|
||||||
showEventEditor: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
calendar.mount(appContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
container.appendChild(appContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make initialization function globally available
|
|
||||||
window.initializeCalendarApp = initializeCalendarApp;
|
|
||||||
@ -1,218 +0,0 @@
|
|||||||
/**
|
|
||||||
* @format
|
|
||||||
* @class AlarmClock
|
|
||||||
* @extends Component
|
|
||||||
* @description A component for managing alarms.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class AlarmClock extends Component {
|
|
||||||
/**
|
|
||||||
* @constructor
|
|
||||||
* @param {Object} props - Component properties
|
|
||||||
*/
|
|
||||||
constructor(props = {}) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
showAddForm: false,
|
|
||||||
newAlarmTime: '07:00',
|
|
||||||
newAlarmLabel: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
// Bind methods
|
|
||||||
this.toggleAddForm = this.toggleAddForm.bind(this);
|
|
||||||
this.handleAddAlarm = this.handleAddAlarm.bind(this);
|
|
||||||
this.formatTime = this.formatTime.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle add alarm form
|
|
||||||
*/
|
|
||||||
toggleAddForm() {
|
|
||||||
// Use setState for form visibility changes as they need re-render
|
|
||||||
this.setState({
|
|
||||||
showAddForm: !this.state.showAddForm,
|
|
||||||
newAlarmTime: '07:00',
|
|
||||||
newAlarmLabel: ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle adding a new alarm
|
|
||||||
*/
|
|
||||||
handleAddAlarm() {
|
|
||||||
const newAlarmTime = this.state.newAlarmTime;
|
|
||||||
const newAlarmLabel = this.state.newAlarmLabel;
|
|
||||||
if (newAlarmTime && this.props.onAddAlarm) {
|
|
||||||
this.props.onAddAlarm({
|
|
||||||
time: newAlarmTime,
|
|
||||||
label: newAlarmLabel || 'Alarm',
|
|
||||||
enabled: true,
|
|
||||||
days: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'] // Default to weekdays
|
|
||||||
});
|
|
||||||
// Use setState to hide form and reset state
|
|
||||||
this.setState({
|
|
||||||
showAddForm: false,
|
|
||||||
newAlarmTime: '07:00',
|
|
||||||
newAlarmLabel: ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format time for display
|
|
||||||
*/
|
|
||||||
formatTime(timeString) {
|
|
||||||
const [hours, minutes] = timeString.split(':');
|
|
||||||
if (this.props.format24h) {
|
|
||||||
return `${hours}:${minutes}`;
|
|
||||||
} else {
|
|
||||||
const hour = parseInt(hours);
|
|
||||||
const ampm = hour >= 12 ? 'PM' : 'AM';
|
|
||||||
const displayHour = hour % 12 || 12;
|
|
||||||
return `${displayHour}:${minutes} ${ampm}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render add alarm form
|
|
||||||
*/
|
|
||||||
renderAddForm() {
|
|
||||||
if (!this.state.showAddForm) return null;
|
|
||||||
|
|
||||||
return this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'add-alarm-form' },
|
|
||||||
this.createElement('h3', {}, 'Add Alarm'),
|
|
||||||
|
|
||||||
this.createElement('input', {
|
|
||||||
type: 'time',
|
|
||||||
value: this.state.newAlarmTime,
|
|
||||||
onChange: (e) => {
|
|
||||||
// Update state directly to avoid re-render during input
|
|
||||||
this.state.newAlarmTime = e.target.value;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
this.createElement('input', {
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'Alarm label (optional)',
|
|
||||||
value: this.state.newAlarmLabel,
|
|
||||||
onChange: (e) => {
|
|
||||||
// Update state directly to avoid re-render during input
|
|
||||||
this.state.newAlarmLabel = e.target.value;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'form-buttons' },
|
|
||||||
this.createElement(
|
|
||||||
'button',
|
|
||||||
{ onClick: this.toggleAddForm },
|
|
||||||
'Cancel'
|
|
||||||
),
|
|
||||||
this.createElement(
|
|
||||||
'button',
|
|
||||||
{ onClick: this.handleAddAlarm },
|
|
||||||
'Add Alarm'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render alarms list
|
|
||||||
*/
|
|
||||||
renderAlarms() {
|
|
||||||
const { alarms } = this.props;
|
|
||||||
|
|
||||||
if (!alarms || alarms.length === 0) {
|
|
||||||
return this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'empty-state' },
|
|
||||||
this.createElement('p', {}, 'No alarms set. Tap + to add one.')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'alarms-list' },
|
|
||||||
...alarms.map(alarm =>
|
|
||||||
this.createElement(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
className: `alarm-item ${alarm.enabled ? 'enabled' : 'disabled'}`,
|
|
||||||
key: alarm.id
|
|
||||||
},
|
|
||||||
this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'alarm-info' },
|
|
||||||
this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'alarm-time' },
|
|
||||||
this.formatTime(alarm.time)
|
|
||||||
),
|
|
||||||
this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'alarm-label' },
|
|
||||||
alarm.label
|
|
||||||
),
|
|
||||||
alarm.days && this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'alarm-days' },
|
|
||||||
alarm.days.join(', ')
|
|
||||||
)
|
|
||||||
),
|
|
||||||
this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'alarm-controls' },
|
|
||||||
this.createElement(
|
|
||||||
'button',
|
|
||||||
{
|
|
||||||
className: 'toggle-alarm',
|
|
||||||
onClick: () => this.props.onToggleAlarm(alarm.id)
|
|
||||||
},
|
|
||||||
alarm.enabled ? 'On' : 'Off'
|
|
||||||
),
|
|
||||||
this.createElement(
|
|
||||||
'button',
|
|
||||||
{
|
|
||||||
className: 'remove-alarm',
|
|
||||||
onClick: () => this.props.onRemoveAlarm(alarm.id),
|
|
||||||
'aria-label': 'Delete alarm'
|
|
||||||
},
|
|
||||||
'Delete'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the alarm clock component
|
|
||||||
*/
|
|
||||||
render() {
|
|
||||||
return this.createElement(
|
|
||||||
'div',
|
|
||||||
{ className: 'alarm-clock' },
|
|
||||||
|
|
||||||
// Add alarm button
|
|
||||||
!this.state.showAddForm && this.createElement(
|
|
||||||
'button',
|
|
||||||
{
|
|
||||||
className: 'add-alarm-button',
|
|
||||||
onClick: this.toggleAddForm
|
|
||||||
},
|
|
||||||
'+ Add Alarm'
|
|
||||||
),
|
|
||||||
|
|
||||||
// Add alarm form
|
|
||||||
this.renderAddForm(),
|
|
||||||
|
|
||||||
// Alarms list
|
|
||||||
this.renderAlarms()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||