@@ -116,14 +116,25 @@ export function cn(...classes: (string | undefined | null | false)[]): string {
116116 return classes . filter ( Boolean ) . join ( ' ' ) ;
117117}
118118
119+ // Declare Android types for rich text
120+ declare const android : any ;
121+
119122/**
120- * Create iOS attributed string for rich text
123+ * Create attributed/spannable string for rich text (iOS and Android)
121124 */
122125export function createAttributedString ( text : string , attributes : Record < string , unknown > ) : any {
123- if ( ! global . isIOS ) {
124- return null ;
126+ if ( global . isIOS ) {
127+ return createIOSAttributedString ( text , attributes ) ;
128+ } else if ( global . isAndroid ) {
129+ return createAndroidSpannableString ( text , attributes ) ;
125130 }
131+ return null ;
132+ }
126133
134+ /**
135+ * Create iOS attributed string for rich text
136+ */
137+ function createIOSAttributedString ( text : string , attributes : Record < string , unknown > ) : any {
127138 const attributedString = NSMutableAttributedString . alloc ( ) . initWithString ( text ) ;
128139
129140 if ( attributes . bold ) {
@@ -157,6 +168,49 @@ export function createAttributedString(text: string, attributes: Record<string,
157168 return attributedString ;
158169}
159170
171+ /**
172+ * Create Android SpannableString for rich text
173+ */
174+ function createAndroidSpannableString ( text : string , attributes : Record < string , unknown > ) : any {
175+ const spannableString = new android . text . SpannableStringBuilder ( text ) ;
176+
177+ // Bold
178+ if ( attributes . bold ) {
179+ spannableString . setSpan ( new android . text . style . StyleSpan ( android . graphics . Typeface . BOLD ) , 0 , text . length , android . text . Spanned . SPAN_EXCLUSIVE_EXCLUSIVE ) ;
180+ }
181+
182+ // Italic
183+ if ( attributes . italic ) {
184+ spannableString . setSpan ( new android . text . style . StyleSpan ( android . graphics . Typeface . ITALIC ) , 0 , text . length , android . text . Spanned . SPAN_EXCLUSIVE_EXCLUSIVE ) ;
185+ }
186+
187+ // Foreground color
188+ if ( attributes . color ) {
189+ const c = attributes . color as Color ;
190+ const androidColor = android . graphics . Color . argb ( c . a || 255 , c . r || 0 , c . g || 0 , c . b || 0 ) ;
191+ spannableString . setSpan ( new android . text . style . ForegroundColorSpan ( androidColor ) , 0 , text . length , android . text . Spanned . SPAN_EXCLUSIVE_EXCLUSIVE ) ;
192+ }
193+
194+ // Background color
195+ if ( attributes . backgroundColor ) {
196+ const c = attributes . backgroundColor as Color ;
197+ const androidColor = android . graphics . Color . argb ( c . a || 255 , c . r || 0 , c . g || 0 , c . b || 0 ) ;
198+ spannableString . setSpan ( new android . text . style . BackgroundColorSpan ( androidColor ) , 0 , text . length , android . text . Spanned . SPAN_EXCLUSIVE_EXCLUSIVE ) ;
199+ }
200+
201+ // Strikethrough
202+ if ( attributes . strikethrough ) {
203+ spannableString . setSpan ( new android . text . style . StrikethroughSpan ( ) , 0 , text . length , android . text . Spanned . SPAN_EXCLUSIVE_EXCLUSIVE ) ;
204+ }
205+
206+ // Underline
207+ if ( attributes . underline ) {
208+ spannableString . setSpan ( new android . text . style . UnderlineSpan ( ) , 0 , text . length , android . text . Spanned . SPAN_EXCLUSIVE_EXCLUSIVE ) ;
209+ }
210+
211+ return spannableString ;
212+ }
213+
160214/**
161215 * Format font size based on heading level
162216 */
@@ -364,18 +418,29 @@ export function debounce<T extends (...args: any[]) => any>(fn: T, delay: number
364418}
365419
366420/**
367- * Copy text to clipboard (iOS)
421+ * Copy text to clipboard (iOS and Android )
368422 */
369423export function copyToClipboard ( text : string ) : boolean {
370424 if ( global . isIOS ) {
371425 UIPasteboard . generalPasteboard . string = text ;
372426 return true ;
427+ } else if ( global . isAndroid ) {
428+ try {
429+ const context = Utils . android . getApplicationContext ( ) ;
430+ const clipboard = context . getSystemService ( android . content . Context . CLIPBOARD_SERVICE ) ;
431+ const clip = android . content . ClipData . newPlainText ( 'text' , text ) ;
432+ clipboard . setPrimaryClip ( clip ) ;
433+ return true ;
434+ } catch ( e ) {
435+ console . error ( 'Failed to copy to clipboard:' , e ) ;
436+ return false ;
437+ }
373438 }
374439 return false ;
375440}
376441
377442/**
378- * Open URL in Safari
443+ * Open URL in browser (iOS Safari, Android default browser)
379444 */
380445export function openUrl ( url : string ) : boolean {
381446 if ( global . isIOS ) {
@@ -384,6 +449,17 @@ export function openUrl(url: string): boolean {
384449 UIApplication . sharedApplication . openURLOptionsCompletionHandler ( nsUrl , null , null ) ;
385450 return true ;
386451 }
452+ } else if ( global . isAndroid ) {
453+ try {
454+ const context = Utils . android . getApplicationContext ( ) ;
455+ const intent = new android . content . Intent ( android . content . Intent . ACTION_VIEW , android . net . Uri . parse ( url ) ) ;
456+ intent . addFlags ( android . content . Intent . FLAG_ACTIVITY_NEW_TASK ) ;
457+ context . startActivity ( intent ) ;
458+ return true ;
459+ } catch ( e ) {
460+ console . error ( 'Failed to open URL:' , e ) ;
461+ return false ;
462+ }
387463 }
388464 return false ;
389465}
0 commit comments