UNPKG

purgetss

Version:

A package that simplifies mobile app creation for Titanium developers.

948 lines (802 loc) 32.3 kB
// PurgeTSS v7.11.1 // Created by César Estrada // https://purgetss.com function Animation(args = {}) { const params = { // Global id: args.id, debug: args.debug ?? false, isIOS: Ti.Platform.osname === 'iphone' || Ti.Platform.osname === 'ipad', // Play / Toggle / Sequence open: false, playing: false, delay: args.delay ?? 0, hasTransformation: (args.scale !== undefined || args.rotate !== undefined), // Draggable draggables: [], // Collision Detection collisionViews: [], dragCB: null, dropCB: null, lastHoverTarget: null, lastKnownTarget: null } logger('Create Animation View: ' + params.id) const animationView = Ti.UI.createView({ width: 0, height: 0, touchEnabled: false }) delete args.id handleTransformations(args) animationView.play = (_views, _cb) => { if (params.debug) { console.log('') } logger('`play` method called on: ' + params.id) params.playing ? logger(`$.${params.view.id}: is playing...`) : mainPlayApplyFn(_views, _cb) } animationView.toggle = animationView.play animationView.apply = (_views, _cb) => { if (params.debug) { console.log('') } logger('`apply` method called on: ' + params.id) mainPlayApplyFn(_views, _cb, 'apply') } animationView.draggable = (_views) => { if (params.debug) { console.log('') } logger('`draggable` method called on: ' + params.id) makeViewsDraggable(_views) } animationView.detectCollisions = (views, _dragCB, _dropCB) => { if (params.debug) { console.log('') } logger('`detectCollisions` method called on: ' + params.id) if (_dragCB) params.dragCB = _dragCB if (_dropCB) params.dropCB = _dropCB const arr = Array.isArray(views) ? views : [views] arr.forEach(view => { view._collisionEnabled = true if (!params.collisionViews.includes(view)) { params.collisionViews.push(view) } }) } function resolvePosition(view) { if (view._originTop !== undefined) return { top: view._originTop, left: view._originLeft } if (view.top !== undefined && view.top !== null && view.left !== undefined && view.left !== null) return { top: view.top, left: view.left } const r = view.rect return { top: r.y, left: r.x } } function normalizePosition(view) { const pos = resolvePosition(view) view.applyProperties({ top: pos.top, left: pos.left, right: null, bottom: null }) view._originTop = pos.top view._originLeft = pos.left } animationView.swap = (view1, view2) => { if (params.debug) { console.log('') } logger('`swap` method called on: ' + params.id) // Cancel any pending bounce-back animations ;[view1, view2].forEach(v => { if (v._bouncingBack) { logger(` -> cancelling bounce-back for ${v.id} before swap`) v.applyProperties({ top: v._originTop, left: v._originLeft, transform: Ti.UI.createMatrix2D() }) v._bouncingBack = false } }) // Read REAL positions — rect gives actual rendered position, no stale variables const targetRect = view2.rect const sourceHome = { top: view1._originTop ?? view1.top, left: view1._originLeft ?? view1.left } // Source goes to target's real position, target goes to source's home const dest1Top = targetRect.y const dest1Left = targetRect.x const dest2Top = sourceHome.top const dest2Left = sourceHome.left logger(` -> ${view1.id}: home(${sourceHome.top}, ${sourceHome.left}) → dest(${dest1Top}, ${dest1Left})`) logger(` -> ${view2.id}: rect(${targetRect.y}, ${targetRect.x}) → dest(${dest2Top}, ${dest2Left})`) const maxZ = params.draggables.length view1.zIndex = maxZ + 1 view2.zIndex = maxZ view1.animate({ ...args, left: dest1Left, top: dest1Top, transform: Ti.UI.createMatrix2D() }, () => { view1.applyProperties({ left: dest1Left, top: dest1Top, transform: Ti.UI.createMatrix2D() }) params.draggables.forEach((d, k) => { d.zIndex = k }) }) view2.animate({ ...args, left: dest2Left, top: dest2Top }, () => { view2.applyProperties({ left: dest2Left, top: dest2Top }) }) view1._originTop = dest1Top view1._originLeft = dest1Left view2._originTop = dest2Top view2._originLeft = dest2Left } animationView.sequence = (_views, _cb) => { if (params.debug) { console.log('') } logger('`sequence` method called on: ' + params.id) const views = Array.isArray(_views) ? _views : [_views] const total = views.length params.open = !params.open chooseAnimationBasedOnState('play') function animateNext(index) { if (index >= total) { return } const isLast = index === total - 1 playView(views[index], (e) => { if (isLast && typeof _cb === 'function') { _cb(e) } else { animateNext(index + 1) } }, 'play', index, total) } animateNext(0) } animationView.shake = (view, intensity = 10) => { if (params.debug) { console.log('') } logger('`shake` method called on: ' + params.id) if (!view) { return notFound() } view.applyProperties({ transform: Ti.UI.createMatrix2D().translate(-intensity, 0) }) view.animate({ ...args, transform: Ti.UI.createMatrix2D().translate(intensity, 0), duration: Math.round((args.duration ?? 400) / 6), autoreverse: true, repeat: 3, curve: Ti.UI.ANIMATION_CURVE_EASE_IN_OUT }, () => { view.applyProperties({ transform: Ti.UI.createMatrix2D() }) }) } animationView.pulse = (view, count = 1) => { if (params.debug) { console.log('') } logger('`pulse` method called on: ' + params.id) if (!view) { return notFound() } view.animate({ ...args, transform: args.transform ?? Ti.UI.createMatrix2D().scale(1.2), autoreverse: true, repeat: count, curve: Ti.UI.ANIMATION_CURVE_EASE_IN_OUT }, () => { view.applyProperties({ transform: Ti.UI.createMatrix2D() }) }) } animationView.snapTo = (view, targets) => { if (params.debug) { console.log('') } logger('`snapTo` method called on: ' + params.id) if (!view) { return notFound() } const arr = Array.isArray(targets) ? targets : [targets] const vr = view.rect const vcx = (view._visualLeft ?? view.left) + vr.width / 2 const vcy = (view._visualTop ?? view.top) + vr.height / 2 let closest = null let minDist = Infinity arr.forEach(target => { if (target === view) { return } const tr = target.rect const tcx = tr.x + tr.width / 2 const tcy = tr.y + tr.height / 2 const dist = Math.sqrt((vcx - tcx) ** 2 + (vcy - tcy) ** 2) if (dist < minDist) { minDist = dist closest = target } }) if (!closest) { return null } const cr = closest.rect normalizePosition(closest) const destLeft = closest._originLeft + (cr.width - vr.width) / 2 const destTop = closest._originTop + (cr.height - vr.height) / 2 view.animate({ ...args, left: destLeft, top: destTop, transform: Ti.UI.createMatrix2D() }, () => { view.applyProperties({ left: destLeft, top: destTop, transform: Ti.UI.createMatrix2D() }) }) view._originTop = destTop view._originLeft = destLeft return closest } animationView.reorder = (views, newOrder) => { if (params.debug) { console.log('') } logger('`reorder` method called on: ' + params.id) const arr = Array.isArray(views) ? views : [views] if (newOrder.length !== arr.length) { return } arr.forEach(v => normalizePosition(v)) const positions = arr.map(v => ({ left: v._originLeft, top: v._originTop })) arr.forEach((view, i) => { const dest = positions[newOrder[i]] view.animate({ ...args, left: dest.left, top: dest.top, transform: Ti.UI.createMatrix2D() }, () => { view.applyProperties({ left: dest.left, top: dest.top, transform: Ti.UI.createMatrix2D() }) }) view._originTop = dest.top view._originLeft = dest.left }) } animationView.undraggable = (_views) => { if (params.debug) { console.log('') } logger('`undraggable` method called on: ' + params.id) const arr = Array.isArray(_views) ? _views : [_views] arr.forEach(view => { if (view._dragListeners) { view.removeEventListener('touchstart', view._dragListeners.onTouchStart) view.removeEventListener('touchend', view._dragListeners.onTouchEnd) view.removeEventListener('touchmove', view._dragListeners.onTouchMove) Ti.Gesture.removeEventListener('orientationchange', view._dragListeners.onOrientationChange) delete view._dragListeners } const idx = params.draggables.indexOf(view) if (idx !== -1) { params.draggables.splice(idx, 1) } const cIdx = params.collisionViews.indexOf(view) if (cIdx !== -1) { params.collisionViews.splice(cIdx, 1) } delete view._originTop delete view._originLeft delete view._visualTop delete view._visualLeft delete view._collisionEnabled delete view._bouncingBack }) } animationView.open = (_views, _cb) => { if (params.debug) { console.log('') } logger('`open` method called on: ' + params.id) mainPlayApplyFn(_views, _cb, 'play', 'open') } animationView.close = (_views, _cb) => { if (params.debug) { console.log('') } logger('`close` method called on: ' + params.id) mainPlayApplyFn(_views, _cb, 'play', 'close') } // Mac Catalyst note: Parent containers of transitioned views should use fixed // dimensions (not Ti.UI.FILL). Resizable containers trigger a UIKit re-layout // that distorts views with rotated Matrix2D transforms. animationView.transition = (views, layouts) => { if (params.debug) { console.log('') } logger('`transition` method called on: ' + params.id) const arr = Array.isArray(views) ? views : [views] arr.forEach((view, i) => { const layout = layouts[i] if (!layout) { view.animate(Ti.UI.createAnimation({ ...args, zIndex: 0, opacity: 0 }), () => { if (params.isIOS) { // Preserve transform so next fade-in continues from last visual state view.applyProperties({ zIndex: 0, opacity: 0, touchEnabled: false }) } else { // Android: reset transform to avoid animator glitches on next fade-in view.applyProperties({ zIndex: 0, opacity: 0, touchEnabled: false, transform: Ti.UI.createMatrix2D(), translation: { x: 0, y: 0 }, rotate: 0, scale: 1 }) } }) return } const tx = layout.translation?.x ?? 0 const ty = layout.translation?.y ?? 0 const rotate = layout.rotate ?? 0 const scale = layout.scale ?? 1 const transform = Ti.UI.createMatrix2D().translate(tx, ty).rotate(rotate).scale(scale) const needsFadeIn = view.opacity === 0 if (layout.zIndex !== undefined) view.zIndex = layout.zIndex const animation = Ti.UI.createAnimation({ ...args, transform }) if (needsFadeIn) animation.opacity = 1 if (layout.width !== undefined) animation.width = layout.width if (layout.height !== undefined) animation.height = layout.height if (layout.opacity !== undefined) animation.opacity = layout.opacity view.animate(animation, () => { const props = { transform, translation: { x: tx, y: ty }, rotate, scale, zIndex: layout.zIndex, touchEnabled: true } if (needsFadeIn) props.opacity = 1 view.applyProperties(props) }) }) } function handleTransformations(args) { if ('anchorPoint' in args || 'rotate' in args || 'scale' in args) { logger(' -> Creating transform') args.transform = Ti.UI.createMatrix2D(args) delete args.scale delete args.rotate delete args.anchorPoint } if (args.animationProperties) { if ('open' in args.animationProperties) { handleTransformOnOpenClose(args, 'open') } if ('close' in args.animationProperties) { handleTransformOnOpenClose(args, 'close') } } } function handleTransformOnOpenClose(args, state) { const properties = args.animationProperties[state] if ('anchorPoint' in properties || 'rotate' in properties || 'scale' in properties) { logger(` -> Creating transformOn${capitalize(state)}`) args[`transformOn${capitalize(state)}`] = Ti.UI.createMatrix2D(properties) } } function mainPlayApplyFn(_views, _cb, action = 'play', state) { logger(' -> `mainPlayApplyFn` helper') if (state) { params.open = state === 'open' } else { params.open = !params.open } chooseAnimationBasedOnState(action) const applyOrPlayView = (view, index = 0, total = 1) => { action === 'play' ? playView(view, _cb, action, index, total) : applyView(view, _cb, action, index, total) } if (Array.isArray(_views)) { args.delay = params.delay _views.forEach((view, index) => { applyOrPlayView(view, index, _views.length) args.delay += params.delay }) } else { applyOrPlayView(_views, 0, 1) } } function makeViewsDraggable(_views) { if (Array.isArray(_views)) { _views.forEach((view, key) => { view.zIndex = key makeDraggable(view) }) } else { makeDraggable(_views) } } function makeDraggable(draggableView) { if (draggableView) { logger(' -> `draggable` helper') let offsetX, offsetY setBounds(draggableView, args.bounds) const onTouchStart = (event) => { offsetX = event.x offsetY = event.y draggableView._wasDragged = false // If a bounce-back animation is still running, complete it immediately if (draggableView._bouncingBack) { logger(` -> completing pending bounce-back for ${draggableView.id}`) draggableView.applyProperties({ top: draggableView._originTop, left: draggableView._originLeft, transform: Ti.UI.createMatrix2D() }) draggableView._bouncingBack = false } updateDraggableZIndex(event) checkDraggable(draggableView, 'drag') if (draggableView._collisionEnabled) { if (draggableView._originTop === undefined) { draggableView._originTop = draggableView.top draggableView._originLeft = draggableView.left logger(` -> touchStart origin capture (first): ${draggableView.id} top=${draggableView.top} left=${draggableView.left}`) } else { logger(` -> touchStart origin kept: ${draggableView.id} origin=(${draggableView._originTop}, ${draggableView._originLeft}) vs top/left=(${draggableView.top}, ${draggableView.left})`) } } } const onTouchEnd = () => { checkDraggable(draggableView, 'drop') // Android: consolidate transform after drag so transition reads correct state if (!params.isIOS && draggableView.translation) { const { translation, rotate, scale } = draggableView draggableView.applyProperties({ translation, rotate, scale, transform: Ti.UI.createMatrix2D().translate(translation.x, translation.y).rotate(rotate ?? 0).scale(scale ?? 1) }) } if (draggableView._collisionEnabled) { const directTarget = checkCollision(draggableView) const target = directTarget ?? params.lastKnownTarget logger(` -> collision check: ${draggableView.id} | direct: ${directTarget?.id ?? 'null'} | lastKnown: ${params.lastKnownTarget?.id ?? 'null'} | final: ${target?.id ?? 'null'}`) if (target) { if (args.animationProperties?.snap?.center) { logger(` -> snap-center: ${draggableView.id} to ${target.id}`) animationView.snapTo(draggableView, [target]) } if (params.dropCB) { logger(` -> dropCB: ${draggableView.id} on ${target.id}`) params.dropCB(draggableView, target) } } else if (!target && args.animationProperties?.snap?.back) { logger(` -> bounce-back: ${draggableView.id} to (${draggableView._originTop}, ${draggableView._originLeft})`) draggableView._bouncingBack = true draggableView.animate({ ...args, top: draggableView._originTop, left: draggableView._originLeft, transform: Ti.UI.createMatrix2D() }, () => { draggableView._bouncingBack = false draggableView.applyProperties({ top: draggableView._originTop, left: draggableView._originLeft, transform: Ti.UI.createMatrix2D() }) logger(` -> bounce-back complete: ${draggableView.id}`) }) } params.lastHoverTarget = null params.lastKnownTarget = null if (params.dragCB) { params.dragCB(draggableView, null) } } } const onTouchMove = (event) => handleTouchMove(event, draggableView, offsetX, offsetY) const onOrientationChange = () => checkBoundaries(draggableView) draggableView._dragListeners = { onTouchStart, onTouchEnd, onTouchMove, onOrientationChange } draggableView.addEventListener('touchend', onTouchEnd) draggableView.addEventListener('touchmove', onTouchMove) draggableView.addEventListener('touchstart', onTouchStart) Ti.Gesture.addEventListener('orientationchange', onOrientationChange) params.draggables.push(draggableView) } else { notFound() } } function handleTouchMove(event, draggableView, offsetX, offsetY) { draggableView._wasDragged = true const convertedPoint = draggableView.convertPointToView({ x: event.x, y: event.y }, draggableView.parent) let top = Math.round(convertedPoint.y - offsetY) let left = Math.round(convertedPoint.x - offsetX) const parentViewRect = draggableView.parent.rect if (draggableView.bounds) { if (draggableView.bounds.top !== undefined && top < draggableView.bounds.top) { top = draggableView.bounds.top } if (draggableView.bounds.left !== undefined && left < draggableView.bounds.left) { left = draggableView.bounds.left } if (draggableView.bounds.right !== undefined && left > parentViewRect.width - draggableView.rect.width - draggableView.bounds.right) { left = parentViewRect.width - draggableView.rect.width - draggableView.bounds.right } if (draggableView.bounds.bottom !== undefined && top > parentViewRect.height - draggableView.rect.height - draggableView.bounds.bottom) { top = parentViewRect.height - draggableView.rect.height - draggableView.bounds.bottom } } draggableView._visualTop = top draggableView._visualLeft = left if (params.isIOS) { const r = draggableView.rotate ?? 0 const s = draggableView.scale ?? 1 if (r !== 0 || s !== 1) { // Delta-based drag for transformed views (preserves rotate/scale) const translation = draggableView.translation ?? { x: 0, y: 0 } const deltaX = Math.round((event.x - offsetX) * s) const deltaY = Math.round((event.y - offsetY) * s) translation.x += deltaX translation.y += deltaY draggableView.applyProperties({ translation, rotate: r, scale: s, transform: Ti.UI.createMatrix2D().translate(translation.x, translation.y).rotate(r).scale(s) }) } else { // Standard drag for non-transformed views const { x, y } = calculateTranslation(draggableView, draggableView.parent.rect, left, top) draggableView.applyProperties({ duration: 0, transform: Ti.UI.createMatrix2D().translate(x, y) }) } } else { const s = draggableView.scale ?? 1 const r = draggableView.rotate ?? 0 if (r !== 0 || s !== 1) { // Delta-based drag for transformed views on Android const translation = draggableView.translation ?? { x: 0, y: 0 } const deltaX = Math.round((event.x - offsetX) * s) const deltaY = Math.round((event.y - offsetY) * s) translation.x += deltaX translation.y += deltaY draggableView.animate(Ti.UI.createAnimation({ duration: 0, transform: Ti.UI.createMatrix2D().translate(translation.x, translation.y).rotate(r).scale(s) }), () => { draggableView.applyProperties({ translation: translation, rotate: r, scale: s }) }) } else { // Transform-based (mirror of iOS Rama B) — keeps top/left untouched so snapTo/transition stay coherent const { x, y } = calculateTranslation(draggableView, draggableView.parent.rect, left, top) draggableView.animate(Ti.UI.createAnimation({ duration: 0, transform: Ti.UI.createMatrix2D().translate(x, y) }), () => { draggableView.applyProperties({ translation: { x, y }, rotate: 0, scale: 1 }) }) } } if (draggableView._collisionEnabled && params.dragCB) { try { const target = checkCollision(draggableView) if (target) { params.lastKnownTarget = target } if (target !== params.lastHoverTarget) { params.lastHoverTarget = target params.dragCB(draggableView, target) } } catch (e) { logger(` !! collision error in touchmove: ${e.message}`) } } } function calculateTranslation(draggableView, parentViewRect, left, top) { let x = left - parentViewRect.width / 2 + draggableView.rect.width / 2 let y = top - parentViewRect.height / 2 + draggableView.rect.height / 2 if (draggableView.left != null) { x = left - draggableView.left } else if (draggableView.right != null) { x = left - (parentViewRect.width - draggableView.right - draggableView.rect.width) } if (draggableView.top != null) { y = top - draggableView.top } else if (draggableView.bottom != null) { y = top - (parentViewRect.height - draggableView.bottom - draggableView.rect.height) } if (draggableView.constraint === 'vertical') { x = 0 } else if (draggableView.constraint === 'horizontal') { y = 0 } return { x, y } } function setBounds(view, bounds) { if (bounds) { view.bounds = view.bounds ? { ...bounds, ...view.bounds } : bounds } } function updateDraggableZIndex(event) { if (args.animationProperties?.keepZIndex) return const source = realSourceView(event.source) const idx = params.draggables.indexOf(source) if (idx !== -1) { params.draggables.push(params.draggables.splice(idx, 1)[0]) params.draggables.forEach((draggable, key) => { draggable.zIndex = key }) } } function realSourceView(_source) { return params.draggables.map(a => a.id).includes(_source.id) ? _source : realSourceView(_source.parent) } function chooseAnimationBasedOnState(action) { if (args.animationProperties) { args = params.open ? { ...args, ...args.animationProperties.open } : { ...args, ...args.animationProperties.close } if (action === 'play') { logger(` -> '${action}' Check Animation`) args.transform = params.open ? args.transformOnOpen : args.transformOnClose params.open = args.autoreverse ? !params.open : params.open } } } function checkBoundaries(_view) { const bounds = _view.bounds const parentRect = _view.parent.rect if (bounds) { if (bounds.right !== undefined && _view.left > parentRect.width - _view.rect.width - bounds.right) { _view.left = parentRect.width - _view.rect.width - bounds.right } if (bounds.bottom !== undefined && _view.top > parentRect.height - _view.rect.height - bounds.bottom) { _view.top = parentRect.height - _view.rect.height - bounds.bottom } } } function checkComplete(view, action) { if (args.animationProperties?.complete) { logger(' -> `complete` Animation') if (action === 'play') { params.playing = true view.animate(Ti.UI.createAnimation({ ...args, ...args.animationProperties.complete, transform: Ti.UI.createMatrix2D(args.animationProperties.complete) }), () => { params.playing = false }) } else { view.applyProperties(args.animationProperties.complete) } } } function logger(_message, forceLog = false) { if (params.debug || forceLog) { console.warn(`::ti.animation:: ${_message}`) } } function notFound() { console.error('The provided target can’t be found!') } function createAnimationObject(_child, type) { return Ti.UI.createAnimation({ ...(args.animationProperties?.children ?? {}), ..._child.animationProperties?.child ?? {}, ..._child.animationProperties?.[type] ?? {}, transform: Ti.UI.createMatrix2D(_child.animationProperties?.[type] ?? {}) }) } function checkCollision(draggableView) { const top = draggableView._visualTop const left = draggableView._visualLeft if (left === null || left === undefined || top === null || top === undefined) { return null } const cy = top + draggableView.rect.height / 2 const cx = left + draggableView.rect.width / 2 for (const other of params.collisionViews) { if (other === draggableView) { continue } const r = other.rect if (cx > r.x && cx < r.x + r.width && cy > r.y && cy < r.y + r.height) { return other } } return null } function checkDraggable(_view, _action) { logger('Check Draggable') logger(' -> `' + _action + '`') const draggingType = _view.draggingType ?? args.draggingType const argsActions = args.draggable ? args.draggable[_action] : null const draggableActions = _view.draggable ? _view.draggable[_action] : null const theArgs = draggableActions ? { ...argsActions, ...draggableActions } : argsActions if (theArgs) { if (draggingType === 'apply') { _view.applyProperties(theArgs) } else { _view.animate(Ti.UI.createAnimation(theArgs)) } } } function playView(view, _cb, action, index = 0, total = 1) { if (view) { logger(' -> `animate` View') params.view = view params.playing = true const animation = Ti.UI.createAnimation(args) const onComplete = (event) => { checkComplete(view, action) params.playing = false if (typeof _cb === 'function') { const enrichedEvent = { // Safe properties from the original event type: event.type, bubbles: event.bubbles, cancelBubble: event.cancelBubble, // Added properties (primitives only) action, // 'play' state: params.open ? 'open' : 'close', id: params.id, targetId: view.id || 'unknown', // View ID only, not the object index, total, // Helper method to get the view getTarget: () => view } _cb(enrichedEvent) } else { logger('Animation complete on object: ' + JSON.stringify(args)) } } animation.addEventListener('complete', onComplete) view.animate(animation) innerAnimations(view, action) } else { notFound() } } function applyView(view, _cb, action, index = 0, total = 1) { if (view) { logger(' -> `apply` View') view.applyProperties(args) innerAnimations(view, action) checkComplete(view, action) params.playing = false if (typeof _cb === 'function') { const enrichedEvent = { type: 'applied', bubbles: false, cancelBubble: false, // Added properties (primitives only) action, // 'apply' state: params.open ? 'open' : 'close', id: params.id, targetId: view.id || 'unknown', // View ID only, not the object index, total, // Helper method to get the view getTarget: () => view } _cb(enrichedEvent) } } else { notFound() } } function innerAnimations(_view, _action) { _view.children.forEach((child) => { const state = params.open ? 'open' : 'close' if (child.animationProperties?.[state]) { if (_action === 'play') { child.animate(createAnimationObject(child, state), () => { if (child.animationProperties.complete) { child.animate(createAnimationObject(child, 'complete')) } }) } else { child.applyProperties({ transform: Ti.UI.createMatrix2D(child.animationProperties[state]), ...child.animationProperties[state] }) } } }) } function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1) } return animationView } exports.AnimationProperties = Animation function deviceInfo() { console.warn('------------------- DEVICE INFO -------------------') const platform = Ti.Platform const displayCaps = platform.displayCaps // Classic-compatible detection (no Alloy dependency) const isTablet = platform.osname === 'ipad' const isHandheld = ['iphone', 'android'].includes(platform.osname) && !isTablet console.warn(` version: ${platform.version} name: ${platform.name} isTablet: ${isTablet} isHandheld: ${isHandheld} model: ${platform.model} locale: ${platform.locale} osname: ${platform.osname} ostype: ${platform.ostype} username: ${platform.username} manufacturer: ${platform.manufacturer} architecture: ${platform.architecture} availableMemory: ${platform.availableMemory} Ti.Platform.displayCaps.dpi: ${displayCaps.dpi} Ti.Platform.displayCaps.density: ${displayCaps.density} Ti.Platform.displayCaps.platformWidth: ${displayCaps.platformWidth} Ti.Platform.displayCaps.platformHeight: ${displayCaps.platformHeight}`) if (platform.osname === 'android') { console.warn(` Ti.Platform.displayCaps.xdpi: ${displayCaps.xdpi} Ti.Platform.displayCaps.ydpi: ${displayCaps.ydpi}`) } if (['iphone', 'ipad', 'android'].includes(platform.osname)) { console.warn(` Ti.Platform.displayCaps.logicalDensityFactor: ${displayCaps.logicalDensityFactor}`) } console.warn('') console.warn('----------------- END DEVICE INFO -----------------') } exports.deviceInfo = deviceInfo function saveComponent({ source, directory = Ti.Filesystem.tempDirectory }) { const componentImage = source.toImage() const md5 = `${Ti.Utils.md5HexDigest(componentImage)}.png` Ti.Filesystem.getFile(directory, md5).write(componentImage) Ti.Media.saveToPhotoGallery(componentImage) } exports.saveComponent = saveComponent exports.createAnimation = (args) => new Animation(args) // --- Appearance Management (Light/Dark/System) --- function Appearance() { const PROP_KEY = 'userInterfaceStyle' const STYLES = { dark: Ti.UI.USER_INTERFACE_STYLE_DARK, light: Ti.UI.USER_INTERFACE_STYLE_LIGHT, system: Ti.UI.USER_INTERFACE_STYLE_UNSPECIFIED } let currentMode = 'system' function applyMode(mode) { currentMode = mode Ti.UI.overrideUserInterfaceStyle = STYLES[mode] Ti.App.Properties.setInt(PROP_KEY, STYLES[mode]) } return { init() { const saved = Ti.App.Properties.getInt(PROP_KEY, STYLES.system) currentMode = Object.keys(STYLES).find(key => STYLES[key] === saved) || 'system' Ti.UI.overrideUserInterfaceStyle = saved }, set(mode) { if (!STYLES.hasOwnProperty(mode)) return applyMode(mode) }, get() { return currentMode }, toggle() { const next = (currentMode === 'dark') ? 'light' : 'dark' applyMode(next) } } } exports.Appearance = Appearance()