@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.
Files changed (38) hide show
  1. package/dist/{call-VKq1I8yP.d.cts → call-CaPDWVk3.d.cts} +44 -1
  2. package/dist/{call-VKq1I8yP.d.ts → call-CaPDWVk3.d.ts} +44 -1
  3. package/dist/{chunk-LMJXX4YM.js → chunk-UHNLEBBO.js} +4 -2
  4. package/dist/chunk-UHNLEBBO.js.map +1 -0
  5. package/dist/element/index.d.ts +68 -9
  6. package/dist/element/index.js +962 -114
  7. package/dist/element/index.js.map +1 -1
  8. package/dist/{engine-C0BOkIcW.d.ts → engine-BgBsIcEa.d.ts} +1 -1
  9. package/dist/{engine-BlQBo2Ys.d.cts → engine-CU2rEo3g.d.cts} +1 -1
  10. package/dist/index.cjs +120 -10
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +5 -5
  13. package/dist/index.d.ts +5 -5
  14. package/dist/index.js +69 -10
  15. package/dist/index.js.map +1 -1
  16. package/dist/react/index.cjs +2 -0
  17. package/dist/react/index.cjs.map +1 -1
  18. package/dist/react/index.d.cts +2 -2
  19. package/dist/react/index.d.ts +2 -2
  20. package/dist/react/index.js +1 -1
  21. package/dist/{softphone-Cyq3hiIM.d.ts → softphone-DEt0TtQw.d.ts} +26 -10
  22. package/dist/{softphone-DlGV12Xp.d.cts → softphone-dZMkW8V1.d.cts} +26 -10
  23. package/dist/softphone.css +110 -12
  24. package/dist/{sources-IRKNQPWW.js → sources-M3GYMS6V.js} +54 -3
  25. package/dist/sources-M3GYMS6V.js.map +1 -0
  26. package/dist/testing/index.d.cts +2 -2
  27. package/dist/testing/index.d.ts +2 -2
  28. package/dist/ui/index.cjs +885 -28
  29. package/dist/ui/index.cjs.map +1 -1
  30. package/dist/ui/index.d.cts +25 -3
  31. package/dist/ui/index.d.ts +25 -3
  32. package/dist/ui/index.js +885 -31
  33. package/dist/ui/index.js.map +1 -1
  34. package/dist/zadarma/index.d.cts +2 -2
  35. package/dist/zadarma/index.d.ts +2 -2
  36. package/package.json +1 -1
  37. package/dist/chunk-LMJXX4YM.js.map +0 -1
  38. 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(ctx.destination);
13790
- const osc = ctx.createOscillator();
13791
- osc.type = "sine";
13792
- osc.frequency.value = TONE_HZ;
13793
- osc.connect(gain);
13794
- osc.start();
13795
- const LEVEL = 0.8;
13796
- const RAMP = 0.02;
13797
- const burst = (at, dur) => {
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(LEVEL, at + RAMP);
13800
- gain.gain.setValueAtTime(LEVEL, at + dur - RAMP);
13801
- gain.gain.linearRampToValueAtTime(0, at + dur);
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 scheduleCycle = (at) => {
13804
- burst(at, 0.4);
13805
- burst(at + 0.6, 0.4);
13806
- return 3;
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 += scheduleCycle(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
- try {
13829
- osc.stop();
13830
- } catch {
14040
+ for (const o of scheduled) {
14041
+ try {
14042
+ o.stop();
14043
+ } catch {
14044
+ }
13831
14045
  }
14046
+ scheduled.clear();
13832
14047
  try {
13833
- gain.disconnect();
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
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "cv-view cv-settings", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "cv-settings-body", children: [
15349
- /* @__PURE__ */ jsxRuntime.jsx(SettingsSection, { title: "Dispositivos", children: /* @__PURE__ */ jsxRuntime.jsx(DevicePickers, {}) }),
15350
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "cv-settings-more", children: "M\xE1s ajustes se ir\xE1n a\xF1adiendo pr\xF3ximamente." })
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") tone.current = playRingtone();
15597
- else if (next === "ringback") tone.current = playRingback();
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