flet-box 0.0.0-stage → 1.0.2
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/LICENSE +21 -0
- package/README.md +190 -2
- package/bin/cli.js +394 -0
- package/bin/commands/buildAndroid.js +200 -0
- package/bin/commands/buildIOS.js +237 -0
- package/bin/commands/create.js +406 -0
- package/bin/commands/createBundle.js +375 -0
- package/bin/commands/createComponent.js +115 -0
- package/bin/commands/createScreen.js +161 -0
- package/bin/commands/killServer.js +107 -0
- package/bin/commands/package.js +131 -0
- package/bin/commands/release.js +189 -0
- package/bin/commands/runBundle.js +62 -0
- package/bin/commands/runCloudflare.js +89 -0
- package/bin/commands/runNative.js +109 -0
- package/bin/commands/runServer.js +390 -0
- package/bin/ota/loader.js +361 -0
- package/bin/utils/bundleConfig.js +178 -0
- package/bin/utils/capacitor.js +137 -0
- package/bin/utils/colors.js +215 -0
- package/bin/utils/helpers.js +80 -0
- package/bin/utils/spinner.js +96 -0
- package/bin/utils/templates.js +1824 -0
- package/bin/utils/version.js +108 -0
- package/package.json +58 -4
- package/src/animations/AnimatedBox.js +607 -0
- package/src/animations/AnimatedText.js +105 -0
- package/src/animations/MatrixRain.js +220 -0
- package/src/animations/ParallaxBox.js +369 -0
- package/src/animations/animate.js +332 -0
- package/src/animations/index.js +25 -0
- package/src/app.js +41 -0
- package/src/assets/icon-1024-maskable.png +0 -0
- package/src/assets/icon-192-maskable.png +0 -0
- package/src/assets/icon-192.png +0 -0
- package/src/assets/icon-512-maskable.png +0 -0
- package/src/assets/icon-512.png +0 -0
- package/src/assets/logo.png +0 -0
- package/src/core/App.js +44 -0
- package/src/core/hmr-client.js +142 -0
- package/src/core/index.js +34 -0
- package/src/core/pwa.js +100 -0
- package/src/core/runApp.js +430 -0
- package/src/create-index.sh +237 -0
- package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
- package/src/fonts/icons.css +23 -0
- package/src/index.d.ts +1930 -0
- package/src/index.js +288 -0
- package/src/navigations/AdaptiveScaffold.js +149 -0
- package/src/navigations/AppBar.js +331 -0
- package/src/navigations/BottomNavigation.js +298 -0
- package/src/navigations/CollapsibleSideBar.js +139 -0
- package/src/navigations/Drawer.js +333 -0
- package/src/navigations/DrawerItem.js +260 -0
- package/src/navigations/Router.js +475 -0
- package/src/navigations/Scaffold.js +537 -0
- package/src/navigations/Tabs.js +384 -0
- package/src/navigations/index.js +57 -0
- package/src/services/RamStore.js +213 -0
- package/src/services/Session.js +310 -0
- package/src/services/Storage.js +309 -0
- package/src/services/http.js +350 -0
- package/src/services/index.js +57 -0
- package/src/tools/animation.js +42 -0
- package/src/tools/array.js +95 -0
- package/src/tools/border.js +28 -0
- package/src/tools/clipboard.js +104 -0
- package/src/tools/color.js +329 -0
- package/src/tools/createList.js +215 -0
- package/src/tools/delay.js +55 -0
- package/src/tools/device.js +138 -0
- package/src/tools/dict.js +297 -0
- package/src/tools/dimensions.js +118 -0
- package/src/tools/filter.js +41 -0
- package/src/tools/flex.js +26 -0
- package/src/tools/gradient.js +34 -0
- package/src/tools/grid.js +57 -0
- package/src/tools/index.js +38 -0
- package/src/tools/mapList.js +107 -0
- package/src/tools/margin.js +93 -0
- package/src/tools/memo.js +60 -0
- package/src/tools/os.js +104 -0
- package/src/tools/padding.js +93 -0
- package/src/tools/print.js +69 -0
- package/src/tools/random.js +299 -0
- package/src/tools/ref.js +70 -0
- package/src/tools/rgba.js +4 -0
- package/src/tools/shadow.js +23 -0
- package/src/tools/string.js +85 -0
- package/src/tools/time.js +674 -0
- package/src/tools/transform.js +68 -0
- package/src/tools/transition.js +48 -0
- package/src/tools/useState.js +268 -0
- package/src/tools/uuid.js +42 -0
- package/src/utils/TextInputValidator.js +256 -0
- package/src/utils/composeUpdate.js +65 -0
- package/src/utils/getWidgetProps.js +35 -0
- package/src/utils/index.js +76 -0
- package/src/utils/markdownParser.js +368 -0
- package/src/utils/mediaTime.js +79 -0
- package/src/utils/navigation.js +263 -0
- package/src/utils/scriptInjector.js +73 -0
- package/src/utils/stackPosition.js +31 -0
- package/src/utils/stopWebRefresh.js +77 -0
- package/src/utils/styleInjector.js +38 -0
- package/src/utils/syntaxHighlight.js +339 -0
- package/src/utils/themes.js +347 -0
- package/src/utils/units.js +87 -0
- package/src/utils/visualEffects.js +169 -0
- package/src/widget-factory/addChildren.js +114 -0
- package/src/widget-factory/assignProps.js +256 -0
- package/src/widget-factory/createWidget.js +28 -0
- package/src/widget-factory/effects.js +132 -0
- package/src/widget-factory/index.js +7 -0
- package/src/widget-factory/lifecycle.js +210 -0
- package/src/widget-factory/makeParentable.js +89 -0
- package/src/widget-factory/pixTool.js +40 -0
- package/src/widget-factory/processProps.js +122 -0
- package/src/widget-factory/reactivity.js +98 -0
- package/src/widget-factory/remTool.js +34 -0
- package/src/widget-factory/stackPosition.js +87 -0
- package/src/widget-factory/stringTool.js +26 -0
- package/src/widget-factory/tools.js +249 -0
- package/src/widget-factory/translateProps.js +1049 -0
- package/src/widget-factory/warnings.js +284 -0
- package/src/widget-factory/widgetFactory.js +200 -0
- package/src/widgets/Accordion.js +562 -0
- package/src/widgets/AdMob.js +315 -0
- package/src/widgets/AdSense.js +268 -0
- package/src/widgets/AlertDialog.js +162 -0
- package/src/widgets/Audio.js +311 -0
- package/src/widgets/Avatar.js +210 -0
- package/src/widgets/Badge.js +189 -0
- package/src/widgets/BottomSheet.js +287 -0
- package/src/widgets/Button.js +247 -0
- package/src/widgets/Card.js +33 -0
- package/src/widgets/Carousel.js +301 -0
- package/src/widgets/Chart.js +632 -0
- package/src/widgets/Checkbox.js +106 -0
- package/src/widgets/Chip.js +153 -0
- package/src/widgets/CircularBar.js +352 -0
- package/src/widgets/CircularChart.js +549 -0
- package/src/widgets/CodeViewer.js +236 -0
- package/src/widgets/Column.js +29 -0
- package/src/widgets/Container.js +31 -0
- package/src/widgets/DataTable.js +257 -0
- package/src/widgets/Divider.js +93 -0
- package/src/widgets/DraggBox.js +115 -0
- package/src/widgets/Dropdown.js +505 -0
- package/src/widgets/DroppBox.js +180 -0
- package/src/widgets/FloatingActionButton.js +91 -0
- package/src/widgets/GridView.js +20 -0
- package/src/widgets/Icon.js +33 -0
- package/src/widgets/Image.js +15 -0
- package/src/widgets/Input.js +565 -0
- package/src/widgets/Inspector.js +254 -0
- package/src/widgets/InstallButton.js +110 -0
- package/src/widgets/ListTile.js +224 -0
- package/src/widgets/ListView.js +503 -0
- package/src/widgets/Markdown.js +1236 -0
- package/src/widgets/Modal.js +496 -0
- package/src/widgets/Pagination.js +195 -0
- package/src/widgets/ProgressBar.js +178 -0
- package/src/widgets/QRCode.js +467 -0
- package/src/widgets/Radio.js +132 -0
- package/src/widgets/Rating.js +261 -0
- package/src/widgets/Row.js +29 -0
- package/src/widgets/Skeleton.js +243 -0
- package/src/widgets/Slider.js +527 -0
- package/src/widgets/SnackBar.js +227 -0
- package/src/widgets/Stack.js +29 -0
- package/src/widgets/Stepper.js +296 -0
- package/src/widgets/Switch.js +102 -0
- package/src/widgets/Text.js +98 -0
- package/src/widgets/Tooltip.js +267 -0
- package/src/widgets/TreeView.js +370 -0
- package/src/widgets/Video.js +92 -0
- package/src/widgets/index.js +52 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file AlertDialog.js
|
|
3
|
+
* @description A modal alert dialog built on top of the Modal widget.
|
|
4
|
+
* Supports four semantic variants (normal, danger, warning, success), each
|
|
5
|
+
* with its own icon and accent color. Exposes open/close methods and fires
|
|
6
|
+
* optional callbacks on confirm, cancel, and close.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Modal } from "./Modal.js";
|
|
10
|
+
import { Button } from "./Button.js";
|
|
11
|
+
import { Text } from "./Text.js";
|
|
12
|
+
import { Column } from "./Column.js";
|
|
13
|
+
import { Icon } from "./Icon.js";
|
|
14
|
+
import { colors } from "../utils/themes.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates an AlertDialog — a pre-styled modal with a title, message, icon,
|
|
18
|
+
* and confirm/cancel buttons whose appearance adapts to the chosen variant.
|
|
19
|
+
*
|
|
20
|
+
* Returns a plain object (not an HTMLElement) with `open`, `close`, and
|
|
21
|
+
* `modal` so the caller can programmatically control visibility while keeping
|
|
22
|
+
* the underlying Modal instance accessible for advanced use.
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} props - Configuration for the dialog.
|
|
25
|
+
* @param {string} props.title - Heading text shown at the top of the dialog.
|
|
26
|
+
* @param {string} props.message - Body text providing more detail to the user.
|
|
27
|
+
* @param {Function} [props.onConfirm] - Called when the user clicks the confirm button, before the dialog closes.
|
|
28
|
+
* @param {Function} [props.onCancel] - Called when the user clicks the cancel button, before the dialog closes.
|
|
29
|
+
* @param {Function} [props.onClose] - Called whenever the dialog closes, regardless of how it was dismissed.
|
|
30
|
+
* @param {'normal'|'danger'|'warning'|'success'} [props.variant='normal'] - Determines the icon and button accent color.
|
|
31
|
+
* @param {boolean} [props.showCancel=true] - When false, the cancel button is omitted.
|
|
32
|
+
* @param {string} [props.confirmText='Accept'] - Label for the confirm button.
|
|
33
|
+
* @param {string} [props.cancelText='Cancel'] - Label for the cancel button.
|
|
34
|
+
* @returns {{ open: Function, close: Function, modal: HTMLElement }} Controller object for the dialog.
|
|
35
|
+
*/
|
|
36
|
+
export const AlertDialog = (props) => {
|
|
37
|
+
let {
|
|
38
|
+
title,
|
|
39
|
+
message,
|
|
40
|
+
onConfirm,
|
|
41
|
+
onCancel,
|
|
42
|
+
onClose,
|
|
43
|
+
variant = "normal", // 'normal', 'danger', 'warning', 'success'
|
|
44
|
+
showCancel = true,
|
|
45
|
+
confirmText = "Accept",
|
|
46
|
+
cancelText = "Cancel",
|
|
47
|
+
...rest
|
|
48
|
+
} = props;
|
|
49
|
+
|
|
50
|
+
// Map each variant to its icon name, icon color, and confirm-button color
|
|
51
|
+
const variantColors = {
|
|
52
|
+
normal: { confirm: colors.primary, icon: "info", iconColor: colors.info },
|
|
53
|
+
danger: {
|
|
54
|
+
confirm: colors.danger,
|
|
55
|
+
icon: "warning",
|
|
56
|
+
iconColor: colors.danger,
|
|
57
|
+
},
|
|
58
|
+
warning: {
|
|
59
|
+
confirm: colors.warning,
|
|
60
|
+
icon: "error",
|
|
61
|
+
iconColor: colors.warning,
|
|
62
|
+
},
|
|
63
|
+
success: {
|
|
64
|
+
confirm: colors.success,
|
|
65
|
+
icon: "check_circle",
|
|
66
|
+
iconColor: colors.success,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const variantStyle = variantColors[variant] || variantColors.normal;
|
|
70
|
+
|
|
71
|
+
// Decorative icon at the top of the dialog body
|
|
72
|
+
const icon = Icon({
|
|
73
|
+
name: variantStyle.icon,
|
|
74
|
+
size: 40,
|
|
75
|
+
color: variantStyle.iconColor,
|
|
76
|
+
marginBottom: 8,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Centered column: icon → title → message
|
|
80
|
+
const content = Column({
|
|
81
|
+
alignItems: "center",
|
|
82
|
+
textAlign: "center",
|
|
83
|
+
children: [
|
|
84
|
+
icon,
|
|
85
|
+
Text({
|
|
86
|
+
value: title,
|
|
87
|
+
size: 24,
|
|
88
|
+
fontWeight: "bold",
|
|
89
|
+
color: colors.text,
|
|
90
|
+
textAlign: "center",
|
|
91
|
+
}),
|
|
92
|
+
Text({
|
|
93
|
+
value: message,
|
|
94
|
+
size: 18,
|
|
95
|
+
fontWeight: "bold",
|
|
96
|
+
color: colors.textSecondary,
|
|
97
|
+
textAlign: "center",
|
|
98
|
+
marginTop: 8,
|
|
99
|
+
}),
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Build the action-button row — cancel first (if shown), then confirm
|
|
104
|
+
const actions = [];
|
|
105
|
+
|
|
106
|
+
if (showCancel) {
|
|
107
|
+
actions.push(
|
|
108
|
+
Button({
|
|
109
|
+
text: cancelText,
|
|
110
|
+
borderColor: colors.danger,
|
|
111
|
+
variant: "filled",
|
|
112
|
+
color: colors.danger,
|
|
113
|
+
onPress: () => {
|
|
114
|
+
if (onCancel) onCancel();
|
|
115
|
+
closeModal();
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
actions.push(
|
|
122
|
+
Button({
|
|
123
|
+
text: confirmText,
|
|
124
|
+
variant: "filled",
|
|
125
|
+
bgColor: variantStyle.confirm,
|
|
126
|
+
color: colors.text,
|
|
127
|
+
borderColor: colors.surface,
|
|
128
|
+
onPress: () => {
|
|
129
|
+
if (onConfirm) onConfirm();
|
|
130
|
+
closeModal();
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
let modal = null;
|
|
136
|
+
// Placeholder until the modal is created below — avoids a temporal dead-zone
|
|
137
|
+
let closeModal = () => {};
|
|
138
|
+
|
|
139
|
+
// Delegate to the base Modal widget, passing our content and actions
|
|
140
|
+
modal = Modal({
|
|
141
|
+
content: content,
|
|
142
|
+
actions: actions,
|
|
143
|
+
showCloseButton: false,
|
|
144
|
+
width: 320,
|
|
145
|
+
borderRadius: 24,
|
|
146
|
+
onClose: () => {
|
|
147
|
+
if (onClose) onClose();
|
|
148
|
+
},
|
|
149
|
+
...rest,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Now that modal exists, wire up the real close function
|
|
153
|
+
closeModal = () => modal.close();
|
|
154
|
+
|
|
155
|
+
// Public controller API
|
|
156
|
+
const open = () => modal.open();
|
|
157
|
+
const close = () => modal.close();
|
|
158
|
+
|
|
159
|
+
return { open, close, modal };
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export default AlertDialog;
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Audio.js
|
|
3
|
+
* @description A thin wrapper around the native `<audio>` element that adds
|
|
4
|
+
* reliable duration detection (with an AudioContext fallback for formats where
|
|
5
|
+
* the browser cannot report duration from metadata alone), progress callbacks,
|
|
6
|
+
* and a clean imperative API for controlling playback.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { WidgetFactory } from "../widget-factory/index.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates an Audio widget — an `<audio>` element enriched with helper methods
|
|
13
|
+
* and reliable duration detection.
|
|
14
|
+
*
|
|
15
|
+
* Duration detection works in three stages:
|
|
16
|
+
* 1. Read directly from `audio.duration` on `loadedmetadata` / `canplay`.
|
|
17
|
+
* 2. Poll every 200 ms for up to two seconds if the element still reports NaN or Infinity.
|
|
18
|
+
* 3. Fetch the file as an ArrayBuffer and decode it with AudioContext as a last resort.
|
|
19
|
+
*
|
|
20
|
+
* @param {Object} props - Configuration for the audio widget.
|
|
21
|
+
* @param {string} props.src - URL of the audio file to load.
|
|
22
|
+
* @param {boolean} [props.autoplay=false] - Whether playback starts automatically on load.
|
|
23
|
+
* @param {boolean} [props.controls=false] - Whether to render the browser's native playback controls.
|
|
24
|
+
* @param {boolean} [props.loop=false] - Whether playback restarts automatically when the track ends.
|
|
25
|
+
* @param {boolean} [props.muted=false] - Whether the audio starts muted.
|
|
26
|
+
* @param {number} [props.volume=1] - Initial volume level (0.0–1.0).
|
|
27
|
+
* @param {Function} [props.onPlay] - Fired when playback begins.
|
|
28
|
+
* @param {Function} [props.onPause] - Fired when playback is paused.
|
|
29
|
+
* @param {Function} [props.onEnd] - Fired when the track finishes.
|
|
30
|
+
* @param {Function} [props.onTimeUpdate] - Fired on each timeupdate event. Receives `(currentTime, duration, percentPlayed)`.
|
|
31
|
+
* @param {Function} [props.onProgress] - Fired on each timeupdate event with only the percent value.
|
|
32
|
+
* @param {Function} [props.onLoad] - Fired once when the duration is successfully resolved. Receives `(duration)`.
|
|
33
|
+
* @param {Function} [props.ref] - Callback that receives the `<audio>` element after creation.
|
|
34
|
+
* @returns {HTMLAudioElement} The audio element, extended with playback control methods.
|
|
35
|
+
*/
|
|
36
|
+
export const Audio = (props) => {
|
|
37
|
+
const {
|
|
38
|
+
src,
|
|
39
|
+
autoplay = false,
|
|
40
|
+
controls = false,
|
|
41
|
+
loop = false,
|
|
42
|
+
muted = false,
|
|
43
|
+
volume = 1,
|
|
44
|
+
onPlay,
|
|
45
|
+
onPause,
|
|
46
|
+
onEnd,
|
|
47
|
+
onTimeUpdate,
|
|
48
|
+
onProgress,
|
|
49
|
+
onLoad,
|
|
50
|
+
ref,
|
|
51
|
+
...rest
|
|
52
|
+
} = props;
|
|
53
|
+
|
|
54
|
+
const audio = WidgetFactory({
|
|
55
|
+
tag: "audio",
|
|
56
|
+
widgetName: "Audio",
|
|
57
|
+
src: src,
|
|
58
|
+
...rest,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Audio attributes are element properties, not CSS attributes — set them directly
|
|
62
|
+
audio.controls = Boolean(controls);
|
|
63
|
+
audio.loop = Boolean(loop);
|
|
64
|
+
audio.muted = Boolean(muted);
|
|
65
|
+
audio.autoplay = Boolean(autoplay);
|
|
66
|
+
audio.preload = "metadata";
|
|
67
|
+
audio.volume = typeof volume === "number" ? volume : 1;
|
|
68
|
+
|
|
69
|
+
// Keep native play/pause bound before we might override them
|
|
70
|
+
const nativePlay = audio.play.bind(audio);
|
|
71
|
+
const nativePause = audio.pause.bind(audio);
|
|
72
|
+
|
|
73
|
+
let currentTime = 0;
|
|
74
|
+
let duration = 0;
|
|
75
|
+
let durationPoll = null;
|
|
76
|
+
let audioContextFallbackAttempted = false;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Recalculates the current playback position and fires the progress callbacks.
|
|
80
|
+
* Called on every `timeupdate` event from the element.
|
|
81
|
+
*/
|
|
82
|
+
const updateTime = () => {
|
|
83
|
+
currentTime = audio.currentTime || 0;
|
|
84
|
+
const percent = duration ? (currentTime / duration) * 100 : 0;
|
|
85
|
+
if (onTimeUpdate) onTimeUpdate(currentTime, duration, percent);
|
|
86
|
+
if (onProgress) onProgress(percent);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Stores the resolved duration and fires `onLoad` exactly once.
|
|
91
|
+
* Clears any active polling interval once the duration is known.
|
|
92
|
+
*
|
|
93
|
+
* @param {number} dur - The resolved audio duration in seconds.
|
|
94
|
+
*/
|
|
95
|
+
const setDuration = (dur) => {
|
|
96
|
+
if (dur > 0 && duration === 0) {
|
|
97
|
+
duration = dur;
|
|
98
|
+
if (onLoad) onLoad(duration);
|
|
99
|
+
if (durationPoll) clearInterval(durationPoll);
|
|
100
|
+
durationPoll = null;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Attempts to read the duration directly from the audio element.
|
|
106
|
+
*
|
|
107
|
+
* @returns {boolean} `true` if a valid finite duration was obtained.
|
|
108
|
+
*/
|
|
109
|
+
const tryGetDurationFromAudio = () => {
|
|
110
|
+
const d = audio.duration;
|
|
111
|
+
if (d && d !== Infinity && d > 0) {
|
|
112
|
+
setDuration(d);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Starts a polling interval that retries duration detection every 200 ms.
|
|
120
|
+
* Useful for formats (e.g. MP3 with VBR) where the browser reports duration
|
|
121
|
+
* later than `loadedmetadata`.
|
|
122
|
+
*/
|
|
123
|
+
const pollDuration = () => {
|
|
124
|
+
if (durationPoll) return;
|
|
125
|
+
durationPoll = setInterval(() => {
|
|
126
|
+
if (tryGetDurationFromAudio()) {
|
|
127
|
+
clearInterval(durationPoll);
|
|
128
|
+
durationPoll = null;
|
|
129
|
+
}
|
|
130
|
+
}, 200);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Last-resort fallback: fetches the audio file as a binary buffer, decodes
|
|
135
|
+
* it with the Web Audio API, and extracts the duration from the resulting
|
|
136
|
+
* AudioBuffer. Only attempted once per instance.
|
|
137
|
+
*
|
|
138
|
+
* This is necessary for some file formats where the browser's media pipeline
|
|
139
|
+
* cannot determine duration without fully downloading the file.
|
|
140
|
+
*/
|
|
141
|
+
const fallbackGetDuration = async () => {
|
|
142
|
+
if (audioContextFallbackAttempted) return;
|
|
143
|
+
audioContextFallbackAttempted = true;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const response = await fetch(src);
|
|
147
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
148
|
+
const audioContext = new (
|
|
149
|
+
window.AudioContext || window.webkitAudioContext
|
|
150
|
+
)();
|
|
151
|
+
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
152
|
+
const durationSeconds = audioBuffer.duration;
|
|
153
|
+
setDuration(durationSeconds);
|
|
154
|
+
audioContext.close();
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.warn("[Audio] Fallback duration fetch failed:", err);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Wire up native audio element events
|
|
161
|
+
audio.addEventListener("loadedmetadata", () => {
|
|
162
|
+
if (!tryGetDurationFromAudio()) pollDuration();
|
|
163
|
+
});
|
|
164
|
+
audio.addEventListener("durationchange", () => tryGetDurationFromAudio());
|
|
165
|
+
audio.addEventListener("canplay", () => {
|
|
166
|
+
if (!tryGetDurationFromAudio()) pollDuration();
|
|
167
|
+
});
|
|
168
|
+
audio.addEventListener("timeupdate", updateTime);
|
|
169
|
+
audio.addEventListener("play", () => {
|
|
170
|
+
if (onPlay) onPlay();
|
|
171
|
+
});
|
|
172
|
+
audio.addEventListener("pause", () => {
|
|
173
|
+
if (onPause) onPause();
|
|
174
|
+
});
|
|
175
|
+
audio.addEventListener("ended", () => {
|
|
176
|
+
if (onEnd) onEnd();
|
|
177
|
+
});
|
|
178
|
+
audio.addEventListener("error", (e) => console.warn("[Audio] Error:", e));
|
|
179
|
+
|
|
180
|
+
// Kick off duration detection immediately; if unavailable after 2 s, use fetch fallback
|
|
181
|
+
if (!tryGetDurationFromAudio()) {
|
|
182
|
+
pollDuration();
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
if (!duration) fallbackGetDuration();
|
|
185
|
+
}, 2000);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Cancel the duration polling interval when the widget unmounts
|
|
189
|
+
audio.onUnmount(() => {
|
|
190
|
+
if (durationPoll) clearInterval(durationPoll);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ========== PUBLIC PLAYBACK API ==========
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Starts playback. Silently swallows the NotAllowedError thrown by browsers
|
|
197
|
+
* that require a user gesture before audio can play.
|
|
198
|
+
* @returns {HTMLAudioElement} The audio element (for chaining).
|
|
199
|
+
*/
|
|
200
|
+
audio.playAudio = () => {
|
|
201
|
+
nativePlay().catch((e) => console.warn(e));
|
|
202
|
+
return audio;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Pauses playback.
|
|
207
|
+
* @returns {HTMLAudioElement} The audio element (for chaining).
|
|
208
|
+
*/
|
|
209
|
+
audio.pauseAudio = () => {
|
|
210
|
+
nativePause();
|
|
211
|
+
return audio;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Stops playback and resets the playhead to the beginning.
|
|
216
|
+
* @returns {HTMLAudioElement} The audio element (for chaining).
|
|
217
|
+
*/
|
|
218
|
+
audio.stop = () => {
|
|
219
|
+
nativePause();
|
|
220
|
+
audio.currentTime = 0;
|
|
221
|
+
updateTime();
|
|
222
|
+
return audio;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Seeks to an absolute position in seconds.
|
|
227
|
+
* Silently ignores out-of-range values.
|
|
228
|
+
*
|
|
229
|
+
* @param {number} seconds - Target time in seconds.
|
|
230
|
+
* @returns {HTMLAudioElement} The audio element (for chaining).
|
|
231
|
+
*/
|
|
232
|
+
audio.seekTo = (seconds) => {
|
|
233
|
+
if (seconds >= 0 && seconds <= (duration || audio.duration)) {
|
|
234
|
+
audio.currentTime = seconds;
|
|
235
|
+
updateTime();
|
|
236
|
+
}
|
|
237
|
+
return audio;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Seeks to a percentage of the total duration (0–100).
|
|
242
|
+
*
|
|
243
|
+
* @param {number} percent - Target position as a percentage.
|
|
244
|
+
* @returns {HTMLAudioElement} The audio element (for chaining).
|
|
245
|
+
*/
|
|
246
|
+
audio.seekToPercent = (percent) => {
|
|
247
|
+
const total = duration || audio.duration;
|
|
248
|
+
if (total) audio.seekTo((percent / 100) * total);
|
|
249
|
+
return audio;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/** @returns {number} The current playback position in seconds. */
|
|
253
|
+
audio.getCurrentTime = () => currentTime;
|
|
254
|
+
|
|
255
|
+
/** @returns {number} The total track duration in seconds, or 0 if not yet known. */
|
|
256
|
+
audio.getDuration = () => duration || audio.duration || 0;
|
|
257
|
+
|
|
258
|
+
/** @returns {number} The current playback position as a percentage (0–100). */
|
|
259
|
+
audio.getProgressPercent = () => {
|
|
260
|
+
const total = duration || audio.duration;
|
|
261
|
+
return total ? (currentTime / total) * 100 : 0;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Increases volume by `step` (clamped to 1.0).
|
|
266
|
+
* @param {number} [step=0.1] - Volume increment.
|
|
267
|
+
* @returns {HTMLAudioElement} The audio element (for chaining).
|
|
268
|
+
*/
|
|
269
|
+
audio.volumeUp = (step = 0.1) => {
|
|
270
|
+
audio.volume = Math.min(1, audio.volume + step);
|
|
271
|
+
return audio;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Decreases volume by `step` (clamped to 0.0).
|
|
276
|
+
* @param {number} [step=0.1] - Volume decrement.
|
|
277
|
+
* @returns {HTMLAudioElement} The audio element (for chaining).
|
|
278
|
+
*/
|
|
279
|
+
audio.volumeDown = (step = 0.1) => {
|
|
280
|
+
audio.volume = Math.max(0, audio.volume - step);
|
|
281
|
+
return audio;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Mutes the audio element without changing the volume setting.
|
|
286
|
+
* @returns {HTMLAudioElement} The audio element (for chaining).
|
|
287
|
+
*/
|
|
288
|
+
audio.muteAudio = () => {
|
|
289
|
+
audio.muted = true;
|
|
290
|
+
return audio;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Unmutes the audio element.
|
|
295
|
+
* @returns {HTMLAudioElement} The audio element (for chaining).
|
|
296
|
+
*/
|
|
297
|
+
audio.unmuteAudio = () => {
|
|
298
|
+
audio.muted = false;
|
|
299
|
+
return audio;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/** @returns {boolean} `true` if the audio is currently playing. */
|
|
303
|
+
audio.isPlaying = () => !audio.paused;
|
|
304
|
+
|
|
305
|
+
// Expose the element via ref callback if provided
|
|
306
|
+
if (ref && typeof ref === "function") ref(audio);
|
|
307
|
+
|
|
308
|
+
return audio;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export default Audio;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Avatar.js
|
|
3
|
+
* @description A user avatar widget that renders an image, an icon, or
|
|
4
|
+
* initials derived from a name, depending on what props are provided.
|
|
5
|
+
* Falls back gracefully: image → icon → initials → generic person icon.
|
|
6
|
+
* Supports circular, rounded-square, and square shapes, and exposes a method
|
|
7
|
+
* to update its content without rebuilding the DOM.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { WidgetFactory } from "../widget-factory/index.js";
|
|
11
|
+
import { colors } from "../utils/themes.js";
|
|
12
|
+
import { Image } from "./Image.js";
|
|
13
|
+
import { Icon } from "./Icon.js";
|
|
14
|
+
import { Text } from "./Text.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates an Avatar widget.
|
|
18
|
+
*
|
|
19
|
+
* Content priority (first match wins):
|
|
20
|
+
* 1. `src` — renders an `<img>` filling the avatar circle.
|
|
21
|
+
* 2. `icon` — renders a Material icon centered in the avatar.
|
|
22
|
+
* 3. `name` — extracts up to two initials and renders them as text.
|
|
23
|
+
* 4. Fallback — renders a generic "person" icon.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} props - Configuration for the avatar.
|
|
26
|
+
* @param {string} [props.src] - URL of a profile image.
|
|
27
|
+
* @param {string} [props.name] - Full name from which initials are derived (e.g. "Jane Doe" → "JD").
|
|
28
|
+
* @param {number} [props.size=40] - Diameter of the avatar in pixels.
|
|
29
|
+
* @param {number} [props.fontSize=16] - Font size of initials text in pixels (overridden by the auto-computed size when `size` changes).
|
|
30
|
+
* @param {'circle'|'rounded'|'square'} [props.shape='circle'] - Shape of the avatar container.
|
|
31
|
+
* @param {string} [props.bgColor=colors.primary] - Background color shown behind icons or initials.
|
|
32
|
+
* @param {string} [props.textColor=colors.text] - Color of icon or initials.
|
|
33
|
+
* @param {string} [props.icon] - Material icon name to display instead of an image or initials.
|
|
34
|
+
* @param {Function} [props.onPress] - Click handler. When provided, a hover scale effect is also applied.
|
|
35
|
+
* @returns {HTMLElement} The avatar container element, augmented with an `updateContent` method.
|
|
36
|
+
*/
|
|
37
|
+
export const Avatar = (props) => {
|
|
38
|
+
let {
|
|
39
|
+
src,
|
|
40
|
+
name,
|
|
41
|
+
size = 40,
|
|
42
|
+
fontSize = 16,
|
|
43
|
+
shape = "circle",
|
|
44
|
+
bgColor = colors.primary,
|
|
45
|
+
textColor = colors.text,
|
|
46
|
+
icon,
|
|
47
|
+
onPress,
|
|
48
|
+
...rest
|
|
49
|
+
} = props;
|
|
50
|
+
|
|
51
|
+
// Map shape to a CSS border-radius value
|
|
52
|
+
let borderRadius = "50%";
|
|
53
|
+
if (shape === "rounded") borderRadius = `${size * 0.2}px`;
|
|
54
|
+
if (shape === "square") borderRadius = "0";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extracts up to two initials from a full name.
|
|
58
|
+
* Single-word names return just the first letter; multi-word names use
|
|
59
|
+
* the first letter of the first and last words.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} fullName - The user's full name.
|
|
62
|
+
* @returns {string} 1–2 uppercase initials, or "?" if the name is empty.
|
|
63
|
+
*/
|
|
64
|
+
const getInitials = (fullName) => {
|
|
65
|
+
if (!fullName) return "?";
|
|
66
|
+
const parts = fullName.trim().split(/\s+/);
|
|
67
|
+
if (parts.length === 1) return parts[0].charAt(0).toUpperCase();
|
|
68
|
+
return (
|
|
69
|
+
parts[0].charAt(0) + parts[parts.length - 1].charAt(0)
|
|
70
|
+
).toUpperCase();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Choose the appropriate child element based on prop priority
|
|
74
|
+
let content = null;
|
|
75
|
+
if (src) {
|
|
76
|
+
content = Image({
|
|
77
|
+
src: src,
|
|
78
|
+
width: "100%",
|
|
79
|
+
height: "100%",
|
|
80
|
+
fit: "cover",
|
|
81
|
+
});
|
|
82
|
+
} else if (icon) {
|
|
83
|
+
content = Icon({
|
|
84
|
+
name: icon,
|
|
85
|
+
size: size * 0.5,
|
|
86
|
+
color: textColor,
|
|
87
|
+
});
|
|
88
|
+
} else if (name) {
|
|
89
|
+
const initials = getInitials(name);
|
|
90
|
+
content = Text({
|
|
91
|
+
text: initials,
|
|
92
|
+
color: textColor,
|
|
93
|
+
size: size * 0.4,
|
|
94
|
+
weight: "bold",
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
// Generic fallback when no content is supplied
|
|
98
|
+
content = Icon({
|
|
99
|
+
name: "person",
|
|
100
|
+
size: size * 0.5,
|
|
101
|
+
color: textColor,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const avatar = WidgetFactory({
|
|
106
|
+
widgetName: "Avatar",
|
|
107
|
+
display: "inline-flex",
|
|
108
|
+
alignItems: "center",
|
|
109
|
+
justifyContent: "center",
|
|
110
|
+
width: size,
|
|
111
|
+
height: size,
|
|
112
|
+
borderRadius: borderRadius,
|
|
113
|
+
backgroundColor: bgColor,
|
|
114
|
+
color: textColor,
|
|
115
|
+
overflow: "hidden",
|
|
116
|
+
fontSize: fontSize,
|
|
117
|
+
fontWeight: "500",
|
|
118
|
+
userSelect: "none",
|
|
119
|
+
cursor: onPress ? "pointer" : "default",
|
|
120
|
+
transition: "transform 0.2s ease",
|
|
121
|
+
child: content,
|
|
122
|
+
onclick: onPress,
|
|
123
|
+
...rest,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Add a subtle scale-up on hover when the avatar is interactive
|
|
127
|
+
if (onPress) {
|
|
128
|
+
avatar.addEventListener("mouseenter", () => {
|
|
129
|
+
avatar.style.transform = "scale(1.05)";
|
|
130
|
+
});
|
|
131
|
+
avatar.addEventListener("mouseleave", () => {
|
|
132
|
+
avatar.style.transform = "scale(1)";
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Updates the avatar's content and/or dimensions without recreating the element.
|
|
138
|
+
* Only the properties present in `newProps` are applied; others are left as-is.
|
|
139
|
+
*
|
|
140
|
+
* @param {Object} newProps - Partial props to apply.
|
|
141
|
+
* @param {string} [newProps.src] - New image URL.
|
|
142
|
+
* @param {string} [newProps.name] - New name (re-derives initials).
|
|
143
|
+
* @param {string} [newProps.icon] - New icon name.
|
|
144
|
+
* @param {string} [newProps.bgColor] - New background color.
|
|
145
|
+
* @param {number|string} [newProps.size] - New avatar diameter.
|
|
146
|
+
*/
|
|
147
|
+
avatar.updateContent = (newProps) => {
|
|
148
|
+
// Sync the mutable local variables with any supplied overrides
|
|
149
|
+
if (newProps.src !== undefined) src = newProps.src;
|
|
150
|
+
if (newProps.name !== undefined) name = newProps.name;
|
|
151
|
+
if (newProps.icon !== undefined) icon = newProps.icon;
|
|
152
|
+
if (newProps.bgColor !== undefined)
|
|
153
|
+
avatar.style.backgroundColor = newProps.bgColor;
|
|
154
|
+
if (newProps.size !== undefined) {
|
|
155
|
+
const newSize = newProps.size;
|
|
156
|
+
avatar.style.width =
|
|
157
|
+
typeof newSize === "number" ? `${newSize}px` : newSize;
|
|
158
|
+
avatar.style.height =
|
|
159
|
+
typeof newSize === "number" ? `${newSize}px` : newSize;
|
|
160
|
+
avatar.style.fontSize =
|
|
161
|
+
typeof newSize === "number" ? `${newSize * 0.4}px` : "16px";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Rebuild the inner content using the updated props
|
|
165
|
+
let newContent = null;
|
|
166
|
+
if (newProps.src !== undefined || src !== undefined) {
|
|
167
|
+
const finalSrc = newProps.src !== undefined ? newProps.src : src;
|
|
168
|
+
newContent = Image({
|
|
169
|
+
src: finalSrc,
|
|
170
|
+
width: "100%",
|
|
171
|
+
height: "100%",
|
|
172
|
+
fit: "cover",
|
|
173
|
+
});
|
|
174
|
+
} else if (newProps.icon !== undefined || icon !== undefined) {
|
|
175
|
+
const finalIcon = newProps.icon !== undefined ? newProps.icon : icon;
|
|
176
|
+
const finalSize = newProps.size || size;
|
|
177
|
+
newContent = Icon({
|
|
178
|
+
name: finalIcon,
|
|
179
|
+
size: finalSize * 0.5,
|
|
180
|
+
color: textColor,
|
|
181
|
+
});
|
|
182
|
+
} else if (newProps.name !== undefined || name !== undefined) {
|
|
183
|
+
const finalName = newProps.name !== undefined ? newProps.name : name;
|
|
184
|
+
const initials = getInitials(finalName);
|
|
185
|
+
const finalSize = newProps.size || size;
|
|
186
|
+
newContent = Text({
|
|
187
|
+
text: initials,
|
|
188
|
+
color: textColor,
|
|
189
|
+
size: finalSize * 0.4,
|
|
190
|
+
weight: "bold",
|
|
191
|
+
});
|
|
192
|
+
} else {
|
|
193
|
+
const finalSize = newProps.size || size;
|
|
194
|
+
newContent = Icon({
|
|
195
|
+
name: "person",
|
|
196
|
+
size: finalSize * 0.5,
|
|
197
|
+
color: textColor,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Replace the existing child with the new content
|
|
202
|
+
while (avatar.firstChild) avatar.removeChild(avatar.firstChild);
|
|
203
|
+
avatar.appendChild(newContent);
|
|
204
|
+
avatar._child = newContent;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return avatar;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export default Avatar;
|