mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +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