İçeriğe geç

Kendi uygulamanızı ekleyin

Çalışan bir örnek example/mic_phone_demo içinde. Klasörü kopyalayın, isimleri değiştirin, bir uygulamanız olsun.

my_app/
fxmanifest.lua
client.lua registers the app
server.lua anything that touches money or the database
ui/index.html your page
ui/icon.svg your icon
-- fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
client_script 'client.lua'
server_script 'server.lua'
files { -- WITHOUT THIS your page and icon are not served and the frame comes up blank
'ui/index.html',
'ui/icon.svg',
}

files bloğu, insanların unuttuğu tek şeydir. FiveM yalnızca bir kaynağın orada listelediği şeyleri sunar; dolayısıyla orada olmayan bir sayfa 404 döndürür ve uygulamanız boş bir ekranla açılır.

local function register()
exports.mic_phone:AddApp({
id = 'phonedemo',
name = 'Field Notes',
icon = ('https://cfx-nui-%s/ui/icon.svg'):format(GetCurrentResourceName()),
ui = ('%s/ui/index.html'):format(GetCurrentResourceName()),
})
end
AddEventHandler('mic_phone:ready', register) -- fires on start, and after every restart of the phone

Sözleşmenin tamamı bu ve gerçekten hepsi bu: telefonda hiçbir şey yapılandırılmaz ve hiçbir şey yeniden başlatılmaz. Kaynağınızı başlatın, uygulama Ana Ekran’da olur — default = true onu oraya koyar, default = false ise bunun yerine App Store’a koyar. Her iki durumda da karar sizindir, kendi dosyanızda.

Config.AppList.Custom, bir sunucu sahibinin sizin kararınızı geçersiz kılabilmesi için var — uygulamanıza fiyat koymak, önceden yüklü vermek, kapatmak. Uygulamanızın çalışması için asla gerekmez.

Diğer telefonların sizi yapmaya zorladığı ve sizin yapmak zorunda olmadığınız dört şey:

  • Uygulamayı eklemeden önce kaldırmanız gerekmez. Aynı kaynaktan aynı id’yi kaydetmek onu günceller. Yalnızca başka bir kaynağın, birinin zaten sahip olduğu bir id’yi sahiplenmesi reddedilir ve nedeni söylenir.
  • Telefonu beklemeniz gerekmez. mic_phone:ready, telefon uygulama alabildiğinde ve her yeniden başladığında tekrar tetiklenir; böylece uygulamanız restart mic_phone sonrasında kendiliğinden geri gelir.
  • Kaynağınız durduğunda temizlik yapmanız gerekmez. Uygulamalarınız onunla birlikte gider. Durmuş bir script, arkasında hiçbir işe yaramayan bir ikon bırakmaz.
  • Telefonu yeniden başlatmanız gerekmez. Biri telefonunu elinde tutarken kaydedilen bir uygulama, o anda orada belirir.

Bir uygulama kendisi hakkında ne söyleyebilir

Section titled “Bir uygulama kendisi hakkında ne söyleyebilir”
idharfler, rakamlar, _ ve -. Sunucu genelinde benzersiz
nameikonun üzerinde yazan
subtitle, description, features, versionApp Store sayfası
developer, categoryApp Store’da gösterilir
coloricon olmadığında ikonun arka planı
iconbir URL. https://cfx-nui-<resource>/path.png onu kendi klasörünüzden sunar
uiresource/path/index.html, uygulamanızın sayfası. Yalnızca Lua çalıştıran bir uygulama için boş bırakın. Bu sunucudaki bir kaynağın içindeki bir yol, asla harici bir URL değil — aşağıya bakın
landscapesayfa telefonun açılmış olmasını istiyor
size, imagesApp Store sayfasının gösterdiği şeyler. Resimler https:// olmalı
defaulttrue, onu App Store yerine doğrudan Ana Ekran’a koyar
onOpen, onCloseaçıldığında ve kapandığında çağrılır

Kendi fiyatınızı belirleyemezsiniz. price alanı yok ve bu bilerek: sayı istemcide yaşardı ve istemcinin beyan ettiği bir fiyatı istemci değiştirebilir — App Store bir fiyat gösterir, sunucu hiçbir şey tahsil etmezdi. Fiyatlandırma, sunucu sahibinin yazdığı ve sunucunun okuduğu Config.AppList.Custom ile yapılır. Uygulamanızın para tutmasını istiyorsanız, onlardan bir satır eklemelerini isteyin; bu tek bir satırdır.

Son sözü şehir söyler. Burada istediğiniz her şey bir istektir. Telefonun kendi config’indeki Config.AppList.Custom, kimse kaynağınızı düzenlemeden uygulamanıza fiyat koyabilir, önceden yüklü verebilir ya da tamamen kapatabilir. Bu bilerek böyle — bir sunucu sahibi bir script için ücret almak adına onu fork etmek zorunda kalmamalı.

Sayfanız bir iframe’dir. Telefonun içine uzanamaz, mesajları okuyamaz ya da kendi dikdörtgeninin dışına çizim yapamaz; telefon da onun içine uzanamaz. Her şey mesajlarla geçer.

ui, bu sunucudaki bir kaynağın içindeki bir yol olmak zorundadır. Bir frame, sayfanın Content-Security-Policy’sinin kendi origin’ine izin vermesini gerektirir ve CSP’de “herhangi bir cfx-nui- kaynağı” demenin bir yolu yoktur — joker karakter yalnızca en soldaki etiketin tamamı olarak kullanılabilir, yani https://cfx-nui-* yazılamaz ve frame’inizin yüklenebilmesi için direktifin https: değerine açılması gerekir. Bu geniştir, dolayısıyla telefon bunu diğer tarafta daraltır: origin’i kaynak yolunuzdan kendisi oluşturur ve zaten bir URL olan her şeyi reddeder. Sayfanızı başka bir yerden sunmak istiyorsanız, kendi klasörünüzden sunun.

Sayfanızda, iki satır:

parent.postMessage({ type: 'phone:ready' }, '*'); // say you have loaded
const send = m => parent.postMessage({ type: 'phone:message', message: m }, '*');

ve dinleyin:

window.addEventListener('message', e => {
if (e.data?.type === 'phone:theme') document.documentElement.dataset.theme = e.data.theme;
if (e.data?.type === 'phone:message') { /* from your Lua */ }
});

Lua’nızda:

exports.mic_phone:SendAppMessage('phonedemo', { kind = 'hello' })
AddEventHandler(GetCurrentResourceName() .. ':phone:message', function(message)
-- from your page. Routed here by the resource that owns the app, so nothing else can send these
end)

onOpen içinden istediğinizi gönderin. Henüz yüklenmemiş bir sayfaya giden mesaj onu bekler ve phone:ready dediği anda sırayla ulaşır. Diğer telefonlar bunu yapmamanız için sizi uyarır, çünkü onların mesajı frame ile yarışır; burada yarışmaz, çünkü uygulamanız açıldığında bir şey göndermek bariz olan şeydir ve bariz şeyler çalışmalıdır.

phone:theme, sayfanız merhaba dediğinde telefonun light/dark değeri ve diliyle birlikte gelir. Onu takip edin ve uygulamanız birinin üstüne yapıştırdığı bir web sitesine benzemeyi bırakır.

Sayfanız telefondan, zaten yapmayı bildiği şeyleri yapmasını isteyebilir — bir bildirim göstermek, kamerayı açmak, birinin bir fotoğraf ya da kişi seçmesini sağlamak. On beş satır bunu kurar ve sonra her soru tek bir await’tir:

let next = 1; const waiting = new Map();
const ask = (what, args) => new Promise(resolve => {
const id = next++; waiting.set(id, resolve);
parent.postMessage({ type: 'phone:ask', id, what, with: args || {} }, '*');
});
window.addEventListener('message', e => {
if (e.data?.type === 'phone:answer') { const done = waiting.get(e.data.id); if (done) { waiting.delete(e.data.id); done(e.data.value); } }
});
const me = await ask('me'); // { number, name }
const photo = await ask('photos.pick'); // { id, url, width, height } or null
const shot = await ask('camera.take'); // the same, from the real Camera
const person = await ask('contacts.pick'); // { name, number } or null
await ask('notify', { title: 'Field Notes', body: 'Something happened.' });
await ask('toast', { text: 'Saved.' });
const look = await ask('theme'); // { theme, language, tablet }
await ask('close'); // put yourself away
SoruNe döner
me{ number, name }diğer telefonların zaten gördüğü şey, kişi hakkında başka hiçbir şey yok
notifytruesizin uygulamanız altında kaydedilir; Ayarlar’da susturmak sizi de susturur
toasttrueekranın altında bir satır
photos.pickbir fotoğraf ya da nulltelefonun kitaplığını açar; null kapatıldığı anlamına gelir
camera.takebir fotoğraf ya da nullgerçek Kamera’yı açar, bekler ve sizi geri getirir
contacts.pickbir kişi ya da nullbir isim listesi açar
theme{ theme, language, tablet }phone:theme’in ilettiği aynı değerler
closetrueAna Ekran’a döner

Ve telefona bir şey geri veren kısım:

await ask('message.compose', { number: '555-0110', text: 'Your order is ready.' });
await ask('call.start', { number: '555-0110' });
await ask('share.text', { text: 'Look at this.' }); // → { to, number } or null
await ask('share.photo', { url: photo.url });
await ask('photos.save', { url: someImageUrl }); // → { id, width, height } or null
await ask('waypoint.set', { x: 219, y: -855 });
const money = await ask('money.balance'); // { balance }
SoruNe döner
message.composetrueMesajlar’ı kelimeler kutuda olarak açar. Gönder’e bir insan basar
call.starttruenumarayı çaldırır
share.text / share.photo{ to, number } ya da nullbir seçici açar, sonra seçilen kişiye mesaj oluşturur
photos.save{ id, width, height }uygulamanızın yaptığı bir resmi, bir kamera çekimi gibi Fotoğraflar’da saklar
waypoint.settrueharitada bir pin
money.balance{ balance }yalnızca okuma — para aşağıda anlatıldığı gibi sunucuda hareket eder

Kimse görmeden hiçbir şey birinin adına gönderilmez. message.compose kutuyu doldurur ve gönder butonunu bir insana bırakır; paylaşım her seferinde kimin için olduğunu sorar. Kişilerinize sessizce mesaj atabilen bir uygulama, hiç mesaj atamayan bir uygulamadan daha kötü bir şey olurdu.

Para sunucuda, kendi sunucu dosyanızdan hareket eder, çünkü bir istemcinin tartışabildiği bir fiyat fiyat değildir. Üç kapı vardır ve hangisini istediğiniz parayı kimin hareket ettirdiğine bağlıdır:

-- somebody else already took the money; this only writes the line in the Wallet
exports.mic_phone:AddTransaction(src, { title = 'Tequi-la-la', detail = 'Two drinks', amount = -40 })
-- take it, and write the line. false, 'funds' and nothing moved when they cannot pay
local ok, tx = exports.mic_phone:Charge(src, 250, { title = 'Field Notes', detail = 'Pro upgrade' })
-- give it, and write the line
exports.mic_phone:Pay(src, 1000, { title = 'Delivery', detail = 'Job finished' })
exports.mic_phone:GetBalance(src)

Üçü de ekrandaki Cüzdan’ı hemen günceller. Charge ve Pay ya tamamen başarısız olur ya da tamamen başarılı olur: reddedilen bir tahsilat bakiyeyi ve geçmişi tam olarak olduğu gibi bırakır.

İki şey bunu ayakta tutar ve listenin cömert değil de kısa olmasının sebebi budur:

Uygulamanız kimsenin vermediği veriyi asla almaz. contacts.pick rehberi döndürmez — bir liste açar, bir kişi tek bir isme dokunur ve yalnızca o isim geri gelir. Döngü içinde sormak telefonu okumaz; her cevap bir dokunuşa mal olur. Aynısı fotoğraflar için de geçerlidir.

Yalnızca ekrandaki uygulama sorabilir. Açık uygulama olmayan bir frame’den gelen soru cevaplanmaz ve hiçbir şey açmaz; böylece bir yerde çalışmaya devam eden bir sayfa, biri başka bir şey yaparken hareket edemez.

İhtiyacınız olan şey o listede değilse, telefonun reddettiğinden değil — kimsenin onu eklememiş olmasındandır. Birini eklemek, html/apps/store/app-api.js içindeki PHONE_APP_ASKS içine bir girdi eklemekten ibarettir, başka hiçbir şey değil.

exports.mic_phone:RegisteredApps() --> every app registered on this client, and by whom
exports.mic_phone:IsAppOpen('phonedemo') --> is it on screen
exports.mic_phone:OpenAppId() --> which custom app is, or nil
exports.mic_phone:OpenApp('phonedemo') --> open it, if the phone will open at all

Uygulamanız görünmüyorsa başvuracağınız RegisteredApps’tir. Sunucudaki phoneapps, config’in ne dediğini yazdırır, ki bu farklı bir listedir — uygulamanız onu kaydeden istemcide yaşar ve phoneapps içinde yalnızca birisi ona Config.AppList.Custom içinde bir girdi verdiyse görünür.

Config’de ne default = true ne de installed olan bir uygulama, biri indirene kadar App Store’da durur. Uygulamanız kayıtlıysa ve Ana Ekran’da bulamıyorsanız, genellikle sebep budur.

GördüğünüzNeredeyse her zaman
Uygulama hiçbir yerde yokÖnce RegisteredApps(). Orada yoksa AddApp reddedilmiştir — sebep sunucunun değil istemci konsolunda (F8) yazdırılır
Kayıtlı ama Ana Ekran’da değilApp Store’dadır: ya default = false yaptınız ya da sahibi Config.AppList.Custom içine How = 'store' koydu
Uygulama boş bir sayfayla açılıyorui dosyanız fxmanifest’inizin files bloğunda değil
Konsolda Refused to frameui değeriniz bir URL. Bu sunucudaki bir kaynağın içindeki bir yol olmak zorunda
Sayfa yükleniyor, mesajlar hiç gelmiyorSayfanız hiç phone:ready göndermedi. O gönderene kadar hiçbir şey teslim edilmez — tutulur, kaybolmaz; böylece onu gönderdiğinizde hepsi bir anda gelir
no answer in 12000msTelefonda olmayan bir şeyi çağırdınız. Adı yukarıdaki listeyle karşılaştırın

Sunucudaki phoneapps config’i yazdırır, uygulamanızı değil — bir kaynağın kaydettiği uygulama onu kaydeden istemcide yaşar ve orada yalnızca birisi ona bir Config.AppList.Custom girdisi verdiyse görünür. exports.mic_phone:RegisteredApps() o makinenin gerçekte sahip olduğu listedir.

Telefonun API’sinin geri kalanı — state bag’ler, aramalar, kişiler, bildirimler — Export’lar ve state bag’ler sayfasındadır.