diff --git a/LICENSE b/LICENSE index 9d6ed2d..b20e04b 100755 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ The MIT License (MIT) -nativescript-yourplugin -Copyright (c) 2016, Your Name +nativescript-audio +Copyright (c) 2016, Brad Martin & Nathan Walker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index e8798a3..3cdec8a 100755 --- a/README.md +++ b/README.md @@ -1,9 +1,17 @@ # NativeScript-Audio -NativeScript plugin to play and record audio files. +NativeScript plugin to play and record audio files for Android and iOS. -*Currently Android only, iOS is in the works.* +Uses the following native classes: -[Android Media Recorder Docs](http://developer.android.com/reference/android/media/MediaRecorder.html) +#### Android + +* [Player](http://developer.android.com/reference/android/media/MediaPlayer.html) +* [Recorder](http://developer.android.com/reference/android/media/MediaRecorder.html) + +#### iOS + +* [Player](https://developer.apple.com/library/ios/documentation/AVFoundation/Reference/AVAudioPlayerClassReference/) +* [Recorder](https://developer.apple.com/library/ios/documentation/AVFoundation/Reference/AVAudioRecorder_ClassReference/) ## Installation `npm install nativescript-audio` @@ -12,41 +20,38 @@ NativeScript plugin to play and record audio files. ![AudioExample](screens/audiosample.gif) - ## API -#### *Recording* - -##### canDeviceRecord() - *Promise* -- retruns: *boolean* - -##### startRecorder( { filename: string, errorCallback?: Function, infoCallback?: Function } ) - *Promise* -- returns: *recorder* (android.media.MediaRecorder) - -##### stopRecorder(recorder: recorder object from startRecorder) - +#### TNSRecorder -##### disposeRecorder(recorder: recorder object from startRecorder) -- *Free up system resources when done with recorder* +Method | Description +-------- | --------- +`TNSRecorder.CAN_RECORD()`: `boolean` | Determine if ready to record. +`start({ filename: string, errorCallback?: Function, infoCallback?: Function })`: `Promise` | Start recording file. +`stop()`: `void` | Stop recording. +`dispose()`: `void` | Free up system resources when done with recorder. +#### TNSPlayer -#### *Playing* +Method | Description +-------- | --------- +`playFromFile( { audioFile: string, completeCallback?: Function, errorCallback?: Function, infoCallback?: Function; } )`: `Promise` | Play from a file. +`playFromUrl( { audioFile: string, completeCallback?: Function, errorCallback?: Function, infoCallback?: Function; } )`: `Promise` | Play from a url. +`pause()`: `void` | Pause playback. +`dispose()`: `void` | Free up resources when done playing audio. +`isAudioPlaying()`: `boolean` | Determine if player is playing. +`getAudioTrackDuration()`: `Promise` | duration of media file assigned to mediaPlayer -##### playFromFile( { audioFile: string, completeCallback?: Function, errorCallback?: Function, infoCallback?: Function; } ) - *Promise* -- returns mediaPlayer (android.media.MediaPlayer) +## Why the TNS prefixed name? -##### playFromUrl( { audioFile: string, completeCallback?: Function, errorCallback?: Function, infoCallback?: Function; } ) - *Promise* -- returns mediaPlayer (android.media.MediaPlayer) +`TNS` stands for **T**elerik **N**ative**S**cript -##### pausePlayer(mediaPlayer) - *Promise* -- return boolean +iOS uses classes prefixed with `NS` (stemming from the [NeXTSTEP](https://en.wikipedia.org/wiki/NeXTSTEP) days of old): +https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSString_Class/ -##### disposePlayer(mediaPlayer) - -- Free up resources when done playing audio with this instance of your mediaPlayer +To avoid confusion with iOS native classes, `TNS` is used instead. -##### isAudioPlaying(mediaPlayer) - *Promise* -- returns boolean +# License -##### getAudioTrackDuration(mediaPlayer) - *Promise* -- returns string - duration of media file assigned to mediaPlayer +[MIT](/LICENSE) diff --git a/audio.android.ts b/audio.android.ts index 5c45163..c14882a 100755 --- a/audio.android.ts +++ b/audio.android.ts @@ -1,324 +1,14 @@ -import {Common} from './audio.common'; -import types = require("utils/types"); -import definition = require("./audio"); -import app = require("application"); -import * as utilsModule from "utils/utils"; -import * as fileSystemModule from "file-system"; -import * as enumsModule from "ui/enums"; - -let MediaPlayer = android.media.MediaPlayer; -let MediaRecorder = android.media.MediaRecorder; - -var utils: typeof utilsModule; -function ensureUtils() { - if (!utils) { - utils = require("utils/utils"); - } -} - -var fs: typeof fileSystemModule; -function ensureFS() { - if (!fs) { - fs = require("file-system"); - } -} - -var enums: typeof enumsModule; -function ensureEnums() { - if (!enums) { - enums = require("ui/enums"); - } -} - -export var playFromFile = function(options: definition.AudioPlayerOptions): Promise { - return new Promise((resolve, reject) => { - try { - var audioPath; - - ensureFS(); - - var fileName = types.isString(options.audioFile) ? options.audioFile.trim() : ""; - if (fileName.indexOf("~/") === 0) { - fileName = fs.path.join(fs.knownFolders.currentApp().path, fileName.replace("~/", "")); - console.log('fileName: ' + fileName); - audioPath = fileName; - } - - var mediaPlayer = new MediaPlayer(); - mediaPlayer.setAudioStreamType(android.media.AudioManager.STREAM_MUSIC); - mediaPlayer.setDataSource(audioPath); - mediaPlayer.prepareAsync(); - - // On Complete - mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener({ - onCompletion: function(mp) { - options.completeCallback(); - } - })); - - // On Error - mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener({ - onError: function(mp: any, what: number, extra: number) { - options.errorCallback(); - } - })); - - // On Info - mediaPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener({ - onInfo: function(mp: any, what: number, extra: number) { - options.infoCallback(); - } - })) - - // On Prepared - mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener({ - onPrepared: function(mp) { - mp.start(); - resolve(mp); - } - })); - - } catch (ex) { - reject(ex); - } - }); -} - -export var playFromUrl = function(options: definition.AudioPlayerOptions): Promise { - return new Promise((resolve, reject) => { - try { - - var mediaPlayer = new MediaPlayer(); - mediaPlayer.setAudioStreamType(android.media.AudioManager.STREAM_MUSIC); - mediaPlayer.setDataSource(options.audioFile); - mediaPlayer.prepareAsync(); - - // On Complete - if (options.completeCallback) { - mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener({ - onCompletion: function(mp) { - options.completeCallback(); - } - })); - } - - // On Error - if (options.errorCallback) { - mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener({ - onError: function(mp: any, what: number, extra: number) { - options.errorCallback(); - } - })); - } - - // On Info - if (options.infoCallback) { - mediaPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener({ - onInfo: function(mp: any, what: number, extra: number) { - options.infoCallback(); - } - })) - } - - // On Prepared - mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener({ - onPrepared: function(mp) { - mp.start(); - resolve(mp); - } - })); - - } catch (ex) { - reject(ex); - } - }); -} - -export var pausePlayer = function(player: any): Promise { - return new Promise((resolve, reject) => { - try { - var isPlaying = player.isPlaying(); - if (isPlaying) { - console.log('PAUSE'); - player.pause(); - resolve(true); - } - resolve(false); - } catch (ex) { - reject(ex); - } - }); -} - -export var disposePlayer = function(player: any): Promise { - return new Promise((resolve, reject) => { - try { - player.release(); - resolve(); - } catch (ex) { - reject(ex); - } - }); -} - -export var isAudioPlaying = function(player: any): boolean { - if (player.isPlaying() === true) { - return true; - } else { - return false; - } -} - -export var getAudioTrackDuration = function(player: any): Promise { - return new Promise((resolve, reject) => { - try { - var duration = player.getDuration(); - resolve(duration.toString()); - } catch (ex) { - reject(ex); - } - }); -} - - -/**** AUDIO RECORDING ****/ - -export var canDeviceRecord = function(): boolean { - var pManager = app.android.context.getPackageManager(); - var canRecord = pManager.hasSystemFeature(android.content.pm.PackageManager.FEATURE_MICROPHONE); - if (canRecord) { - return true; - } else { - return false; - } -} - -export var startRecorder = function(options: definition.AudioRecorderOptions): Promise { - return new Promise((resolve, reject) => { - try { - var recorder = new MediaRecorder(); - recorder.setAudioSource(0); - recorder.setOutputFormat(0); - recorder.setAudioEncoder(0); - // recorder.setOutputFile("/sdcard/example.mp4"); - recorder.setOutputFile(options.filename); - recorder.prepare(); - recorder.start(); - - // Is there any benefit to calling start() before setting listener? - - // On Error - recorder.setOnErrorListener(new MediaRecorder.OnErrorListener({ - onError: function(mr: any, what: number, extra: number) { - options.errorCallback({ msg: what, extra: extra }); - } - })); - - // On Info - recorder.setOnInfoListener(new MediaRecorder.OnInfoListener({ - onInfo: function(mr: any, what: number, extra: number) { - options.infoCallback({ msg: what, extra: extra }); - } - })); - - resolve(recorder); - - } catch (ex) { - reject(ex); - } - }); -} - -export var stopRecorder = function(recorder: any): Promise { - return new Promise((resolve, reject) => { - try { - recorder.stop(); - resolve(); - } catch (ex) { - reject(ex); - } - }); -} - -export var disposeRecorder = function(recorder: any): Promise { - return new Promise((resolve, reject) => { - try { - recorder.release(); - resolve(); - } catch (ex) { - reject(ex); - } - }); -} - - - - - - - - - -// export var playFromResource = function(options: definition.AudioPlayerOptions): Promise { -// return new Promise((resolve, reject) => { -// try { -// var audioPath; - -// ensureUtils(); - -// var res = utils.ad.getApplicationContext().getResources(); -// var packageName = utils.ad.getApplication().getPackageName(); -// var identifier = utils.ad.getApplicationContext().getResources().getIdentifier("in_the_night", "raw", packageName); -// console.log(identifier); -// console.log(packageName); -// console.log(res); -// if (res) { -// var resourcePath = "android.resource://" + packageName + "/raw/" + options.audioFile; -// audioPath = resourcePath; -// } - -// var mediaPlayer = new MediaPlayer(); -// mediaPlayer.setAudioStreamType(android.media.AudioManager.STREAM_MUSIC); -// mediaPlayer.setDataSource(audioPath); -// mediaPlayer.prepareAsync(); - -// // On Complete -// if (options.completeCallback) { -// mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener({ -// onCompletion: function(mp) { -// options.completeCallback(); -// } -// })); -// } - -// // On Error -// if (options.errorCallback) { -// mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener({ -// onError: function(mp: any, what: number, extra: number) { -// options.errorCallback({ msg: what, extra: extra }); -// } -// })); -// } - -// // On Info -// if (options.infoCallback) { -// mediaPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener({ -// onInfo: function(mp: any, what: number, extra: number) { -// options.infoCallback({ msg: what, extra: extra }); -// } -// })) -// } - -// // On Prepared - this resolves and returns the android.media.MediaPlayer; -// mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener({ -// onPrepared: function(mp) { -// mp.start(); -// resolve(mp); -// } -// })); - -// } catch (ex) { -// reject(ex); -// } -// }); -// } \ No newline at end of file +/** + * Option interfaces + */ +export * from './src/options'; + +/** + * Player + */ +export * from './src/android/player'; + +/** + * Recorder + */ +export * from './src/android/recorder'; diff --git a/audio.common.ts b/audio.common.ts deleted file mode 100755 index 9495da6..0000000 --- a/audio.common.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as app from 'application'; -import * as dialogs from 'ui/dialogs'; - -export class Common { - public message: string; - - constructor() { - this.message = Utils.SUCCESS_MSG(); - } -} - -export class Utils { - public static SUCCESS_MSG(): string { - let msg = `Your plugin is working on ${app.android ? 'Android' : 'iOS'}.`; - - setTimeout(() => { - dialogs.alert(`${msg} For real. It's really working :)`).then(() => console.log(`Dialog closed.`)); - }, 2000); - - return msg; - } -} - diff --git a/audio.d.ts b/audio.d.ts deleted file mode 100644 index 36c529a..0000000 --- a/audio.d.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Contains the Audio class. - */ - -declare module "audio" { - import fs = require("file-system"); - - // export class Audio { - // - // } - /** - * Starts playing audio file from local app files. - */ - export function playFromFile(options: AudioPlayerOptions): Promise; - - /** - * Starts playing audio file from res/raw folder - */ - export function playFromResource(options: AudioPlayerOptions): Promise; - - /** - * Starts playing audio file from url - */ - export function playFromUrl(options: AudioPlayerOptions): Promise; - - /** - * Pauses playing audio file. - * @param player The audio player to pause. - */ - export function pausePlayer(player: any): Promise; - - /** - * Releases resources from the audio player. - * @param player The audio player to reset. - */ - export function disposePlayer(player: any): Promise; - - /** - * Check if the audio is actively playing. - * @param player The audio player to check. - */ - export function isAudioPlaying(player: any): Promise; - - /** - * Get the duration of the audio file playing. - * @param player The audio player to check length of current file. - */ - export function getAudioTrackDuration(player: any): Promise; - - /** - * Gets the capability of the device if it can record audio. - */ - export function deviceCanRecord(): boolean; - - /** - * Starts the native audio recording control. - */ - export function startRecorder(options: AudioRecorderOptions): Promise; - - /** - * Stops the native audio recording control. - */ - export function stopRecorder(recorder: any): Promise; - - /** - * Releases resources from the recorder. - * @param recorder The audio player to reset. - */ - export function disposeRecorder(recorder: any): Promise; - - /** - * Provides options for the audio player. - */ - export interface AudioPlayerOptions { - /** - * Gets or sets the audio file url. - */ - audioFile: string; - - /** - * Gets or sets the callback when the currently playing audio file completes. - */ - completeCallback?: Function; - - /** - * Gets or sets the callback when an error occurs with the audio player. - */ - errorCallback?: Function; - - /** - * Gets or sets the callback to be invoked to communicate some info and/or warning about the media or its playback. - */ - infoCallback?: Function; - } - - export interface AudioRecorderOptions { - /** - * Gets or sets the recorded file name. - */ - filename: string; - - /** - * Gets or set the max duration of the recording session. - */ - maxDuration?: number; - - /** - * Gets or sets the callback when an error occurs with the media recorder. - */ - errorCallback?: Function; - - /** - * Gets or sets the callback to be invoked to communicate some info and/or warning about the media or its playback. - */ - infoCallback?: Function; - } - -} \ No newline at end of file diff --git a/audio.ios.ts b/audio.ios.ts index 5216e9e..20134f2 100755 --- a/audio.ios.ts +++ b/audio.ios.ts @@ -1,5 +1,15 @@ -import {Common} from './audio.common'; +/** + * Option interfaces + */ +export * from './src/options'; + +/** + * Player + */ +export * from './src/ios/player'; + +/** + * Recorder + */ +export * from './src/ios/recorder'; -// export class YourPlugin extends Common { -// -// } \ No newline at end of file diff --git a/demo/app/App_Resources/iOS/Info.plist b/demo/app/App_Resources/iOS/Info.plist index 18f333a..3b3fe2b 100755 --- a/demo/app/App_Resources/iOS/Info.plist +++ b/demo/app/App_Resources/iOS/Info.plist @@ -41,5 +41,16 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UIBackgroundModes + + audio + + NSMicrophoneUsageDescription + The Audio Recorder needs to access your Microphone to record. diff --git a/demo/app/app.ts b/demo/app/app.ts new file mode 100644 index 0000000..83ec98c --- /dev/null +++ b/demo/app/app.ts @@ -0,0 +1,4 @@ +import * as application from 'application'; +application.mainModule = "main-page"; +application.cssFile = "./app.css"; +application.start(); \ No newline at end of file diff --git a/demo/app/main-page.ts b/demo/app/main-page.ts index d538449..88beb5c 100755 --- a/demo/app/main-page.ts +++ b/demo/app/main-page.ts @@ -1,290 +1,15 @@ -import observable = require('data/observable'); -// import page = require('ui/page'); -import fs = require('file-system'); -import audioModule = require("nativescript-audio"); -import snackbar = require("nativescript-snackbar"); -import app = require("application"); -import color = require("color"); -import platform = require("platform"); -import types = require("utils/types"); +import * as app from 'application'; +import {Color} from 'color'; +import * as platform from 'platform'; +import {AudioDemo} from "./main-view-model"; -var MediaRecorder = android.media.MediaRecorder; -var MediaPlayer = android.media.MediaPlayer; - -var data = new observable.Observable({ - isPlaying: false -}); - - -var recorder; -var mediaPlayer; -var audioSessionId; -var page; - -var audioUrls = [ - { name: 'Fight Club', pic: '~/pics/canoe_girl.jpeg', url: 'http://www.noiseaddicts.com/samples_1w72b820/2514.mp3' }, - { name: 'To The Bat Cave!!!', pic: '~/pics/bears.jpeg', url: 'http://www.noiseaddicts.com/samples_1w72b820/17.mp3' }, - { name: 'Marlon Brando', pic: '~/pics/northern_lights.jpeg', url: 'http://www.noiseaddicts.com/samples_1w72b820/47.mp3' } -]; - -// Event handler for Page "loaded" event attached in main-page.xml function pageLoaded(args) { - // Get the event sender - page = args.object; - page.bindingContext = data; - - if (app.android && platform.device.sdkVersion >= "21") { - var window = app.android.startActivity.getWindow(); - window.setNavigationBarColor(new color.Color("#C2185B").android); - } + var page = args.object; + page.bindingContext = new AudioDemo(); + + if (app.android && platform.device.sdkVersion >= "21") { + var window = app.android.startActivity.getWindow(); + window.setNavigationBarColor(new Color("#C2185B").android); + } } exports.pageLoaded = pageLoaded; - - -function startRecord(args) { - - var canRecord = audioModule.canDeviceRecord(); - - if (canRecord) { - - var audioFolder = fs.knownFolders.currentApp().getFolder("audio"); - console.log(JSON.stringify(audioFolder)); - - var file = "~/audio/recording.mp3"; - - var recorderOptions = { - - filename: audioFolder.path + "/recording.mp3", - - infoCallback: function() { - console.log(); - }, - - errorCallback: function() { - console.log(); - snackbar.simple('Error recording.'); - } - }; - - data.set("isRecording", true); - audioModule.startRecorder(recorderOptions).then(function(result) { - recorder = result; - }, function(err) { - data.set("isRecording", false); - alert(err); - }); - } else { - alert("This device cannot record audio."); - } -} -exports.startRecord = startRecord - -function stopRecord(args) { - audioModule.disposeRecorder(recorder).then(function() { - data.set("isRecording", false); - snackbar.simple("Recorder stopped"); - }, function(ex) { - console.log(ex); - data.set("isRecording", false); - }); -} -exports.stopRecord = stopRecord; - -function getFile(args) { - try { - var audioFolder = fs.knownFolders.currentApp().getFolder("audio"); - var recordedFile = audioFolder.getFile("recording.mp3"); - console.log(JSON.stringify(recordedFile)); - console.log('recording exists: ' + fs.File.exists(recordedFile.path)); - data.set("recordedAudioFile", recordedFile.path); - } catch (ex) { - console.log(ex); - } -} -exports.getFile = getFile; - - -function playRecordedFile(args) { - - var audioFolder = fs.knownFolders.currentApp().getFolder("audio"); - var recordedFile = audioFolder.getFile("recording.mp3"); - console.log("RECORDED FILE : " + JSON.stringify(recordedFile)); - - var playerOptions = { - audioFile: "~/audio/recording.mp3", - - completeCallback: function() { - snackbar.simple("Audio file complete"); - data.set("isPlaying", false); - audioModule.disposePlayer(mediaPlayer).then(function() { - console.log('DISPOSED'); - }, function(err) { - console.log(err); - }); - }, - - errorCallback: function() { - alert('Error callback'); - data.set("isPlaying", false); - }, - - infoCallback: function() { - alert('Info callback'); - } - }; - - data.set("isPlaying", true); - audioModule.playFromFile(playerOptions).then(function(result) { - console.log(result); - mediaPlayer = result; - }, function(err) { - console.log(err); - data.set("isPlaying", false); - }); - -} -exports.playRecordedFile = playRecordedFile; - - - -/***** AUDIO PLAYER *****/ - -function playAudio(filepath, fileType) { - - try { - var playerOptions = { - audioFile: filepath, - - completeCallback: function() { - snackbar.simple("Audio file complete"); - data.set("isPlaying", false); - audioModule.disposePlayer(mediaPlayer).then(function() { - console.log('DISPOSED'); - }, function(err) { - console.log('ERROR disposePlayer: ' + err); - }); - }, - - errorCallback: function(err) { - snackbar.simple('Error occurred during playback.'); - console.log(err); - data.set("isPlaying", false); - }, - - infoCallback: function(info) { - alert('Info callback: ' + info.msg); - console.log("what: " + info); - } - }; - - data.set("isPlaying", true); - - if (fileType === 'localFile') { - audioModule.playFromFile(playerOptions).then(function(result) { - console.log(result); - mediaPlayer = result; - }, function(err) { - console.log(err); - data.set("isPlaying", false); - }); - } else if (fileType === 'remoteFile') { - audioModule.playFromUrl(playerOptions).then(function(result) { - console.log(result); - mediaPlayer = result; - }, function(err) { - console.log(err); - data.set("isPlaying", false); - }); - } - } catch (ex) { - console.log(ex); - } - -} - - - -///** -// * PLAY RESOURCES FILE -// */ -// function playResFile(args) { -// var filepath = 'in_the_night'; -// if (mediaPlayer) { -// mediaPlayer = null; -// } - -// playAudio(filepath, 'resFile'); - -// } -// exports.playResFile = playResFile; - -/** - * PLAY REMOTE AUDIO FILE - */ -function playRemoteFile(args) { - console.log('playRemoteFile'); - var filepath = 'http://www.noiseaddicts.com/samples_1w72b820/2514.mp3'; - if (mediaPlayer) { - mediaPlayer = null; - } - - playAudio(filepath, 'remoteFile'); - -} -exports.playRemoteFile = playRemoteFile; - -/** - * PLAY LOCAL AUDIO FILE from app folder - */ -function playLocalFile(args) { - var filepath = '~/audio/angel.mp3'; - if (mediaPlayer) { - mediaPlayer = null; - } - - playAudio(filepath, 'localFile'); - -} -exports.playLocalFile = playLocalFile; - - - - - - -/** - * PAUSE PLAYING - */ -function pauseAudio(args) { - audioModule.pausePlayer(mediaPlayer).then(function(result) { - console.log(result); - data.set("isPlaying", false); - }, function(err) { - console.log(err); - data.set("isPlaying", true); - }); -} -exports.pauseAudio = pauseAudio; - - - - - -function stopPlaying(args) { - audioModule.disposePlayer(mediaPlayer).then(function() { - snackbar.simple("Media Player Disposed"); - }, function(err) { - console.log(err); - }); -} -exports.stopPlaying = stopPlaying; - - -/** - * RESUME PLAYING - */ -function resumePlaying(args) { - console.log('START'); - mediaPlayer.start(); -} -exports.resumePlaying = resumePlaying; \ No newline at end of file diff --git a/demo/app/main-page.xml b/demo/app/main-page.xml index a401623..e652c30 100755 --- a/demo/app/main-page.xml +++ b/demo/app/main-page.xml @@ -1,57 +1,47 @@ - + loaded="pageLoaded"> - + - - - - - - - -