mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 22:01:40 +00:00
ADD - initial commit
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# sky_phone
|
||||
|
||||
FiveM phone scaffold for Sky-Systems, using Vue 3 and Konsta UI.
|
||||
|
||||
## Development
|
||||
|
||||
```powershell
|
||||
cd frontend
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
The development command starts Vite and a small mock server for NUI callbacks. The phone opens
|
||||
automatically in browser development mode.
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
cd frontend
|
||||
pnpm build
|
||||
```
|
||||
|
||||
The production build is published to `sky_phone/source/html/`. In FiveM, ensure `sky_base` before
|
||||
`sky_phone` and use `/phone` to toggle the UI.
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.local
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const frontendRoot = __dirname
|
||||
const sourceDirectory = path.join(frontendRoot, 'dist')
|
||||
const targetDirectory = path.join(
|
||||
frontendRoot,
|
||||
'..',
|
||||
'sky_phone',
|
||||
'source',
|
||||
'html',
|
||||
)
|
||||
|
||||
if (!fs.existsSync(sourceDirectory)) {
|
||||
throw new Error(
|
||||
'Missing frontend/dist. Run vite build before publishing the NUI.',
|
||||
)
|
||||
}
|
||||
|
||||
fs.rmSync(targetDirectory, { force: true, recursive: true })
|
||||
fs.mkdirSync(targetDirectory, { recursive: true })
|
||||
fs.cpSync(sourceDirectory, targetDirectory, { recursive: true })
|
||||
|
||||
const targetIndex = path.join(targetDirectory, 'index.html')
|
||||
const normalizedIndex = fs
|
||||
.readFileSync(targetIndex, 'utf8')
|
||||
.replace(/\r\n?/g, '\n')
|
||||
.replace(/\n[ \t]*\n([ \t]*<\/body>)/g, '\n$1')
|
||||
fs.writeFileSync(targetIndex, normalizedIndex)
|
||||
|
||||
console.log(`Published NUI to ${targetDirectory}`)
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface Window {
|
||||
GetParentResourceName?: () => string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
{ files: ['**/*.{ts,mts,tsx,vue}'], name: 'app/files-to-lint' },
|
||||
globalIgnores(['**/dist/**', 'testserver/**', 'build.cjs']),
|
||||
pluginVue.configs['flat/essential'],
|
||||
vueTsConfigs.recommended,
|
||||
skipFormatting,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sky Phone</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "sky-phone-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently -k -n FRONTEND,BACKEND -c cyan,green \"vite\" \"node testserver/index.cjs\"",
|
||||
"build": "pnpm typecheck && pnpm build-only",
|
||||
"build-only": "vite build && node build.cjs",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --build",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write src/ testserver/ build.cjs",
|
||||
"test": "node --test"
|
||||
},
|
||||
"dependencies": {
|
||||
"konsta": "~5.2.0",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.17",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@types/node": "^22.15.32",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.5.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"concurrently": "^9.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-plugin-vue": "~10.2.0",
|
||||
"prettier": "3.5.3",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "~5.8.0",
|
||||
"vite": "^7.0.0",
|
||||
"vue-tsc": "^2.2.10"
|
||||
}
|
||||
}
|
||||
Generated
+2876
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { kApp } from 'konsta/vue'
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
|
||||
import { usePhoneStore, type PhoneOpenPayload } from '@/stores/phone'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
|
||||
type AppMessage = {
|
||||
type?: string
|
||||
data?: PhoneOpenPayload
|
||||
}
|
||||
|
||||
const phone = usePhoneStore()
|
||||
|
||||
function onMessage(event: MessageEvent<AppMessage>): void {
|
||||
if (event.data?.type === 'app:open') {
|
||||
phone.open(event.data.data)
|
||||
} else if (event.data?.type === 'app:close') {
|
||||
phone.close()
|
||||
}
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent): void {
|
||||
if (event.key !== 'Escape' || !phone.isOpen) return
|
||||
|
||||
phone.close()
|
||||
void nuiCall('close')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', onMessage)
|
||||
window.addEventListener('keydown', onKeydown)
|
||||
void nuiCall('ui:ready')
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
phone.open()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('message', onMessage)
|
||||
window.removeEventListener('keydown', onKeydown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main v-if="phone.isOpen" class="phone-stage">
|
||||
<section class="phone-device" aria-label="Phone preview">
|
||||
<div class="phone-device__speaker" aria-hidden="true"></div>
|
||||
<div class="phone-screen">
|
||||
<k-app theme="ios" safe-areas class="phone-app">
|
||||
<RouterView />
|
||||
</k-app>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
@@ -0,0 +1,111 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'konsta/vue/theme.css';
|
||||
|
||||
@theme {
|
||||
--color-brand-primary: #007aff;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Arial,
|
||||
sans-serif;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
button,
|
||||
a {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.phone-stage {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.phone-device {
|
||||
position: relative;
|
||||
width: min(39vh, 390px);
|
||||
aspect-ratio: 390 / 844;
|
||||
padding: min(1.05vh, 10px);
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
background: linear-gradient(145deg, #303238 0%, #090a0d 38%, #25272d 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: min(5.2vh, 52px);
|
||||
box-shadow:
|
||||
0 4vh 10vh rgba(0, 0, 0, 0.55),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.phone-device__speaker {
|
||||
position: absolute;
|
||||
top: min(2.1vh, 21px);
|
||||
left: 50%;
|
||||
z-index: 20;
|
||||
width: min(9vh, 90px);
|
||||
height: min(2.8vh, 28px);
|
||||
transform: translateX(-50%);
|
||||
background: #08090b;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 999px;
|
||||
box-shadow: inset 0 -1px 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.phone-screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #f2f2f7;
|
||||
border-radius: min(4.25vh, 42px);
|
||||
}
|
||||
|
||||
.phone-app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.phone-home {
|
||||
height: 100%;
|
||||
padding-top: min(2.8vh, 28px);
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 20% 22%,
|
||||
rgba(66, 165, 245, 0.28),
|
||||
transparent 38%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 80% 58%,
|
||||
rgba(175, 82, 222, 0.2),
|
||||
transparent 42%
|
||||
),
|
||||
linear-gradient(160deg, #f8fbff 0%, #edf0f8 48%, #f8f3fb 100%);
|
||||
}
|
||||
|
||||
.phone-home__content {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
align-content: start;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createPinia } from 'pinia'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './assets/main.css'
|
||||
|
||||
createApp(App).use(createPinia()).use(router).mount('#app')
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
import PhoneHomeView from '@/views/PhoneHomeView.vue'
|
||||
|
||||
export default createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
component: PhoneHomeView,
|
||||
path: '/',
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
type LocaleTree = Record<string, unknown>
|
||||
|
||||
export type PhoneOpenPayload = {
|
||||
lang?: string
|
||||
locales?: LocaleTree
|
||||
}
|
||||
|
||||
const defaultLocales: LocaleTree = {
|
||||
Phone: {
|
||||
close: 'Close',
|
||||
readyBody: 'Konsta UI is connected to the FiveM NUI bridge.',
|
||||
readyTitle: 'Boilerplate ready',
|
||||
title: 'Phone',
|
||||
},
|
||||
}
|
||||
|
||||
function getByPath(source: LocaleTree, path: string): unknown {
|
||||
return path.split('.').reduce<unknown>((current, segment) => {
|
||||
if (
|
||||
typeof current !== 'object' ||
|
||||
current === null ||
|
||||
Array.isArray(current)
|
||||
)
|
||||
return undefined
|
||||
return (current as LocaleTree)[segment]
|
||||
}, source)
|
||||
}
|
||||
|
||||
export const usePhoneStore = defineStore('phone', {
|
||||
state: () => ({
|
||||
isOpen: false,
|
||||
lang: 'en',
|
||||
locales: defaultLocales,
|
||||
}),
|
||||
actions: {
|
||||
close(): void {
|
||||
this.isOpen = false
|
||||
},
|
||||
open(payload: PhoneOpenPayload = {}): void {
|
||||
this.lang = payload.lang ?? 'en'
|
||||
this.locales = payload.locales ?? defaultLocales
|
||||
this.isOpen = true
|
||||
},
|
||||
t(path: string): string {
|
||||
const translated = getByPath(this.locales, path)
|
||||
const fallback = getByPath(defaultLocales, path)
|
||||
return typeof translated === 'string'
|
||||
? translated
|
||||
: typeof fallback === 'string'
|
||||
? fallback
|
||||
: path
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
const resourceName = window.GetParentResourceName?.() ?? 'sky_phone'
|
||||
|
||||
export type NuiResponse<T = unknown> = {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
}
|
||||
|
||||
export async function nuiCall<T = unknown>(
|
||||
endpoint: string,
|
||||
data: Record<string, unknown> = {},
|
||||
): Promise<NuiResponse<T>> {
|
||||
const baseUrl = import.meta.env.DEV
|
||||
? 'http://localhost:3001/api'
|
||||
: `https://${resourceName}`
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/${endpoint}`, {
|
||||
body: JSON.stringify(data),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = `${response.status} ${response.statusText}`
|
||||
console.error(`[NUI] ${endpoint} failed: ${error}`)
|
||||
return { error, success: false }
|
||||
}
|
||||
|
||||
const body = await response.text()
|
||||
return body ? (JSON.parse(body) as NuiResponse<T>) : { success: true }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
console.error(`[NUI] ${endpoint} failed:`, error)
|
||||
return { error: message, success: false }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { kBlock, kButton, kGlass, kNavbar, kPage } from 'konsta/vue'
|
||||
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
|
||||
const phone = usePhoneStore()
|
||||
|
||||
function closePhone(): void {
|
||||
phone.close()
|
||||
void nuiCall('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<k-page class="phone-home">
|
||||
<k-navbar :title="phone.t('Phone.title')" class="top-0 sticky" />
|
||||
|
||||
<div class="phone-home__content">
|
||||
<k-glass class="mx-4 rounded-3xl p-4">
|
||||
<h1 class="mb-2 text-xl font-semibold">
|
||||
{{ phone.t('Phone.readyTitle') }}
|
||||
</h1>
|
||||
<p class="text-sm leading-6 text-black/60">
|
||||
{{ phone.t('Phone.readyBody') }}
|
||||
</p>
|
||||
</k-glass>
|
||||
|
||||
<k-block inset>
|
||||
<k-button large rounded @click="closePhone">
|
||||
{{ phone.t('Phone.close') }}
|
||||
</k-button>
|
||||
</k-block>
|
||||
</div>
|
||||
</k-page>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
const cors = require('cors')
|
||||
const express = require('express')
|
||||
|
||||
const app = express()
|
||||
const port = 3001
|
||||
|
||||
app.use(cors())
|
||||
app.use(express.json())
|
||||
|
||||
app.post('/api/:endpoint', (request, response) => {
|
||||
console.log(`[NUI] ${request.params.endpoint}`, request.body)
|
||||
response.json({ success: true })
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Mock NUI server listening on http://localhost:${port}`)
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"include": ["vite.config.ts"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
build: {
|
||||
assetsDir: 'assets',
|
||||
emptyOutDir: true,
|
||||
outDir: 'dist',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: 'assets/sky-[name]-[hash].[ext]',
|
||||
chunkFileNames: 'assets/sky-[name]-[hash].js',
|
||||
entryFileNames: 'assets/sky-[name]-[hash].js',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [tailwindcss(), vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
Config.Command = "phone"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Config = Config or {}
|
||||
Locales = Locales or {}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
Locales["en"] = {
|
||||
CommandDescription = "Open your phone.",
|
||||
Nui = {
|
||||
Phone = {
|
||||
title = "Phone",
|
||||
readyTitle = "Boilerplate ready",
|
||||
readyBody = "Konsta UI is connected to the FiveM NUI bridge.",
|
||||
close = "Close",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
||||
lua54 'yes'
|
||||
|
||||
author 'Sky-Systems'
|
||||
description 'Sky Phone'
|
||||
version '0.1.0'
|
||||
|
||||
escrow_ignore 'config/**'
|
||||
|
||||
shared_scripts {
|
||||
'@sky_base/source/import.lua',
|
||||
'config/init.lua',
|
||||
}
|
||||
|
||||
client_scripts {
|
||||
'config/config.lua',
|
||||
'config/locales/*.lua',
|
||||
'source/client/main.lua',
|
||||
}
|
||||
|
||||
files {
|
||||
'source/html/index.html',
|
||||
'source/html/assets/**',
|
||||
}
|
||||
|
||||
ui_page 'source/html/index.html'
|
||||
|
||||
dependency 'sky_base'
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
local is_open = false
|
||||
|
||||
local function get_locale()
|
||||
return Locales[Sky.Config.locale] or Locales["en"]
|
||||
end
|
||||
|
||||
local function send_open_message()
|
||||
SendNUIMessage({
|
||||
type = "app:open",
|
||||
data = {
|
||||
lang = Sky.Config.locale,
|
||||
locales = get_locale().Nui,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
local function open_phone()
|
||||
if is_open then
|
||||
return
|
||||
end
|
||||
|
||||
is_open = true
|
||||
SetNuiFocus(true, true)
|
||||
send_open_message()
|
||||
end
|
||||
|
||||
local function close_phone()
|
||||
if not is_open then
|
||||
return
|
||||
end
|
||||
|
||||
is_open = false
|
||||
SetNuiFocus(false, false)
|
||||
SendNUIMessage({ type = "app:close" })
|
||||
end
|
||||
|
||||
RegisterCommand(Config.Command, function()
|
||||
if is_open then
|
||||
close_phone()
|
||||
return
|
||||
end
|
||||
|
||||
open_phone()
|
||||
end, false)
|
||||
|
||||
RegisterNUICallback("ui:ready", function(_, cb)
|
||||
if is_open then
|
||||
send_open_message()
|
||||
end
|
||||
|
||||
cb({ success = true })
|
||||
end)
|
||||
|
||||
RegisterNUICallback("close", function(_, cb)
|
||||
close_phone()
|
||||
cb({ success = true })
|
||||
end)
|
||||
|
||||
CreateThread(function()
|
||||
TriggerEvent("chat:addSuggestion", "/" .. Config.Command, get_locale().CommandDescription)
|
||||
end)
|
||||
|
||||
AddEventHandler("onResourceStop", function(resource_name)
|
||||
if resource_name ~= GetCurrentResourceName() then
|
||||
return
|
||||
end
|
||||
|
||||
if is_open then
|
||||
SetNuiFocus(false, false)
|
||||
end
|
||||
|
||||
TriggerEvent("chat:removeSuggestion", "/" .. Config.Command)
|
||||
end)
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sky Phone</title>
|
||||
<script type="module" crossorigin src="./assets/sky-index-D5IGMOYX.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/sky-index-DMw9emdW.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user