@coventia/softphone 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{call-VKq1I8yP.d.cts → call-CaPDWVk3.d.cts} +44 -1
- package/dist/{call-VKq1I8yP.d.ts → call-CaPDWVk3.d.ts} +44 -1
- package/dist/{chunk-LMJXX4YM.js → chunk-UHNLEBBO.js} +4 -2
- package/dist/chunk-UHNLEBBO.js.map +1 -0
- package/dist/element/index.d.ts +68 -9
- package/dist/element/index.js +962 -114
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-C0BOkIcW.d.ts → engine-BgBsIcEa.d.ts} +1 -1
- package/dist/{engine-BlQBo2Ys.d.cts → engine-CU2rEo3g.d.cts} +1 -1
- package/dist/index.cjs +120 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +69 -10
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +2 -0
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.js +1 -1
- package/dist/{softphone-Cyq3hiIM.d.ts → softphone-DEt0TtQw.d.ts} +26 -10
- package/dist/{softphone-DlGV12Xp.d.cts → softphone-dZMkW8V1.d.cts} +26 -10
- package/dist/softphone.css +110 -12
- package/dist/{sources-IRKNQPWW.js → sources-M3GYMS6V.js} +54 -3
- package/dist/sources-M3GYMS6V.js.map +1 -0
- package/dist/testing/index.d.cts +2 -2
- package/dist/testing/index.d.ts +2 -2
- package/dist/ui/index.cjs +885 -28
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +25 -3
- package/dist/ui/index.d.ts +25 -3
- package/dist/ui/index.js +885 -31
- package/dist/ui/index.js.map +1 -1
- package/dist/zadarma/index.d.cts +2 -2
- package/dist/zadarma/index.d.ts +2 -2
- package/package.json +1 -1
- package/dist/chunk-LMJXX4YM.js.map +0 -1
- package/dist/sources-IRKNQPWW.js.map +0 -1
package/dist/ui/index.cjs
CHANGED
|
@@ -13128,6 +13128,14 @@ var ArrowRight = createLucideIcon("ArrowRight", [
|
|
|
13128
13128
|
["path", { d: "m12 5 7 7-7 7", key: "xquz4c" }]
|
|
13129
13129
|
]);
|
|
13130
13130
|
|
|
13131
|
+
// ../../node_modules/lucide-react/dist/esm/icons/bell-ring.js
|
|
13132
|
+
var BellRing = createLucideIcon("BellRing", [
|
|
13133
|
+
["path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9", key: "1qo2s2" }],
|
|
13134
|
+
["path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0", key: "qgo35s" }],
|
|
13135
|
+
["path", { d: "M4 2C2.8 3.7 2 5.7 2 8", key: "tap9e0" }],
|
|
13136
|
+
["path", { d: "M22 8c0-2.3-.8-4.3-2-6", key: "5bb3ad" }]
|
|
13137
|
+
]);
|
|
13138
|
+
|
|
13131
13139
|
// ../../node_modules/lucide-react/dist/esm/icons/calendar-clock.js
|
|
13132
13140
|
var CalendarClock = createLucideIcon("CalendarClock", [
|
|
13133
13141
|
["path", { d: "M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5", key: "1osxxc" }],
|
|
@@ -13242,6 +13250,13 @@ var MonitorSmartphone = createLucideIcon("MonitorSmartphone", [
|
|
|
13242
13250
|
["rect", { width: "6", height: "10", x: "16", y: "12", rx: "2", key: "1egngj" }]
|
|
13243
13251
|
]);
|
|
13244
13252
|
|
|
13253
|
+
// ../../node_modules/lucide-react/dist/esm/icons/music.js
|
|
13254
|
+
var Music = createLucideIcon("Music", [
|
|
13255
|
+
["path", { d: "M9 18V5l12-2v13", key: "1jmyc2" }],
|
|
13256
|
+
["circle", { cx: "6", cy: "18", r: "3", key: "fqmcym" }],
|
|
13257
|
+
["circle", { cx: "18", cy: "16", r: "3", key: "1hluhg" }]
|
|
13258
|
+
]);
|
|
13259
|
+
|
|
13245
13260
|
// ../../node_modules/lucide-react/dist/esm/icons/pause.js
|
|
13246
13261
|
var Pause = createLucideIcon("Pause", [
|
|
13247
13262
|
["rect", { x: "14", y: "4", width: "4", height: "16", rx: "1", key: "zuxfzm" }],
|
|
@@ -13367,6 +13382,19 @@ var Users = createLucideIcon("Users", [
|
|
|
13367
13382
|
["path", { d: "M16 3.13a4 4 0 0 1 0 7.75", key: "1da9ce" }]
|
|
13368
13383
|
]);
|
|
13369
13384
|
|
|
13385
|
+
// ../../node_modules/lucide-react/dist/esm/icons/volume-2.js
|
|
13386
|
+
var Volume2 = createLucideIcon("Volume2", [
|
|
13387
|
+
[
|
|
13388
|
+
"path",
|
|
13389
|
+
{
|
|
13390
|
+
d: "M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z",
|
|
13391
|
+
key: "uqj9uw"
|
|
13392
|
+
}
|
|
13393
|
+
],
|
|
13394
|
+
["path", { d: "M16 9a5 5 0 0 1 0 6", key: "1q6k2b" }],
|
|
13395
|
+
["path", { d: "M19.364 18.364a9 9 0 0 0 0-12.728", key: "ijwkga" }]
|
|
13396
|
+
]);
|
|
13397
|
+
|
|
13370
13398
|
// ../../node_modules/lucide-react/dist/esm/icons/x.js
|
|
13371
13399
|
var X = createLucideIcon("X", [
|
|
13372
13400
|
["path", { d: "M18 6 6 18", key: "1bl5f8" }],
|
|
@@ -13493,7 +13521,9 @@ function useDevices() {
|
|
|
13493
13521
|
outputs: audio.outputs,
|
|
13494
13522
|
currentInputId: audio.currentInputId,
|
|
13495
13523
|
currentOutputId: audio.currentOutputId,
|
|
13524
|
+
currentRingtoneOutputId: audio.currentRingtoneOutputId,
|
|
13496
13525
|
canSelectOutput: audio.canSelectOutput,
|
|
13526
|
+
canSelectRingtoneOutput: audio.canSelectRingtoneOutput,
|
|
13497
13527
|
setInput: (deviceId) => audio.setInput(deviceId),
|
|
13498
13528
|
setOutput: (deviceId) => audio.setOutput(deviceId),
|
|
13499
13529
|
setRingtoneOutput: (deviceId) => audio.setRingtoneOutput(deviceId),
|
|
@@ -13738,6 +13768,59 @@ var KEYPAD_KEYS = [
|
|
|
13738
13768
|
["#", ""]
|
|
13739
13769
|
];
|
|
13740
13770
|
|
|
13771
|
+
// src/ui/audio/classic-ring.ts
|
|
13772
|
+
var C6 = 1046.5;
|
|
13773
|
+
var E6 = 1318.51;
|
|
13774
|
+
var HARMONICITY = 4;
|
|
13775
|
+
var INDEX = 1.8;
|
|
13776
|
+
var CLASSIC_CYCLE_SECONDS = 3;
|
|
13777
|
+
var CLASSIC_GAIN = 0.3841;
|
|
13778
|
+
function classicRingGraph(ctx, dest) {
|
|
13779
|
+
const output = ctx.createGain();
|
|
13780
|
+
output.gain.value = CLASSIC_GAIN;
|
|
13781
|
+
const chain = ["highpass", "highpass", "lowpass", "lowpass"].map((type) => {
|
|
13782
|
+
const f = ctx.createBiquadFilter();
|
|
13783
|
+
f.type = type;
|
|
13784
|
+
f.frequency.value = type === "highpass" ? 250 : 7500;
|
|
13785
|
+
f.Q.value = 0.707;
|
|
13786
|
+
return f;
|
|
13787
|
+
});
|
|
13788
|
+
for (let i = 0; i < chain.length - 1; i++) chain[i].connect(chain[i + 1]);
|
|
13789
|
+
chain[chain.length - 1].connect(output);
|
|
13790
|
+
output.connect(dest);
|
|
13791
|
+
return { input: chain[0], output };
|
|
13792
|
+
}
|
|
13793
|
+
function strike(ctx, input, t, freq, vel, track) {
|
|
13794
|
+
const carrier = ctx.createOscillator();
|
|
13795
|
+
carrier.frequency.value = freq;
|
|
13796
|
+
const modulator = ctx.createOscillator();
|
|
13797
|
+
modulator.frequency.value = freq * HARMONICITY;
|
|
13798
|
+
const depth = ctx.createGain();
|
|
13799
|
+
depth.gain.setValueAtTime(0, t);
|
|
13800
|
+
depth.gain.linearRampToValueAtTime(INDEX * freq, t + 4e-3);
|
|
13801
|
+
depth.gain.setTargetAtTime(0, t + 4e-3, 0.02);
|
|
13802
|
+
modulator.connect(depth);
|
|
13803
|
+
depth.connect(carrier.frequency);
|
|
13804
|
+
const amp = ctx.createGain();
|
|
13805
|
+
amp.gain.setValueAtTime(0, t);
|
|
13806
|
+
amp.gain.linearRampToValueAtTime(vel, t + 5e-3);
|
|
13807
|
+
amp.gain.setTargetAtTime(0, t + 5e-3, 0.15);
|
|
13808
|
+
amp.gain.setTargetAtTime(0, t + 0.045, 0.08);
|
|
13809
|
+
carrier.connect(amp);
|
|
13810
|
+
amp.connect(input);
|
|
13811
|
+
for (const o of [carrier, modulator]) {
|
|
13812
|
+
o.start(t);
|
|
13813
|
+
o.stop(t + 0.7);
|
|
13814
|
+
track(o);
|
|
13815
|
+
}
|
|
13816
|
+
}
|
|
13817
|
+
function scheduleClassicCycle(ctx, input, t0, track) {
|
|
13818
|
+
for (const ring of [0, 0.6]) {
|
|
13819
|
+
for (let i = 0; i < 8; i++) strike(ctx, input, t0 + ring + i * 0.05, i % 2 ? E6 : C6, 0.55 + 0.1 * (i % 2), track);
|
|
13820
|
+
}
|
|
13821
|
+
return CLASSIC_CYCLE_SECONDS;
|
|
13822
|
+
}
|
|
13823
|
+
|
|
13741
13824
|
// src/ui/audio/audio.ts
|
|
13742
13825
|
var TONE_HZ = 425;
|
|
13743
13826
|
function createRemoteAudio() {
|
|
@@ -13780,30 +13863,159 @@ function primeAudio() {
|
|
|
13780
13863
|
if (ctx && ctx.state === "suspended") void ctx.resume?.().catch(() => {
|
|
13781
13864
|
});
|
|
13782
13865
|
}
|
|
13783
|
-
function playRingtone() {
|
|
13866
|
+
function playRingtone(options = {}) {
|
|
13784
13867
|
const ctx = sharedContext();
|
|
13785
13868
|
if (!ctx) return { stop() {
|
|
13786
13869
|
} };
|
|
13870
|
+
const bus = ctx.createGain();
|
|
13871
|
+
const route = routeToDevice(ctx, bus, options.sinkId ?? null);
|
|
13872
|
+
const voice = options.buffer ? fileVoice(ctx, bus, options.buffer, options.loop ?? null) : synthesisedVoice(ctx, bus);
|
|
13873
|
+
return {
|
|
13874
|
+
stop() {
|
|
13875
|
+
voice.stop();
|
|
13876
|
+
route.stop();
|
|
13877
|
+
}
|
|
13878
|
+
};
|
|
13879
|
+
}
|
|
13880
|
+
function fileVoice(ctx, bus, buffer, loop) {
|
|
13881
|
+
const FILE_LEVEL = 1;
|
|
13882
|
+
const RAMP = 0.02;
|
|
13883
|
+
const LEAD = 0.08;
|
|
13787
13884
|
const gain = ctx.createGain();
|
|
13788
13885
|
gain.gain.value = 0;
|
|
13789
|
-
gain.connect(
|
|
13790
|
-
const
|
|
13791
|
-
|
|
13792
|
-
|
|
13793
|
-
|
|
13794
|
-
|
|
13795
|
-
|
|
13796
|
-
|
|
13797
|
-
|
|
13886
|
+
gain.connect(bus);
|
|
13887
|
+
const src = ctx.createBufferSource();
|
|
13888
|
+
src.buffer = buffer;
|
|
13889
|
+
src.loop = true;
|
|
13890
|
+
const points = loop && loop.start >= 0 && loop.end > loop.start && loop.end <= buffer.duration + 1e-3 ? loop : null;
|
|
13891
|
+
if (points) {
|
|
13892
|
+
src.loopStart = points.start;
|
|
13893
|
+
src.loopEnd = points.end;
|
|
13894
|
+
}
|
|
13895
|
+
src.connect(gain);
|
|
13896
|
+
let stopped = false;
|
|
13897
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
13898
|
+
}).finally(() => {
|
|
13899
|
+
if (stopped) return;
|
|
13900
|
+
const at = ctx.currentTime + LEAD;
|
|
13798
13901
|
gain.gain.setValueAtTime(0, at);
|
|
13799
|
-
gain.gain.linearRampToValueAtTime(
|
|
13800
|
-
|
|
13801
|
-
|
|
13902
|
+
gain.gain.linearRampToValueAtTime(FILE_LEVEL, at + RAMP);
|
|
13903
|
+
src.start(at, points?.start ?? 0);
|
|
13904
|
+
});
|
|
13905
|
+
return {
|
|
13906
|
+
stop() {
|
|
13907
|
+
stopped = true;
|
|
13908
|
+
try {
|
|
13909
|
+
src.stop();
|
|
13910
|
+
} catch {
|
|
13911
|
+
}
|
|
13912
|
+
try {
|
|
13913
|
+
gain.disconnect();
|
|
13914
|
+
} catch {
|
|
13915
|
+
}
|
|
13916
|
+
}
|
|
13917
|
+
};
|
|
13918
|
+
}
|
|
13919
|
+
function routeToDevice(ctx, bus, sinkId) {
|
|
13920
|
+
const toDefault = () => {
|
|
13921
|
+
bus.connect(ctx.destination);
|
|
13802
13922
|
};
|
|
13803
|
-
const
|
|
13804
|
-
|
|
13805
|
-
|
|
13806
|
-
|
|
13923
|
+
const el = sinkId && typeof document !== "undefined" ? document.createElement("audio") : null;
|
|
13924
|
+
const dest = el && typeof ctx.createMediaStreamDestination === "function" ? ctx.createMediaStreamDestination() : null;
|
|
13925
|
+
const canRoute = el && dest && typeof el.setSinkId === "function";
|
|
13926
|
+
if (!canRoute || !sinkId) {
|
|
13927
|
+
toDefault();
|
|
13928
|
+
return { stop: () => safeDisconnect(bus) };
|
|
13929
|
+
}
|
|
13930
|
+
let stopped = false;
|
|
13931
|
+
el.style.display = "none";
|
|
13932
|
+
el.srcObject = dest.stream;
|
|
13933
|
+
document.body.appendChild(el);
|
|
13934
|
+
bus.connect(dest);
|
|
13935
|
+
void el.setSinkId(sinkId).then(() => el.play()).catch((error) => {
|
|
13936
|
+
if (stopped) return;
|
|
13937
|
+
console.warn("[softphone] sound could not be sent to the chosen device; using the default output", error);
|
|
13938
|
+
safeDisconnect(bus);
|
|
13939
|
+
teardown(el);
|
|
13940
|
+
toDefault();
|
|
13941
|
+
});
|
|
13942
|
+
return {
|
|
13943
|
+
stop() {
|
|
13944
|
+
stopped = true;
|
|
13945
|
+
safeDisconnect(bus);
|
|
13946
|
+
teardown(el);
|
|
13947
|
+
}
|
|
13948
|
+
};
|
|
13949
|
+
}
|
|
13950
|
+
function safeDisconnect(node) {
|
|
13951
|
+
try {
|
|
13952
|
+
node.disconnect();
|
|
13953
|
+
} catch {
|
|
13954
|
+
}
|
|
13955
|
+
}
|
|
13956
|
+
function teardown(el) {
|
|
13957
|
+
el.pause();
|
|
13958
|
+
el.srcObject = null;
|
|
13959
|
+
el.remove();
|
|
13960
|
+
}
|
|
13961
|
+
var TEST_TONE_MS = 900;
|
|
13962
|
+
function playTestTone(sinkId) {
|
|
13963
|
+
const ctx = sharedContext();
|
|
13964
|
+
if (!ctx) return { stop() {
|
|
13965
|
+
} };
|
|
13966
|
+
const bus = ctx.createGain();
|
|
13967
|
+
const route = routeToDevice(ctx, bus, sinkId);
|
|
13968
|
+
const LEVEL = 0.5;
|
|
13969
|
+
const NOTES = [
|
|
13970
|
+
{ hz: 660, at: 0.05 },
|
|
13971
|
+
{ hz: 880, at: 0.4 }
|
|
13972
|
+
];
|
|
13973
|
+
const oscs = [];
|
|
13974
|
+
let stopped = false;
|
|
13975
|
+
let timer = null;
|
|
13976
|
+
const stop = () => {
|
|
13977
|
+
if (stopped) return;
|
|
13978
|
+
stopped = true;
|
|
13979
|
+
if (timer) clearTimeout(timer);
|
|
13980
|
+
for (const o of oscs) {
|
|
13981
|
+
try {
|
|
13982
|
+
o.stop();
|
|
13983
|
+
} catch {
|
|
13984
|
+
}
|
|
13985
|
+
}
|
|
13986
|
+
route.stop();
|
|
13987
|
+
};
|
|
13988
|
+
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
13989
|
+
}).finally(() => {
|
|
13990
|
+
if (stopped) return;
|
|
13991
|
+
const t0 = ctx.currentTime;
|
|
13992
|
+
for (const { hz, at } of NOTES) {
|
|
13993
|
+
const gain = ctx.createGain();
|
|
13994
|
+
gain.gain.value = 0;
|
|
13995
|
+
gain.connect(bus);
|
|
13996
|
+
const osc = ctx.createOscillator();
|
|
13997
|
+
osc.type = "sine";
|
|
13998
|
+
osc.frequency.value = hz;
|
|
13999
|
+
osc.connect(gain);
|
|
14000
|
+
const start = t0 + at;
|
|
14001
|
+
gain.gain.setValueAtTime(0, start);
|
|
14002
|
+
gain.gain.linearRampToValueAtTime(LEVEL, start + 0.015);
|
|
14003
|
+
gain.gain.setValueAtTime(LEVEL, start + 0.25);
|
|
14004
|
+
gain.gain.linearRampToValueAtTime(0, start + 0.32);
|
|
14005
|
+
osc.start(start);
|
|
14006
|
+
osc.stop(start + 0.34);
|
|
14007
|
+
oscs.push(osc);
|
|
14008
|
+
}
|
|
14009
|
+
timer = setTimeout(stop, TEST_TONE_MS);
|
|
14010
|
+
});
|
|
14011
|
+
return { stop };
|
|
14012
|
+
}
|
|
14013
|
+
function synthesisedVoice(ctx, bus) {
|
|
14014
|
+
const { input, output } = classicRingGraph(ctx, bus);
|
|
14015
|
+
const scheduled = /* @__PURE__ */ new Set();
|
|
14016
|
+
const track = (o) => {
|
|
14017
|
+
scheduled.add(o);
|
|
14018
|
+
o.onended = () => scheduled.delete(o);
|
|
13807
14019
|
};
|
|
13808
14020
|
const LOOKAHEAD = 0.5;
|
|
13809
14021
|
const TICK_MS = 150;
|
|
@@ -13812,7 +14024,7 @@ function playRingtone() {
|
|
|
13812
14024
|
let timer = null;
|
|
13813
14025
|
let nextAt = 0;
|
|
13814
14026
|
const pump = () => {
|
|
13815
|
-
while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt +=
|
|
14027
|
+
while (nextAt < ctx.currentTime + LOOKAHEAD) nextAt += scheduleClassicCycle(ctx, input, nextAt, track);
|
|
13816
14028
|
};
|
|
13817
14029
|
void Promise.resolve(ctx.resume?.()).catch(() => {
|
|
13818
14030
|
}).finally(() => {
|
|
@@ -13825,12 +14037,15 @@ function playRingtone() {
|
|
|
13825
14037
|
stop() {
|
|
13826
14038
|
stopped = true;
|
|
13827
14039
|
if (timer) clearInterval(timer);
|
|
13828
|
-
|
|
13829
|
-
|
|
13830
|
-
|
|
14040
|
+
for (const o of scheduled) {
|
|
14041
|
+
try {
|
|
14042
|
+
o.stop();
|
|
14043
|
+
} catch {
|
|
14044
|
+
}
|
|
13831
14045
|
}
|
|
14046
|
+
scheduled.clear();
|
|
13832
14047
|
try {
|
|
13833
|
-
|
|
14048
|
+
output.disconnect();
|
|
13834
14049
|
} catch {
|
|
13835
14050
|
}
|
|
13836
14051
|
}
|
|
@@ -15335,6 +15550,110 @@ function HistoryTab({ source }) {
|
|
|
15335
15550
|
selected ? /* @__PURE__ */ jsxRuntime.jsx(HistoryDetail, { entry: selected, onBack: () => setSelected(null) }) : null
|
|
15336
15551
|
] });
|
|
15337
15552
|
}
|
|
15553
|
+
|
|
15554
|
+
// src/ui/audio/ringtone-store.ts
|
|
15555
|
+
var stores = /* @__PURE__ */ new WeakMap();
|
|
15556
|
+
function ringtoneStore(softphone) {
|
|
15557
|
+
const source = softphone.sources?.ringtones;
|
|
15558
|
+
if (!source) return null;
|
|
15559
|
+
let store = stores.get(softphone);
|
|
15560
|
+
if (!store) {
|
|
15561
|
+
store = createRingtoneStore(source);
|
|
15562
|
+
stores.set(softphone, store);
|
|
15563
|
+
}
|
|
15564
|
+
return store;
|
|
15565
|
+
}
|
|
15566
|
+
function createRingtoneStore(source, decodeImpl = decodeRingtone) {
|
|
15567
|
+
let settings = null;
|
|
15568
|
+
let audioWanted = false;
|
|
15569
|
+
let loaded = null;
|
|
15570
|
+
let loading2 = null;
|
|
15571
|
+
let inFlight = null;
|
|
15572
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
15573
|
+
const notify = () => {
|
|
15574
|
+
for (const l of listeners) l();
|
|
15575
|
+
};
|
|
15576
|
+
const effectiveUrl = () => {
|
|
15577
|
+
if (!settings) return null;
|
|
15578
|
+
return settings.catalogue.find((r) => r.id === settings.effective)?.url ?? null;
|
|
15579
|
+
};
|
|
15580
|
+
const loadEffective = async () => {
|
|
15581
|
+
const url = effectiveUrl();
|
|
15582
|
+
if (!audioWanted || !url || loaded?.url === url || loading2 === url) return;
|
|
15583
|
+
loading2 = url;
|
|
15584
|
+
try {
|
|
15585
|
+
const buffer = await decodeImpl(url);
|
|
15586
|
+
if (audioWanted && effectiveUrl() === url) {
|
|
15587
|
+
loaded = { url, buffer };
|
|
15588
|
+
notify();
|
|
15589
|
+
}
|
|
15590
|
+
} catch (error) {
|
|
15591
|
+
console.warn(`[softphone] ringtone could not be loaded, using the built-in tone: ${url}`, error);
|
|
15592
|
+
} finally {
|
|
15593
|
+
if (loading2 === url) loading2 = null;
|
|
15594
|
+
}
|
|
15595
|
+
};
|
|
15596
|
+
const apply = (next) => {
|
|
15597
|
+
settings = next;
|
|
15598
|
+
if (loaded && loaded.url !== effectiveUrl()) loaded = null;
|
|
15599
|
+
notify();
|
|
15600
|
+
};
|
|
15601
|
+
return {
|
|
15602
|
+
getSettings: () => settings,
|
|
15603
|
+
readyBuffer: () => loaded && loaded.url === effectiveUrl() ? loaded.buffer : null,
|
|
15604
|
+
readyLoop: () => {
|
|
15605
|
+
if (!loaded || loaded.url !== effectiveUrl() || !settings) return null;
|
|
15606
|
+
return settings.catalogue.find((r) => r.id === settings.effective)?.loop ?? null;
|
|
15607
|
+
},
|
|
15608
|
+
refresh() {
|
|
15609
|
+
inFlight ??= (async () => {
|
|
15610
|
+
if (!settings && source.cached) {
|
|
15611
|
+
const early = await source.cached().catch(() => null);
|
|
15612
|
+
if (early && !settings) {
|
|
15613
|
+
apply(early);
|
|
15614
|
+
void loadEffective();
|
|
15615
|
+
}
|
|
15616
|
+
}
|
|
15617
|
+
try {
|
|
15618
|
+
apply(await source.get());
|
|
15619
|
+
} catch (error) {
|
|
15620
|
+
console.warn("[softphone] ringtone settings could not be loaded; keeping the previous ones", error);
|
|
15621
|
+
} finally {
|
|
15622
|
+
inFlight = null;
|
|
15623
|
+
}
|
|
15624
|
+
await loadEffective();
|
|
15625
|
+
})();
|
|
15626
|
+
return inFlight;
|
|
15627
|
+
},
|
|
15628
|
+
async setMine(id) {
|
|
15629
|
+
apply(await source.setMine(id));
|
|
15630
|
+
void loadEffective();
|
|
15631
|
+
},
|
|
15632
|
+
wantAudio(on) {
|
|
15633
|
+
if (audioWanted === on) return;
|
|
15634
|
+
audioWanted = on;
|
|
15635
|
+
if (!on) {
|
|
15636
|
+
loaded = null;
|
|
15637
|
+
notify();
|
|
15638
|
+
} else {
|
|
15639
|
+
void loadEffective();
|
|
15640
|
+
}
|
|
15641
|
+
},
|
|
15642
|
+
decode: (url) => decodeImpl(url),
|
|
15643
|
+
subscribe(listener) {
|
|
15644
|
+
listeners.add(listener);
|
|
15645
|
+
return () => listeners.delete(listener);
|
|
15646
|
+
}
|
|
15647
|
+
};
|
|
15648
|
+
}
|
|
15649
|
+
async function decodeRingtone(url) {
|
|
15650
|
+
const Offline = typeof OfflineAudioContext !== "undefined" ? OfflineAudioContext : globalThis.webkitOfflineAudioContext;
|
|
15651
|
+
if (!Offline) throw new Error("this browser cannot decode audio files");
|
|
15652
|
+
const res = await fetch(url, { credentials: "omit" });
|
|
15653
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
15654
|
+
const bytes = await res.arrayBuffer();
|
|
15655
|
+
return new Offline(1, 1, 48e3).decodeAudioData(bytes);
|
|
15656
|
+
}
|
|
15338
15657
|
function SettingsSection({
|
|
15339
15658
|
title,
|
|
15340
15659
|
children
|
|
@@ -15344,11 +15663,527 @@ function SettingsSection({
|
|
|
15344
15663
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-settings-section-body", children })
|
|
15345
15664
|
] });
|
|
15346
15665
|
}
|
|
15666
|
+
function SettingsNavRow({
|
|
15667
|
+
icon: Icon3,
|
|
15668
|
+
title,
|
|
15669
|
+
value,
|
|
15670
|
+
onOpen,
|
|
15671
|
+
rowRef
|
|
15672
|
+
}) {
|
|
15673
|
+
const inner = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
15674
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-settings-row-icon", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx(Icon3, { size: 16 }) }),
|
|
15675
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-settings-nav-text", children: [
|
|
15676
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-settings-nav-title", children: title }),
|
|
15677
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-settings-nav-value", children: value })
|
|
15678
|
+
] }),
|
|
15679
|
+
onOpen ? /* @__PURE__ */ jsxRuntime.jsx(ChevronRight, { size: 16, className: "cv-settings-nav-chevron", "aria-hidden": true }) : null
|
|
15680
|
+
] });
|
|
15681
|
+
return onOpen ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "cv-settings-nav", onClick: onOpen, ref: rowRef, children: inner }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-settings-nav cv-settings-nav--static", children: inner });
|
|
15682
|
+
}
|
|
15683
|
+
function ChoiceList({
|
|
15684
|
+
label,
|
|
15685
|
+
choices,
|
|
15686
|
+
value,
|
|
15687
|
+
onSelect,
|
|
15688
|
+
onReselect
|
|
15689
|
+
}) {
|
|
15690
|
+
const name = React.useId();
|
|
15691
|
+
const pointer = React.useRef(false);
|
|
15692
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-choices", role: "radiogroup", "aria-label": label, children: choices.map((c) => {
|
|
15693
|
+
const checked = c.value === value;
|
|
15694
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
15695
|
+
"label",
|
|
15696
|
+
{
|
|
15697
|
+
className: "cv-choice",
|
|
15698
|
+
"data-checked": checked || void 0,
|
|
15699
|
+
onPointerDown: () => {
|
|
15700
|
+
pointer.current = true;
|
|
15701
|
+
},
|
|
15702
|
+
children: [
|
|
15703
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
15704
|
+
"input",
|
|
15705
|
+
{
|
|
15706
|
+
type: "radio",
|
|
15707
|
+
className: "cv-choice-input",
|
|
15708
|
+
name,
|
|
15709
|
+
value: c.value,
|
|
15710
|
+
checked,
|
|
15711
|
+
onChange: () => {
|
|
15712
|
+
const via = pointer.current ? "pointer" : "keyboard";
|
|
15713
|
+
pointer.current = false;
|
|
15714
|
+
onSelect(c.value, via);
|
|
15715
|
+
},
|
|
15716
|
+
onClick: () => {
|
|
15717
|
+
if (c.value === value) {
|
|
15718
|
+
pointer.current = false;
|
|
15719
|
+
onReselect?.(c.value);
|
|
15720
|
+
}
|
|
15721
|
+
}
|
|
15722
|
+
}
|
|
15723
|
+
),
|
|
15724
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-choice-text", children: [
|
|
15725
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-label", children: c.label }),
|
|
15726
|
+
c.sub ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-choice-sub", children: c.sub }) : null
|
|
15727
|
+
] }),
|
|
15728
|
+
c.trailing,
|
|
15729
|
+
/* @__PURE__ */ jsxRuntime.jsx(Check, { size: 16, className: "cv-choice-check", "aria-hidden": true })
|
|
15730
|
+
]
|
|
15731
|
+
},
|
|
15732
|
+
c.value
|
|
15733
|
+
);
|
|
15734
|
+
}) });
|
|
15735
|
+
}
|
|
15736
|
+
function SettingsPage({
|
|
15737
|
+
title,
|
|
15738
|
+
intro,
|
|
15739
|
+
onBack,
|
|
15740
|
+
children
|
|
15741
|
+
}) {
|
|
15742
|
+
const titleId = React.useId();
|
|
15743
|
+
const heading = React.useRef(null);
|
|
15744
|
+
React.useEffect(() => heading.current?.focus(), []);
|
|
15745
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
15746
|
+
"div",
|
|
15747
|
+
{
|
|
15748
|
+
className: "cv-detail cv-settings-page",
|
|
15749
|
+
role: "region",
|
|
15750
|
+
"aria-labelledby": titleId,
|
|
15751
|
+
onKeyDown: (e) => {
|
|
15752
|
+
if (e.key === "Escape") {
|
|
15753
|
+
e.stopPropagation();
|
|
15754
|
+
onBack();
|
|
15755
|
+
}
|
|
15756
|
+
},
|
|
15757
|
+
children: [
|
|
15758
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-detail-head", children: /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-detail-back", onClick: onBack, children: [
|
|
15759
|
+
/* @__PURE__ */ jsxRuntime.jsx(ArrowLeft, { size: 16, "aria-hidden": true }),
|
|
15760
|
+
"Ajustes"
|
|
15761
|
+
] }) }),
|
|
15762
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-settings-page-body", children: [
|
|
15763
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "cv-settings-page-title", id: titleId, tabIndex: -1, ref: heading, children: title }),
|
|
15764
|
+
intro ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-settings-page-intro", children: intro }) : null,
|
|
15765
|
+
children
|
|
15766
|
+
] })
|
|
15767
|
+
]
|
|
15768
|
+
}
|
|
15769
|
+
);
|
|
15770
|
+
}
|
|
15771
|
+
var SYSTEM_DEFAULT = "";
|
|
15772
|
+
var PAGES = {
|
|
15773
|
+
input: {
|
|
15774
|
+
title: "Micr\xF3fono",
|
|
15775
|
+
intro: "El micr\xF3fono que usa el tel\xE9fono en las llamadas.",
|
|
15776
|
+
empty: "No se encontr\xF3 ning\xFAn micr\xF3fono."
|
|
15777
|
+
},
|
|
15778
|
+
output: {
|
|
15779
|
+
title: "Altavoces",
|
|
15780
|
+
intro: "D\xF3nde oyes a la otra persona durante una llamada.",
|
|
15781
|
+
empty: "No se encontr\xF3 ning\xFAn altavoz."
|
|
15782
|
+
},
|
|
15783
|
+
ringtone: {
|
|
15784
|
+
title: "Salida del tono",
|
|
15785
|
+
intro: "D\xF3nde suena el tono cuando entra una llamada. La llamada se oye por \xABAltavoces\xBB.",
|
|
15786
|
+
empty: "No se encontr\xF3 ning\xFAn altavoz."
|
|
15787
|
+
}
|
|
15788
|
+
};
|
|
15789
|
+
function deviceSummary(kind, list, id) {
|
|
15790
|
+
const i = id ? list.findIndex((d) => d.deviceId === id) : -1;
|
|
15791
|
+
return i >= 0 ? deviceLabel(list[i], i, kind) : "Predeterminado del sistema";
|
|
15792
|
+
}
|
|
15793
|
+
function DevicePage({ kind, onBack }) {
|
|
15794
|
+
const devices = useDevices();
|
|
15795
|
+
const page = PAGES[kind];
|
|
15796
|
+
const list = kind === "input" ? devices.inputs : devices.outputs;
|
|
15797
|
+
const current = kind === "input" ? devices.currentInputId : kind === "output" ? devices.currentOutputId : devices.currentRingtoneOutputId;
|
|
15798
|
+
const apply = (id) => kind === "input" ? devices.setInput(id) : kind === "output" ? devices.setOutput(id) : devices.setRingtoneOutput(id);
|
|
15799
|
+
const choices = [
|
|
15800
|
+
{ value: SYSTEM_DEFAULT, label: "Predeterminado del sistema" },
|
|
15801
|
+
...list.map((d, i) => ({ value: d.deviceId, label: deviceLabel(d, i, kind) }))
|
|
15802
|
+
];
|
|
15803
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(SettingsPage, { title: page.title, intro: page.intro, onBack, children: [
|
|
15804
|
+
list.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
15805
|
+
ChoiceList,
|
|
15806
|
+
{
|
|
15807
|
+
label: page.title,
|
|
15808
|
+
choices,
|
|
15809
|
+
value: current ?? SYSTEM_DEFAULT,
|
|
15810
|
+
onSelect: (v) => void apply(v === SYSTEM_DEFAULT ? null : v)
|
|
15811
|
+
}
|
|
15812
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-settings-page-empty", children: page.empty }),
|
|
15813
|
+
kind === "output" ? /* @__PURE__ */ jsxRuntime.jsx(SpeakerTest, { sinkId: devices.currentOutputId }) : null
|
|
15814
|
+
] });
|
|
15815
|
+
}
|
|
15816
|
+
function SpeakerTest({ sinkId }) {
|
|
15817
|
+
const [testing, setTesting] = React.useState(false);
|
|
15818
|
+
const tone = React.useRef(null);
|
|
15819
|
+
const timer = React.useRef(null);
|
|
15820
|
+
React.useEffect(
|
|
15821
|
+
() => () => {
|
|
15822
|
+
tone.current?.stop();
|
|
15823
|
+
if (timer.current) clearTimeout(timer.current);
|
|
15824
|
+
},
|
|
15825
|
+
[]
|
|
15826
|
+
);
|
|
15827
|
+
const test = () => {
|
|
15828
|
+
primeAudio();
|
|
15829
|
+
tone.current?.stop();
|
|
15830
|
+
tone.current = playTestTone(sinkId);
|
|
15831
|
+
setTesting(true);
|
|
15832
|
+
if (timer.current) clearTimeout(timer.current);
|
|
15833
|
+
timer.current = setTimeout(() => setTesting(false), TEST_TONE_MS);
|
|
15834
|
+
};
|
|
15835
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "cv-btn cv-settings-test", onClick: test, disabled: testing, children: [
|
|
15836
|
+
/* @__PURE__ */ jsxRuntime.jsx(Volume2, { size: 16, "aria-hidden": true }),
|
|
15837
|
+
testing ? "Sonando\u2026" : "Probar altavoz"
|
|
15838
|
+
] });
|
|
15839
|
+
}
|
|
15840
|
+
|
|
15841
|
+
// src/ui/audio/preview.ts
|
|
15842
|
+
var PREVIEW_MAX_MS = 1e4;
|
|
15843
|
+
var BUILT_IN_PREVIEW_MS = 6e3;
|
|
15844
|
+
function previewDurationMs(buffer, loop) {
|
|
15845
|
+
if (!buffer) return BUILT_IN_PREVIEW_MS;
|
|
15846
|
+
const seconds = loop ? loop.end - loop.start : buffer.duration;
|
|
15847
|
+
return Math.min(seconds * 1e3, PREVIEW_MAX_MS);
|
|
15848
|
+
}
|
|
15849
|
+
function previewRingtone(ringtone, options = {}) {
|
|
15850
|
+
primeAudio();
|
|
15851
|
+
let tone = null;
|
|
15852
|
+
let timer = null;
|
|
15853
|
+
let over = false;
|
|
15854
|
+
let settle;
|
|
15855
|
+
let fail;
|
|
15856
|
+
const done = new Promise((resolve, reject) => {
|
|
15857
|
+
settle = resolve;
|
|
15858
|
+
fail = reject;
|
|
15859
|
+
});
|
|
15860
|
+
done.catch(() => {
|
|
15861
|
+
});
|
|
15862
|
+
const stop = () => {
|
|
15863
|
+
if (over) return;
|
|
15864
|
+
over = true;
|
|
15865
|
+
if (timer) clearTimeout(timer);
|
|
15866
|
+
tone?.stop();
|
|
15867
|
+
settle();
|
|
15868
|
+
};
|
|
15869
|
+
const play = (buffer) => {
|
|
15870
|
+
if (over) return;
|
|
15871
|
+
const loop = buffer ? ringtone.loop ?? null : null;
|
|
15872
|
+
tone = playRingtone({ buffer, loop, sinkId: options.sinkId ?? null });
|
|
15873
|
+
timer = setTimeout(stop, previewDurationMs(buffer, loop));
|
|
15874
|
+
};
|
|
15875
|
+
if (!ringtone.url) {
|
|
15876
|
+
play(null);
|
|
15877
|
+
} else {
|
|
15878
|
+
decodeRingtone(ringtone.url).then(play, (error) => {
|
|
15879
|
+
if (over) return;
|
|
15880
|
+
over = true;
|
|
15881
|
+
fail(error);
|
|
15882
|
+
});
|
|
15883
|
+
}
|
|
15884
|
+
return { stop, done };
|
|
15885
|
+
}
|
|
15886
|
+
|
|
15887
|
+
// src/ui/views/settings/ringtone-state.ts
|
|
15888
|
+
var noSubscribe = () => () => {
|
|
15889
|
+
};
|
|
15890
|
+
var noSettings = () => null;
|
|
15891
|
+
function useRingtoneSettings(store) {
|
|
15892
|
+
const settings = React.useSyncExternalStore(store?.subscribe ?? noSubscribe, store?.getSettings ?? noSettings);
|
|
15893
|
+
const [failed, setFailed] = React.useState(false);
|
|
15894
|
+
const load = React.useCallback(() => {
|
|
15895
|
+
if (!store) return () => {
|
|
15896
|
+
};
|
|
15897
|
+
let live = true;
|
|
15898
|
+
setFailed(false);
|
|
15899
|
+
void store.refresh().then(() => {
|
|
15900
|
+
if (live) setFailed(store.getSettings() === null);
|
|
15901
|
+
});
|
|
15902
|
+
return () => {
|
|
15903
|
+
live = false;
|
|
15904
|
+
};
|
|
15905
|
+
}, [store]);
|
|
15906
|
+
React.useEffect(load, [load]);
|
|
15907
|
+
return { settings, failed, retry: load };
|
|
15908
|
+
}
|
|
15909
|
+
function ringtoneSummary(settings, failed) {
|
|
15910
|
+
if (!settings) return failed ? "No disponible" : "Cargando\u2026";
|
|
15911
|
+
const byId = (id) => id ? settings.catalogue.find((r) => r.id === id) : void 0;
|
|
15912
|
+
const mine = byId(settings.mine);
|
|
15913
|
+
if (mine) return mine.name;
|
|
15914
|
+
const org = byId(settings.orgDefault);
|
|
15915
|
+
if (org) return `${org.name} \xB7 organizaci\xF3n`;
|
|
15916
|
+
return settings.catalogue.find((r) => r.url === null)?.name ?? "Predeterminado";
|
|
15917
|
+
}
|
|
15918
|
+
function useRingtonePreview(store, sinkId) {
|
|
15919
|
+
const [playing, setPlaying] = React.useState(null);
|
|
15920
|
+
const [error, setError] = React.useState(null);
|
|
15921
|
+
const current = React.useRef(null);
|
|
15922
|
+
const generation = React.useRef(0);
|
|
15923
|
+
const stop = React.useCallback(() => {
|
|
15924
|
+
generation.current += 1;
|
|
15925
|
+
const c = current.current;
|
|
15926
|
+
current.current = null;
|
|
15927
|
+
if (c?.timer) clearTimeout(c.timer);
|
|
15928
|
+
c?.tone?.stop();
|
|
15929
|
+
setPlaying(null);
|
|
15930
|
+
}, []);
|
|
15931
|
+
React.useEffect(() => stop, [stop]);
|
|
15932
|
+
const start = (settings, choice) => {
|
|
15933
|
+
stop();
|
|
15934
|
+
primeAudio();
|
|
15935
|
+
setError(null);
|
|
15936
|
+
const gen = generation.current;
|
|
15937
|
+
const orgDefault = settings.catalogue.some((r) => r.id === settings.orgDefault) ? settings.orgDefault : null;
|
|
15938
|
+
const id = choice || orgDefault;
|
|
15939
|
+
const option = settings.catalogue.find((r) => r.id === id) ?? settings.catalogue.find((r) => r.url === null);
|
|
15940
|
+
setPlaying(choice);
|
|
15941
|
+
current.current = { tone: null, timer: null };
|
|
15942
|
+
const play = (buffer) => {
|
|
15943
|
+
if (gen !== generation.current) return;
|
|
15944
|
+
const loop = buffer ? option?.loop ?? null : null;
|
|
15945
|
+
const tone = playRingtone({ buffer, loop, sinkId });
|
|
15946
|
+
current.current = { tone, timer: setTimeout(stop, previewDurationMs(buffer, loop)) };
|
|
15947
|
+
};
|
|
15948
|
+
if (!option?.url) {
|
|
15949
|
+
play(null);
|
|
15950
|
+
return;
|
|
15951
|
+
}
|
|
15952
|
+
store.decode(option.url).then(play, () => {
|
|
15953
|
+
if (gen !== generation.current) return;
|
|
15954
|
+
stop();
|
|
15955
|
+
setError("No se pudo reproducir el tono.");
|
|
15956
|
+
});
|
|
15957
|
+
};
|
|
15958
|
+
return { playing, error, start, stop };
|
|
15959
|
+
}
|
|
15960
|
+
var ORG_DEFAULT = "";
|
|
15961
|
+
var KEYBOARD_SETTLE_MS = 300;
|
|
15962
|
+
function RingtonePage({
|
|
15963
|
+
store,
|
|
15964
|
+
settings,
|
|
15965
|
+
failed,
|
|
15966
|
+
onRetry,
|
|
15967
|
+
onBack
|
|
15968
|
+
}) {
|
|
15969
|
+
const softphone = useSoftphone();
|
|
15970
|
+
const devices = useDevices();
|
|
15971
|
+
const preview = useRingtonePreview(store, devices.canSelectRingtoneOutput ? devices.currentRingtoneOutputId : null);
|
|
15972
|
+
const [pending, setPending] = React.useState(void 0);
|
|
15973
|
+
const [error, setError] = React.useState(null);
|
|
15974
|
+
const save = useSerialSave(store, {
|
|
15975
|
+
onSettled: () => setPending(void 0),
|
|
15976
|
+
onFailed: () => {
|
|
15977
|
+
setPending(void 0);
|
|
15978
|
+
setError("No se pudo guardar el tono. Int\xE9ntalo de nuevo.");
|
|
15979
|
+
}
|
|
15980
|
+
});
|
|
15981
|
+
const latest = React.useRef(settings);
|
|
15982
|
+
latest.current = settings;
|
|
15983
|
+
const settle = React.useRef(null);
|
|
15984
|
+
const { stop: stopPreview } = preview;
|
|
15985
|
+
React.useEffect(() => softphone.on("incomingCall" /* IncomingCall */, stopPreview), [softphone, stopPreview]);
|
|
15986
|
+
React.useEffect(
|
|
15987
|
+
() => () => {
|
|
15988
|
+
if (settle.current) {
|
|
15989
|
+
clearTimeout(settle.current.timer);
|
|
15990
|
+
save(settle.current.value || null);
|
|
15991
|
+
}
|
|
15992
|
+
},
|
|
15993
|
+
[save]
|
|
15994
|
+
);
|
|
15995
|
+
const commit = (value) => {
|
|
15996
|
+
save(value || null);
|
|
15997
|
+
if (latest.current) preview.start(latest.current, value);
|
|
15998
|
+
};
|
|
15999
|
+
const onSelect = (value, via) => {
|
|
16000
|
+
setError(null);
|
|
16001
|
+
setPending(value);
|
|
16002
|
+
if (settle.current) clearTimeout(settle.current.timer);
|
|
16003
|
+
settle.current = null;
|
|
16004
|
+
if (via === "pointer") {
|
|
16005
|
+
commit(value);
|
|
16006
|
+
return;
|
|
16007
|
+
}
|
|
16008
|
+
preview.stop();
|
|
16009
|
+
settle.current = {
|
|
16010
|
+
value,
|
|
16011
|
+
timer: setTimeout(() => {
|
|
16012
|
+
settle.current = null;
|
|
16013
|
+
commit(value);
|
|
16014
|
+
}, KEYBOARD_SETTLE_MS)
|
|
16015
|
+
};
|
|
16016
|
+
};
|
|
16017
|
+
const onReselect = (value) => {
|
|
16018
|
+
if (!settings) return;
|
|
16019
|
+
if (preview.playing === value) preview.stop();
|
|
16020
|
+
else preview.start(settings, value);
|
|
16021
|
+
};
|
|
16022
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
16023
|
+
SettingsPage,
|
|
16024
|
+
{
|
|
16025
|
+
title: "Tono de llamada",
|
|
16026
|
+
intro: "Lo que suena cuando entra una llamada. Se guarda en tu cuenta, as\xED que es el mismo en todos tus dispositivos.",
|
|
16027
|
+
onBack,
|
|
16028
|
+
children: settings ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
16029
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
16030
|
+
ChoiceList,
|
|
16031
|
+
{
|
|
16032
|
+
label: "Tono de llamada",
|
|
16033
|
+
choices: choicesFor(settings, preview.playing),
|
|
16034
|
+
value: pending !== void 0 ? pending : currentValue(settings),
|
|
16035
|
+
onSelect,
|
|
16036
|
+
onReselect
|
|
16037
|
+
}
|
|
16038
|
+
),
|
|
16039
|
+
error ?? preview.error ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-settings-error", role: "alert", children: error ?? preview.error }) : null
|
|
16040
|
+
] }) : failed ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-empty", children: [
|
|
16041
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-empty-title", children: "No se pudieron cargar los tonos." }),
|
|
16042
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "cv-btn cv-settings-retry", onClick: onRetry, children: "Reintentar" })
|
|
16043
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-empty", role: "status", children: [
|
|
16044
|
+
/* @__PURE__ */ jsxRuntime.jsx(LoaderCircle, { className: "cv-empty-spinner", size: 18, "aria-hidden": true }),
|
|
16045
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-empty-title", children: "Cargando\u2026" })
|
|
16046
|
+
] })
|
|
16047
|
+
}
|
|
16048
|
+
);
|
|
16049
|
+
}
|
|
16050
|
+
function currentValue(settings) {
|
|
16051
|
+
return settings.mine && settings.catalogue.some((r) => r.id === settings.mine) ? settings.mine : ORG_DEFAULT;
|
|
16052
|
+
}
|
|
16053
|
+
function choicesFor(settings, playing) {
|
|
16054
|
+
const org = settings.orgDefault ? settings.catalogue.find((r) => r.id === settings.orgDefault) : void 0;
|
|
16055
|
+
const builtIn = settings.catalogue.find((r) => r.url === null);
|
|
16056
|
+
const mark = (value) => playing === value ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "cv-choice-playing", children: [
|
|
16057
|
+
/* @__PURE__ */ jsxRuntime.jsx(Volume2, { size: 15, "aria-hidden": true }),
|
|
16058
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "cv-sr-only", children: "Sonando" })
|
|
16059
|
+
] }) : void 0;
|
|
16060
|
+
const orgChoice = {
|
|
16061
|
+
value: ORG_DEFAULT,
|
|
16062
|
+
label: org ? "Predeterminado de la organizaci\xF3n" : "Predeterminado",
|
|
16063
|
+
sub: (org ?? builtIn)?.name ?? "",
|
|
16064
|
+
trailing: mark(ORG_DEFAULT)
|
|
16065
|
+
};
|
|
16066
|
+
return [orgChoice, ...settings.catalogue.map((r) => ({ value: r.id, label: r.name, trailing: mark(r.id) }))];
|
|
16067
|
+
}
|
|
16068
|
+
function useSerialSave(store, handlers) {
|
|
16069
|
+
const wanted = React.useRef(null);
|
|
16070
|
+
const busy = React.useRef(false);
|
|
16071
|
+
const on = React.useRef(handlers);
|
|
16072
|
+
on.current = handlers;
|
|
16073
|
+
const [save] = React.useState(() => (id) => {
|
|
16074
|
+
wanted.current = { id };
|
|
16075
|
+
if (busy.current) return;
|
|
16076
|
+
busy.current = true;
|
|
16077
|
+
void (async () => {
|
|
16078
|
+
while (wanted.current) {
|
|
16079
|
+
const { id: next } = wanted.current;
|
|
16080
|
+
wanted.current = null;
|
|
16081
|
+
try {
|
|
16082
|
+
await store.setMine(next);
|
|
16083
|
+
if (!wanted.current) on.current.onSettled();
|
|
16084
|
+
} catch {
|
|
16085
|
+
if (!wanted.current) on.current.onFailed();
|
|
16086
|
+
}
|
|
16087
|
+
}
|
|
16088
|
+
busy.current = false;
|
|
16089
|
+
})();
|
|
16090
|
+
});
|
|
16091
|
+
return save;
|
|
16092
|
+
}
|
|
15347
16093
|
function SettingsTab() {
|
|
15348
|
-
|
|
15349
|
-
|
|
15350
|
-
|
|
15351
|
-
|
|
16094
|
+
const softphone = useSoftphone();
|
|
16095
|
+
const devices = useDevices();
|
|
16096
|
+
const store = ringtoneStore(softphone);
|
|
16097
|
+
const ringtone = useRingtoneSettings(store);
|
|
16098
|
+
const [page, setPage] = React.useState(null);
|
|
16099
|
+
const list = React.useRef(null);
|
|
16100
|
+
const rows = React.useRef({});
|
|
16101
|
+
const returnTo = React.useRef(null);
|
|
16102
|
+
React.useEffect(() => {
|
|
16103
|
+
void softphone.audio.refresh();
|
|
16104
|
+
}, [softphone]);
|
|
16105
|
+
React.useEffect(() => {
|
|
16106
|
+
list.current?.toggleAttribute("inert", page !== null);
|
|
16107
|
+
if (page === null && returnTo.current) {
|
|
16108
|
+
rows.current[returnTo.current]?.focus();
|
|
16109
|
+
returnTo.current = null;
|
|
16110
|
+
}
|
|
16111
|
+
}, [page]);
|
|
16112
|
+
const open = (p) => () => {
|
|
16113
|
+
returnTo.current = p;
|
|
16114
|
+
setPage(p);
|
|
16115
|
+
};
|
|
16116
|
+
const back = () => setPage(null);
|
|
16117
|
+
const rowRef = (p) => (el) => {
|
|
16118
|
+
rows.current[p] = el;
|
|
16119
|
+
};
|
|
16120
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-view cv-settings", children: [
|
|
16121
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-settings-body", ref: list, children: [
|
|
16122
|
+
store ? /* @__PURE__ */ jsxRuntime.jsx(SettingsSection, { title: "Llamadas entrantes", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
16123
|
+
SettingsNavRow,
|
|
16124
|
+
{
|
|
16125
|
+
icon: Music,
|
|
16126
|
+
title: "Tono de llamada",
|
|
16127
|
+
value: ringtoneSummary(ringtone.settings, ringtone.failed),
|
|
16128
|
+
onOpen: open("tone"),
|
|
16129
|
+
rowRef: rowRef("tone")
|
|
16130
|
+
}
|
|
16131
|
+
) }) : null,
|
|
16132
|
+
/* @__PURE__ */ jsxRuntime.jsxs(SettingsSection, { title: "Dispositivos", children: [
|
|
16133
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
16134
|
+
SettingsNavRow,
|
|
16135
|
+
{
|
|
16136
|
+
icon: Mic,
|
|
16137
|
+
title: "Micr\xF3fono",
|
|
16138
|
+
value: deviceSummary("input", devices.inputs, devices.currentInputId),
|
|
16139
|
+
onOpen: open("input"),
|
|
16140
|
+
rowRef: rowRef("input")
|
|
16141
|
+
}
|
|
16142
|
+
),
|
|
16143
|
+
devices.canSelectOutput ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
16144
|
+
SettingsNavRow,
|
|
16145
|
+
{
|
|
16146
|
+
icon: Headphones,
|
|
16147
|
+
title: "Altavoces",
|
|
16148
|
+
value: deviceSummary("output", devices.outputs, devices.currentOutputId),
|
|
16149
|
+
onOpen: open("output"),
|
|
16150
|
+
rowRef: rowRef("output")
|
|
16151
|
+
}
|
|
16152
|
+
) : (
|
|
16153
|
+
// Safari and most of Firefox have no `setSinkId`: offering a page whose choice silently does
|
|
16154
|
+
// nothing is worse than saying where the choice really lives.
|
|
16155
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
16156
|
+
SettingsNavRow,
|
|
16157
|
+
{
|
|
16158
|
+
icon: Headphones,
|
|
16159
|
+
title: "Altavoces",
|
|
16160
|
+
value: "Tu navegador no permite elegirlos desde aqu\xED \u2014 c\xE1mbialos en los ajustes del sistema."
|
|
16161
|
+
}
|
|
16162
|
+
)
|
|
16163
|
+
),
|
|
16164
|
+
devices.canSelectRingtoneOutput ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
16165
|
+
SettingsNavRow,
|
|
16166
|
+
{
|
|
16167
|
+
icon: BellRing,
|
|
16168
|
+
title: "Salida del tono",
|
|
16169
|
+
value: deviceSummary("ringtone", devices.outputs, devices.currentRingtoneOutputId),
|
|
16170
|
+
onOpen: open("ringtone"),
|
|
16171
|
+
rowRef: rowRef("ringtone")
|
|
16172
|
+
}
|
|
16173
|
+
) : null
|
|
16174
|
+
] })
|
|
16175
|
+
] }),
|
|
16176
|
+
page === "tone" && store ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
16177
|
+
RingtonePage,
|
|
16178
|
+
{
|
|
16179
|
+
store,
|
|
16180
|
+
settings: ringtone.settings,
|
|
16181
|
+
failed: ringtone.failed,
|
|
16182
|
+
onRetry: ringtone.retry,
|
|
16183
|
+
onBack: back
|
|
16184
|
+
}
|
|
16185
|
+
) : page !== null && page !== "tone" ? /* @__PURE__ */ jsxRuntime.jsx(DevicePage, { kind: page, onBack: back }) : null
|
|
16186
|
+
] });
|
|
15352
16187
|
}
|
|
15353
16188
|
|
|
15354
16189
|
// src/core/env.ts
|
|
@@ -15587,14 +16422,29 @@ function useCallAudio(enabled) {
|
|
|
15587
16422
|
const offDeviceChange = softphone.on("deviceChange" /* DeviceChange */, () => {
|
|
15588
16423
|
void remote.current?.setOutputDevice(softphone.audio.currentOutputId);
|
|
15589
16424
|
});
|
|
16425
|
+
const ringtones = ringtoneStore(softphone);
|
|
16426
|
+
ringtones?.wantAudio(true);
|
|
16427
|
+
const refreshRingtone = () => {
|
|
16428
|
+
if (softphone.isLeader) void ringtones?.refresh();
|
|
16429
|
+
};
|
|
16430
|
+
refreshRingtone();
|
|
16431
|
+
const offLeader = softphone.on("leaderChanged" /* LeaderChanged */, ({ isLeader }) => {
|
|
16432
|
+
if (isLeader) refreshRingtone();
|
|
16433
|
+
});
|
|
15590
16434
|
const stopTone = () => {
|
|
15591
16435
|
tone.current?.stop();
|
|
15592
16436
|
tone.current = null;
|
|
15593
16437
|
};
|
|
15594
16438
|
const setTone = (next) => {
|
|
15595
16439
|
stopTone();
|
|
15596
|
-
if (next === "ringtone")
|
|
15597
|
-
|
|
16440
|
+
if (next === "ringtone") {
|
|
16441
|
+
const audio = softphone.audio;
|
|
16442
|
+
tone.current = playRingtone({
|
|
16443
|
+
buffer: ringtones?.readyBuffer() ?? null,
|
|
16444
|
+
loop: ringtones?.readyLoop() ?? null,
|
|
16445
|
+
sinkId: audio.canSelectRingtoneOutput ? audio.currentRingtoneOutputId : null
|
|
16446
|
+
});
|
|
16447
|
+
} else if (next === "ringback") tone.current = playRingback();
|
|
15598
16448
|
};
|
|
15599
16449
|
const perCall = [];
|
|
15600
16450
|
const followedCalls = /* @__PURE__ */ new Set();
|
|
@@ -15625,6 +16475,7 @@ function useCallAudio(enabled) {
|
|
|
15625
16475
|
stopTone();
|
|
15626
16476
|
remote.current?.detach();
|
|
15627
16477
|
if (wasConnected && (reason === "local" || reason === "remote")) playCallEndedTone();
|
|
16478
|
+
refreshRingtone();
|
|
15628
16479
|
})
|
|
15629
16480
|
);
|
|
15630
16481
|
};
|
|
@@ -15642,6 +16493,8 @@ function useCallAudio(enabled) {
|
|
|
15642
16493
|
offIncoming();
|
|
15643
16494
|
offActive();
|
|
15644
16495
|
offDeviceChange();
|
|
16496
|
+
offLeader();
|
|
16497
|
+
ringtones?.wantAudio(false);
|
|
15645
16498
|
for (const off of perCall) off();
|
|
15646
16499
|
stopTone();
|
|
15647
16500
|
remote.current?.destroy();
|
|
@@ -15816,6 +16669,7 @@ lucide-react/dist/esm/createLucideIcon.js:
|
|
|
15816
16669
|
lucide-react/dist/esm/icons/arrow-left-right.js:
|
|
15817
16670
|
lucide-react/dist/esm/icons/arrow-left.js:
|
|
15818
16671
|
lucide-react/dist/esm/icons/arrow-right.js:
|
|
16672
|
+
lucide-react/dist/esm/icons/bell-ring.js:
|
|
15819
16673
|
lucide-react/dist/esm/icons/calendar-clock.js:
|
|
15820
16674
|
lucide-react/dist/esm/icons/check.js:
|
|
15821
16675
|
lucide-react/dist/esm/icons/chevron-down.js:
|
|
@@ -15831,6 +16685,7 @@ lucide-react/dist/esm/icons/loader-circle.js:
|
|
|
15831
16685
|
lucide-react/dist/esm/icons/mic-off.js:
|
|
15832
16686
|
lucide-react/dist/esm/icons/mic.js:
|
|
15833
16687
|
lucide-react/dist/esm/icons/monitor-smartphone.js:
|
|
16688
|
+
lucide-react/dist/esm/icons/music.js:
|
|
15834
16689
|
lucide-react/dist/esm/icons/pause.js:
|
|
15835
16690
|
lucide-react/dist/esm/icons/phone-incoming.js:
|
|
15836
16691
|
lucide-react/dist/esm/icons/phone-missed.js:
|
|
@@ -15844,6 +16699,7 @@ lucide-react/dist/esm/icons/timer.js:
|
|
|
15844
16699
|
lucide-react/dist/esm/icons/triangle-alert.js:
|
|
15845
16700
|
lucide-react/dist/esm/icons/user.js:
|
|
15846
16701
|
lucide-react/dist/esm/icons/users.js:
|
|
16702
|
+
lucide-react/dist/esm/icons/volume-2.js:
|
|
15847
16703
|
lucide-react/dist/esm/icons/x.js:
|
|
15848
16704
|
lucide-react/dist/esm/lucide-react.js:
|
|
15849
16705
|
(**
|
|
@@ -15860,5 +16716,6 @@ exports.Dialpad = Dialpad;
|
|
|
15860
16716
|
exports.HistoryTab = HistoryTab;
|
|
15861
16717
|
exports.MicPermissionGate = MicPermissionGate;
|
|
15862
16718
|
exports.SoftphoneWidget = SoftphoneWidget;
|
|
16719
|
+
exports.previewRingtone = previewRingtone;
|
|
15863
16720
|
//# sourceMappingURL=index.cjs.map
|
|
15864
16721
|
//# sourceMappingURL=index.cjs.map
|