From 1420a5947ad8321be7221b2d66d724b7336937a0 Mon Sep 17 00:00:00 2001 From: Chris Garrity Date: Thu, 6 Aug 2020 07:36:48 -0400 Subject: [PATCH 1/3] Split iOS and Android interfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor the native interface in preparation for switching iOS to use WKWebview. * Finally rename the folder for device specific interfaces as `tablet` instead of `iPad` * Update `import` statements to use the new name * Create new `iOS.js` and `Android.js` based on previous `iPad/iOS.js` to separate the interfaces * Add new `OS.js` class to manage the class variables, initialize the device interface delegate methods to the correct interface. * refactor how `utils/lib` detects the current platform based on `navigtor.userAgent` based on https://stackoverflow.com/questions/37591279/detect-if-user-is-using-webview-for-android-ios-or-a-regular-browser. previous method relied on the Android interface being loaded or not. It can be difficult to detect the difference between in a browser and in a webview, but for now ScratchJr doesn’t need to worry about running in a browser --- .../scratchjr/android/ScratchJrActivity.java | 2 +- .../Default-Landscape@2x~ipad.png | Bin 61695 -> 80764 bytes .../ios-resources/Default-Landscape~ipad.png | Bin 17556 -> 25852 bytes ios/ScratchJr.xcodeproj/project.pbxproj | 2 +- ios/ScratchJr/src/IO.m | 2 +- ios/ScratchJr/src/ViewController.m | 6 +- src/editor/ScratchJr.js | 54 +-- src/editor/blocks/BlockSpecs.js | 2 +- src/editor/engine/Page.js | 14 +- src/editor/engine/Sprite.js | 20 +- src/editor/ui/Library.js | 14 +- src/editor/ui/Palette.js | 6 +- src/editor/ui/Project.js | 12 +- src/editor/ui/Record.js | 24 +- src/editor/ui/Thumbs.js | 6 +- src/editor/ui/UI.js | 23 +- src/entry/app.js | 15 +- src/entry/editor.js | 8 +- src/entry/home.js | 6 +- src/entry/index.js | 12 +- src/iPad/iOS.js | 375 ------------------ src/lobby/Home.js | 20 +- src/lobby/Lobby.js | 6 +- src/lobby/Samples.js | 8 +- src/painteditor/Camera.js | 12 +- src/painteditor/Paint.js | 26 +- src/tablet/Android.js | 291 ++++++++++++++ src/{iPad => tablet}/IO.js | 44 +- src/{iPad => tablet}/MediaLib.js | 0 src/tablet/OS.js | 287 ++++++++++++++ src/tablet/iOS.js | 301 ++++++++++++++ src/utils/Localization.js | 2 +- src/utils/ScratchAudio.js | 4 +- src/utils/Sound.js | 6 +- src/utils/lib.js | 17 +- 35 files changed, 1074 insertions(+), 553 deletions(-) delete mode 100644 src/iPad/iOS.js create mode 100644 src/tablet/Android.js rename src/{iPad => tablet}/IO.js (95%) rename src/{iPad => tablet}/MediaLib.js (100%) create mode 100644 src/tablet/OS.js create mode 100644 src/tablet/iOS.js diff --git a/android/ScratchJr/app/src/main/java/org/scratchjr/android/ScratchJrActivity.java b/android/ScratchJr/app/src/main/java/org/scratchjr/android/ScratchJrActivity.java index c47a469..a58b89f 100644 --- a/android/ScratchJr/app/src/main/java/org/scratchjr/android/ScratchJrActivity.java +++ b/android/ScratchJr/app/src/main/java/org/scratchjr/android/ScratchJrActivity.java @@ -337,7 +337,7 @@ public class ScratchJrActivity } // We send the project Base64-encoded to JavaScript where it's processed and unpacked String base64Project = Base64.encodeToString(projectData.toByteArray(), Base64.DEFAULT); - runJavaScript("iOS.loadProjectFromSjr('" + base64Project + "');"); + runJavaScript("OS.loadProjectFromSjr('" + base64Project + "');"); } public RelativeLayout getContainer() { diff --git a/editions/free/ios-resources/Default-Landscape@2x~ipad.png b/editions/free/ios-resources/Default-Landscape@2x~ipad.png index ed492020706c0b0255a618d9f36acec74b22f3aa..34d2de1163860fd1898b2fc848b6ad9636b1675b 100644 GIT binary patch literal 80764 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 literal 61695 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 ast=KeEqQysQv?B@qencPD-NCi>Hh(sQcL9k literal 17556 zcmeHO-EZ4e6u)V+sZFWW(G@17AcpC5k;sGJ?fc6G2h@okKP+?pe!FJu3zu##$WE!(RdW0tLt66 z!an@FvphbhBGlDu{JGHkmk(i@52aK_%fyefIl1T+^76ds)r)0JjS#1^xLFi6fzpeG zlFI6%v`N9@w$aVdlu4qU9i=nzX(}cwA{F!wd3|))Ls1l`FTex>1DIk!T`FmUK2TC0sZX*u4=<`YMJj8OT%wG5g?YK6 zjncHisQIbOsYweKr;^%G3oFPNBaGkcV_LyPUAhO%7`Z1bpKEGWt|$dlr+khP3t~|$ zX)4C~Te8EpZ31mV>O#5B%rs(MH_nipofTENP%%u9E=gKAz?lj;yDf{Z(=3gbfOjgHp^wlSo;0Zm#WKwf4G#u=p+GQr z1TXV56;(6-f9doDj{)gKl%Gr|Q)7H$Dlr>>8@XYygJi9b1?SZ$p>TZi~d;1TYZPBPCoGN z>g1Ms`KwFcoZnvf_UAu;yLNl))cWmL+1-vFDzbuHZSHzw^}Sl<+^HK&M_>49C(Ql( z{I&02w+0QW-Cq3y3a#H=ezdI#G8?~ia@&9J+<8voiRp}y%@tR6EWgK~4(d7RL+15m9iQEIAL^%j?Mi;I z;-d7_MZ4}b&mnYzf1I?2t1aqkkKFqB$pdzLpAT)maonzF6^nFpCQaM%{aKVbaLH~$ zHx}uOk2V~3I#EMc=z!fI9ayC5vGkf94+aH;TH66uC~SG+AcHd$&QR+Hgi8r-dBRBo zw+Z}E>#hJl6dZfvxdxt;;L+2%E8x)+p5=*91_VbS+GO1o5Wa)(9TELpg?K0gn49YX zBK8omCleNs0)d=Tb3H(k4ziwPjuWz;kVbBO-xq0B#f1*b{CvxJ}?Tf!kD1 zUBYWM@LG*^G=kd%ZWFjo^$I{p(m|508Hk4@9lVWfU6cRM*Soh4|8VE?<*%*qvUw9k z#&7=q$Ii(cOQE*c-rDdqcZAvatN)h1dw2VvWmHw@3+F#Q97OmtF_z#zc{*GD587~9 A9{>OV diff --git a/ios/ScratchJr.xcodeproj/project.pbxproj b/ios/ScratchJr.xcodeproj/project.pbxproj index 5c156f1..3c9959a 100644 --- a/ios/ScratchJr.xcodeproj/project.pbxproj +++ b/ios/ScratchJr.xcodeproj/project.pbxproj @@ -419,7 +419,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "EDITION=free;\n\n../bin/bundle-compile.sh;\n\nrsync -pvtrlL --cvs-exclude \\\n ../editions/$EDITION/ios-resources/* \\\n \"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/\";\n\nrsync -pvtrlL --cvs-exclude \\\n ../editions/$EDITION/src/* \\\n \"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/HTML5\";\n \nmkdir -p \"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/HTML5/pnglibrary\";\n\n../bin/convert-svg-to-png.py -i \"../editions/$EDITION/src/svglibrary/\" -o \"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/HTML5/pnglibrary\";"; + shellScript = "EDITION=free;\n\n../bin/bundle-compile.sh;\n\nrsync -pvtrlL --cvs-exclude \\\n ../editions/$EDITION/ios-resources/* \\\n \"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/\";\n\nrsync -pvtrlL --cvs-exclude \\\n ../editions/$EDITION/src/* \\\n \"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/HTML5\";\n \nmkdir -p \"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/HTML5/pnglibrary\";\n\n../bin/convert-svg-to-png.py -i \"../editions/$EDITION/src/svglibrary/\" -o \"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/HTML5/pnglibrary\";\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/ios/ScratchJr/src/IO.m b/ios/ScratchJr/src/IO.m index f2eea80..3324e53 100644 --- a/ios/ScratchJr/src/IO.m +++ b/ios/ScratchJr/src/IO.m @@ -282,7 +282,7 @@ NSMutableDictionary *soundtimers; + (void)soundEnded:(NSTimer*)timer { NSString *soundName = [[timer userInfo] objectForKey:@"soundName"]; if (sounds[soundName] == nil) return; - NSString *callback = [NSString stringWithFormat:@"iOS.soundDone('%@');", soundName]; + NSString *callback = [NSString stringWithFormat:@"OS.soundDone('%@');", soundName]; UIWebView *webview = [ViewController webview]; dispatch_async(dispatch_get_main_queue(), ^{ [webview stringByEvaluatingJavaScriptFromString:callback]; diff --git a/ios/ScratchJr/src/ViewController.m b/ios/ScratchJr/src/ViewController.m index 0827e9f..5571b85 100644 --- a/ios/ScratchJr/src/ViewController.m +++ b/ios/ScratchJr/src/ViewController.m @@ -164,7 +164,7 @@ JSContext *js; NSLog(@"could not load the website caused by error DESC: %@", error); NSDictionary *userInfo = [error userInfo]; NSString *desc = [NSString stringWithFormat:@"%@", ([userInfo objectForKey: @"NSLocalizedDescription"] == NULL)? [error localizedDescription]: [userInfo objectForKey: @"NSLocalizedDescription"]]; - NSString *callback = [NSString stringWithFormat: @"iOS.pageError('%@');",desc]; + NSString *callback = [NSString stringWithFormat: @"OS.pageError('%@');",desc]; UIWebView *webview = [ViewController webview]; dispatch_async(dispatch_get_main_queue(), ^{ [webview stringByEvaluatingJavaScriptFromString: callback]; @@ -172,7 +172,7 @@ JSContext *js; } - (void) receiveProject:(NSString *)project{ - NSString *callback = [NSString stringWithFormat:@"iOS.loadProjectFromSjr('%@');", project]; + NSString *callback = [NSString stringWithFormat:@"OS.loadProjectFromSjr('%@');", project]; UIWebView *webview = [ViewController webview]; dispatch_async(dispatch_get_main_queue(), ^{ NSString *res = [webview stringByEvaluatingJavaScriptFromString:callback]; @@ -310,7 +310,7 @@ JSContext *js; return [ScratchJr captureimage:onCameraCaptureComplete]; } -//iOS.sendSjrToShareDialog = function(fileName, emailSubject, emailBody, shareType, b64data) { +//OS.sendSjrToShareDialog = function(fileName, emailSubject, emailBody, shareType, b64data) { -(NSString*) sendSjrUsingShareDialog:(NSString*) fileName :(NSString*) emailSubject :(NSString*) emailBody :(int) shareType :(NSString*) b64data { return [IO sendSjrUsingShareDialog:fileName :emailSubject :emailBody :shareType : b64data]; diff --git a/src/editor/ScratchJr.js b/src/editor/ScratchJr.js index 52c2cf8..d5ee5ea 100644 --- a/src/editor/ScratchJr.js +++ b/src/editor/ScratchJr.js @@ -6,8 +6,8 @@ import Undo from './ui/Undo'; import Alert from './ui/Alert'; import Palette from './ui/Palette'; import Record from './ui/Record'; -import IO from '../iPad/IO'; -import iOS from '../iPad/iOS'; +import IO from '../tablet/IO'; +import OS from '../tablet/OS'; import UI from './ui/UI'; import Menu from './blocks/Menu'; import Library from './ui/Library'; @@ -183,7 +183,7 @@ export default class ScratchJr { document.body.scrollTop = 0; time = (new Date()) - 0; var urlvars = getUrlVars(); - iOS.hascamera(); + OS.hascamera(); ScratchJr.log('starting the app'); BlockSpecs.initBlocks(); Project.loadIcon = document.createElement('img'); @@ -345,7 +345,7 @@ export default class ScratchJr { static saveProject (e, onDone) { if (ScratchJr.isEditable() && editmode == 'storyStarter' && storyStarted && !Project.error) { - iOS.analyticsEvent('samples', 'story_starter_edited', Project.metadata.name); + OS.analyticsEvent('samples', 'story_starter_edited', Project.metadata.name); // Localize sample project names var sampleName = Localization.localize('SAMPLE_' + Project.metadata.name); // Get the new project name @@ -381,14 +381,14 @@ export default class ScratchJr { ScratchJr.stopStripsFromTop(e); ScratchJr.unfocus(e); ScratchJr.saveProject(e, ScratchJr.flippage); - iOS.analyticsEvent('editor', 'project_editor_close'); + OS.analyticsEvent('editor', 'project_editor_close'); } static flippage () { Alert.close(); - iOS.cleanassets('wav', doNext); + OS.cleanassets('wav', doNext); function doNext () { - iOS.cleanassets('svg', ScratchJr.switchPage); + OS.cleanassets('svg', ScratchJr.switchPage); } } @@ -525,7 +525,7 @@ export default class ScratchJr { ScratchJr.displayStatus('none'); inFullscreen = true; UI.enterFullScreen(); - iOS.analyticsEvent('editor', 'full_screen_entered'); + OS.analyticsEvent('editor', 'full_screen_entered'); document.body.style.background = 'black'; } @@ -537,7 +537,7 @@ export default class ScratchJr { inFullscreen = false; UI.quitFullScreen(); onBackButtonCallback.pop(); - iOS.analyticsEvent('editor', 'full_screen_exited'); + OS.analyticsEvent('editor', 'full_screen_exited'); document.body.style.background = 'white'; } @@ -906,24 +906,24 @@ export default class ScratchJr { //Application on the background - // XXX: does this ever happen? - // I'm pretty sure this is dead code -TM - static saveProjectState () { - ScratchAudio.sndFX('tap.wav'); - if (frame.style.display == 'none') { - Paint.saveEditState(ScratchJr.stopServer); - } else { - ScratchJr.unfocus(); - ScratchJr.stopStrips(); - if (ScratchJr.isEditable() && currentProject && !Project.error && changed) { - Project.save(currentProject, ScratchJr.stopServer); - } - } - } - - static stopServer () { - iOS.stopserver(iOS.trace); - } + // // XXX: does this ever happen? + // // I'm pretty sure this is dead code -TM + // static saveProjectState () { + // ScratchAudio.sndFX('tap.wav'); + // if (frame.style.display == 'none') { + // Paint.saveEditState(ScratchJr.stopServer); + // } else { + // ScratchJr.unfocus(); + // ScratchJr.stopStrips(); + // if (ScratchJr.isEditable() && currentProject && !Project.error && changed) { + // Project.save(currentProject, ScratchJr.stopServer); + // } + // } + // } + // + // static stopServer () { + // OS.stopserver(OS.trace); + // } /** * The functions that are invokved when the Android back button is clicked. diff --git a/src/editor/blocks/BlockSpecs.js b/src/editor/blocks/BlockSpecs.js index 40f6f5a..14e4db6 100755 --- a/src/editor/blocks/BlockSpecs.js +++ b/src/editor/blocks/BlockSpecs.js @@ -1,5 +1,5 @@ import Localization from '../../utils/Localization'; -import IO from '../../iPad/IO'; +import IO from '../../tablet/IO'; let loadCount = 0; diff --git a/src/editor/engine/Page.js b/src/editor/engine/Page.js index e92e064..f1d9045 100644 --- a/src/editor/engine/Page.js +++ b/src/editor/engine/Page.js @@ -5,9 +5,9 @@ import UI from '../ui/UI'; import Sprite from './Sprite'; import Palette from '../ui/Palette'; import BlockSpecs from '../blocks/BlockSpecs'; -import iOS from '../../iPad/iOS'; -import IO from '../../iPad/IO'; -import MediaLib from '../../iPad/MediaLib'; +import OS from '../../tablet/OS'; +import IO from '../../tablet/IO'; +import MediaLib from '../../tablet/MediaLib'; import Undo from '../ui/Undo'; import Matrix from '../../geom/Matrix'; import Vector from '../../geom/Vector'; @@ -125,7 +125,9 @@ export default class Page { return; } var me = this; - var url = (MediaLib.keys[name]) ? MediaLib.path + name : (name.indexOf('/') < 0) ? iOS.path + name : name; + var url = (MediaLib.keys[name]) ? + MediaLib.path + name : + (name.indexOf('/') < 0) ? OS.path + name : name; var md5 = (MediaLib.keys[name]) ? MediaLib.path + name : name; if (md5.substr(md5.length - 3) == 'png') { @@ -137,7 +139,7 @@ export default class Page { if (md5.indexOf('/') > -1) { IO.requestFromServer(md5, doNext); } else { - iOS.getmedia(md5, nextStep); + OS.getmedia(md5, nextStep); } function nextStep (base64) { doNext(atob(base64)); @@ -145,7 +147,7 @@ export default class Page { function doNext (str) { str = str.replace(/>\s*<'); me.setSVG(str); - if ((str.indexOf('xlink:href') < 0) && iOS.path) { + if ((str.indexOf('xlink:href') < 0) && OS.path) { me.setBackgroundImage(url, fcn); // does not have embedded images } else { var base64 = IO.getImageDataURL(me.md5, btoa(str)); diff --git a/src/editor/engine/Sprite.js b/src/editor/engine/Sprite.js index c0949d3..75b2312 100755 --- a/src/editor/engine/Sprite.js +++ b/src/editor/engine/Sprite.js @@ -12,9 +12,9 @@ import Project from '../ui/Project'; import Thumbs from '../ui/Thumbs'; import UI from '../ui/UI'; import BlockSpecs from '../blocks/BlockSpecs'; -import iOS from '../../iPad/iOS'; -import IO from '../../iPad/IO'; -import MediaLib from '../../iPad/MediaLib'; +import IO from '../../tablet/IO'; +import OS from '../../tablet/OS'; +import MediaLib from '../../tablet/MediaLib'; import Undo from '../ui/Undo'; import ScriptsPane from '../ui/ScriptsPane'; import SVG2Canvas from '../../utils/SVG2Canvas'; @@ -77,12 +77,14 @@ export default class Sprite { getAsset (whenDone) { var md5 = this.md5; var spr = this; - var url = (MediaLib.keys[md5]) ? MediaLib.path + md5 : (md5.indexOf('/') < 0) ? iOS.path + md5 : md5; + var url = (MediaLib.keys[md5]) ? + MediaLib.path + md5 : + (md5.indexOf('/') < 0) ? OS.path + md5 : md5; md5 = (MediaLib.keys[md5]) ? MediaLib.path + md5 : md5; if (md5.indexOf('/') > -1) { IO.requestFromServer(md5, doNext); } else { - iOS.getmedia(md5, nextStep); + OS.getmedia(md5, nextStep); } function nextStep (base64) { doNext(atob(base64)); @@ -90,7 +92,7 @@ export default class Sprite { function doNext (str) { str = str.replace(/>\s*<'); spr.setSVG(str); - if ((str.indexOf('xlink:href') < 0) && iOS.path) { + if ((str.indexOf('xlink:href') < 0) && OS.path) { whenDone(url); // does not have embedded images } else { var base64 = IO.getImageDataURL(spr.md5, btoa(str)); @@ -716,7 +718,7 @@ export default class Sprite { var sprites = JSON.parse(page.sprites); sprites.push(this.id); page.sprites = JSON.stringify(sprites); - iOS.analyticsEvent('editor', 'text_sprite_create'); + OS.analyticsEvent('editor', 'text_sprite_create'); if ((this.str == '') && !whenDone) { this.setTextBox(); this.activateInput(); @@ -806,7 +808,7 @@ export default class Sprite { document.body.scrollLeft = 0; var form = document.forms.activetextbox; var changed = (this.oldvalue != form.typing.value); - iOS.analyticsEvent('editor', 'text_sprite_close'); + OS.analyticsEvent('editor', 'text_sprite_close'); if (this.noChars(form.typing.value)) { this.deleteText(this.oldvalue != ''); } else { @@ -891,7 +893,7 @@ export default class Sprite { var ti = document.forms.activetextbox.typing; gn('textbox').style.visibility = 'visible'; var me = this; - iOS.analyticsEvent('editor', 'text_sprite_open'); + OS.analyticsEvent('editor', 'text_sprite_open'); ti.onblur = function () { me.unfocusText(); }; diff --git a/src/editor/ui/Library.js b/src/editor/ui/Library.js index 96ce963..76cde19 100644 --- a/src/editor/ui/Library.js +++ b/src/editor/ui/Library.js @@ -1,8 +1,8 @@ import ScratchJr from '../ScratchJr'; -import iOS from '../../iPad/iOS'; -import IO from '../../iPad/IO'; -import MediaLib from '../../iPad/MediaLib'; +import OS from '../../tablet/OS'; +import IO from '../../tablet/IO'; +import MediaLib from '../../tablet/MediaLib'; import Paint from '../../painteditor/Paint'; import Events from '../../utils/Events'; import Localization from '../../utils/Localization'; @@ -406,12 +406,12 @@ export default class Library { // (this is possible if we receive a duplicate project, for example) Library.assetThumbnailUnique(data.altmd5, type, function (isUnique) { if (isUnique) { - iOS.remove(data.altmd5, iOS.trace); + OS.remove(data.altmd5, OS.trace); } }); } - IO.deleteobject(key, data.id, iOS.trace); + IO.deleteobject(key, data.id, OS.trace); } static parseAssetData (data) { @@ -525,7 +525,7 @@ export default class Library { if (!(selectedOne in MediaLib.keys)) { analyticsName = 'user_asset'; } - iOS.analyticsEvent('editor', 'new_character', analyticsName); + OS.analyticsEvent('editor', 'new_character', analyticsName); } Library.close(e); } @@ -542,7 +542,7 @@ export default class Library { if (!(selectedOne in MediaLib.keys)) { analyticsName = 'user_background'; } - iOS.analyticsEvent('editor', 'choose_background', analyticsName); + OS.analyticsEvent('editor', 'choose_background', analyticsName); } Library.close(e); } diff --git a/src/editor/ui/Palette.js b/src/editor/ui/Palette.js index a22dc3b..6f0ec7a 100644 --- a/src/editor/ui/Palette.js +++ b/src/editor/ui/Palette.js @@ -7,8 +7,8 @@ import Block from '../blocks/Block'; import BlockSpecs from '../blocks/BlockSpecs'; import ScriptsPane from './ScriptsPane'; import Undo from './Undo'; -import iOS from '../../iPad/iOS'; -import MediaLib from '../../iPad/MediaLib'; +import OS from '../../tablet/OS'; +import MediaLib from '../../tablet/MediaLib'; import Events from '../../utils/Events'; import Rectangle from '../../geom/Rectangle'; import DrawPath from '../../utils/DrawPath'; @@ -582,7 +582,7 @@ export default class Palette { e.preventDefault(); switch (Palette.getLandingPlace(element, e)) { case 'scripts': - iOS.analyticsEvent('editor', 'new_block_' + element.owner.blocktype); + OS.analyticsEvent('editor', 'new_block_' + element.owner.blocktype); var sc = ScratchJr.getActiveScript(); var dx = localx(sc, element.left); var dy = localy(sc, element.top); diff --git a/src/editor/ui/Project.js b/src/editor/ui/Project.js index 8229dc5..3ad7a9a 100644 --- a/src/editor/ui/Project.js +++ b/src/editor/ui/Project.js @@ -5,8 +5,8 @@ import Palette from './Palette'; import UI from './UI'; import Page from '../engine/Page'; import Sprite from '../engine/Sprite'; -import iOS from '../../iPad/iOS'; -import IO from '../../iPad/IO'; +import OS from '../../tablet/OS'; +import IO from '../../tablet/IO'; import Paint from '../../painteditor/Paint'; import SVG2Canvas from '../../utils/SVG2Canvas'; import {frame, gn, newHTML, scaleMultiplier, getIdFor, @@ -417,7 +417,7 @@ export default class Project { json.cond = 'deleted = ? AND id != ? AND gallery IS NULL'; json.items = ['name', 'thumbnail', 'id']; json.values = ['NO', projectID]; - IO.query(iOS.database, json, function (result) { + IO.query(OS.database, json, function (result) { var pdata = JSON.parse(result); var isUnique = true; for (var p = 0; p < pdata.length; p++) { @@ -444,7 +444,7 @@ export default class Project { if (thumb.md5.indexOf('samples/') < 0) { // In case we've exited story-starter mode Project.thumbnailUnique(thumb.md5, id, function (isUnique) { if (isUnique) { - iOS.remove(thumb.md5, iOS.trace); // remove thumb; + OS.remove(thumb.md5, OS.trace); // remove thumb; } }); } @@ -454,14 +454,14 @@ export default class Project { Project.getThumbnailPNG(ScratchJr.stage.pages[0], 192, 144, getMD5); function getMD5 (dataurl) { var pngBase64 = dataurl.split(',')[1]; - iOS.getmd5(pngBase64, function (str) { + OS.getmd5(pngBase64, function (str) { savePNG(str, pngBase64); }); } function savePNG (md5, pngBase64) { var filename = ScratchJr.currentProject + '_' + md5; - iOS.setmedianame(pngBase64, filename, 'png', doNext); + OS.setmedianame(pngBase64, filename, 'png', doNext); } function doNext (md5) { diff --git a/src/editor/ui/Record.js b/src/editor/ui/Record.js index 47bb8e6..a9dc43f 100644 --- a/src/editor/ui/Record.js +++ b/src/editor/ui/Record.js @@ -1,7 +1,7 @@ import ScratchJr from '../ScratchJr'; import Palette from './Palette'; import Undo from './Undo'; -import iOS from '../../iPad/iOS'; +import OS from '../../tablet/OS'; import ScratchAudio from '../../utils/ScratchAudio'; import {frame, gn, newHTML, isTablet, isAndroid, setProps} from '../../utils/lib'; @@ -60,7 +60,7 @@ export default class Record { // Dialog box hide/show static appear () { - iOS.analyticsEvent('editor', 'record_dialog_open'); + OS.analyticsEvent('editor', 'record_dialog_open'); gn('backdrop').setAttribute('class', 'modal-backdrop fade in'); setProps(gn('backdrop').style, { display: 'block' @@ -72,7 +72,7 @@ export default class Record { } static disappear () { - iOS.analyticsEvent('editor', 'record_dialog_close'); + OS.analyticsEvent('editor', 'record_dialog_close'); setTimeout(function () { gn('backdrop').setAttribute('class', 'modal-backdrop fade'); setProps(gn('backdrop').style, { @@ -148,13 +148,13 @@ export default class Record { if (isRecording) { Record.stopRecording(); // Stop if we're already recording } else { - iOS.sndrecord(Record.startRecording); // Start a recording + OS.sndrecord(Record.startRecording); // Start a recording } } } static startRecording (filename) { - iOS.analyticsEvent('editor', 'start_recording'); + OS.analyticsEvent('editor', 'start_recording'); if (parseInt(filename) < 0) { // Error in getting record filename - go back to editor recordedSound = undefined; @@ -169,7 +169,7 @@ export default class Record { Record.soundname = filename; Record.toggleButtonUI('record', true); var poll = function () { - iOS.volume(Record.updateVolume, Record.recordError); + OS.volume(Record.updateVolume, Record.recordError); }; interval = setInterval(poll, 33); timeLimit = setTimeout(function () { @@ -202,7 +202,7 @@ export default class Record { // Start playing the sound and switch UI appropriately static startPlaying () { - iOS.startplay(Record.timeOutPlay); + OS.startplay(Record.timeOutPlay); Record.toggleButtonUI('play', true); isPlaying = true; } @@ -244,7 +244,7 @@ export default class Record { // Stop playing the sound and switch UI appropriately static stopPlayingSound (fcn) { - iOS.stopplay(fcn); + OS.stopplay(fcn); Record.toggleButtonUI('play', false); isPlaying = false; window.clearTimeout(playTimeLimit); @@ -253,7 +253,7 @@ export default class Record { // Stop the volume monitor and recording static stopRecording (fcn) { - iOS.analyticsEvent('editor', 'stop_recording'); + OS.analyticsEvent('editor', 'stop_recording'); if (timeLimit != null) { clearTimeout(timeLimit); timeLimit = null; @@ -272,7 +272,7 @@ export default class Record { static volumeCheckStopped (fcn) { isRecording = false; Record.recordUIoff(); - iOS.recordstop(fcn); + OS.recordstop(fcn); } // Press OK (check) @@ -293,12 +293,12 @@ export default class Record { } static closeContinueSave () { - iOS.recorddisappear('YES', Record.registerProjectSound); + OS.recorddisappear('YES', Record.registerProjectSound); } static closeContinueRemove () { // don't get the sound - proceed right to tearDown - iOS.recorddisappear('NO', Record.tearDownRecorder); + OS.recorddisappear('NO', Record.tearDownRecorder); } static registerProjectSound () { diff --git a/src/editor/ui/Thumbs.js b/src/editor/ui/Thumbs.js index 8f07bc9..1e6d1d2 100644 --- a/src/editor/ui/Thumbs.js +++ b/src/editor/ui/Thumbs.js @@ -8,7 +8,7 @@ import Page from '../engine/Page'; import ScriptsPane from './ScriptsPane'; import Undo from './Undo'; import UI from './UI'; -import iOS from '../../iPad/iOS'; +import OS from '../../tablet/OS'; import Events from '../../utils/Events'; import ScratchAudio from '../../utils/ScratchAudio'; import {frame, gn, localx, newHTML, scaleMultiplier, getIdFor, @@ -83,7 +83,7 @@ export default class Thumbs { var tb = Thumbs.getType(Thumbs.t, 'pagethumb'); if (ScratchJr.shaking && (e.target.className == 'deletethumb')) { ScratchJr.clearSelection(); - iOS.analyticsEvent('editor', 'delete_scene'); + OS.analyticsEvent('editor', 'delete_scene'); ScratchJr.stage.deletePage(tb.owner); return; } @@ -378,7 +378,7 @@ export default class Thumbs { sc.owner.deactivate(); } ScratchJr.unfocus(e); - iOS.analyticsEvent('editor', 'add_scene'); + OS.analyticsEvent('editor', 'add_scene'); new Page(getIdFor('page')); } diff --git a/src/editor/ui/UI.js b/src/editor/ui/UI.js index 8bd8f1e..f9a7761 100644 --- a/src/editor/ui/UI.js +++ b/src/editor/ui/UI.js @@ -13,15 +13,16 @@ import Stage from '../engine/Stage'; import ScriptsPane from './ScriptsPane'; import Undo from './Undo'; import Library from './Library'; -import iOS from '../../iPad/iOS'; -import IO from '../../iPad/IO'; -import MediaLib from '../../iPad/MediaLib'; +import OS from '../../tablet/OS'; +import IO from '../../tablet/IO'; +import MediaLib from '../../tablet/MediaLib'; import Paint from '../../painteditor/Paint'; import Events from '../../utils/Events'; import Localization from '../../utils/Localization'; import ScratchAudio from '../../utils/ScratchAudio'; -import {frame, gn, CSSTransition, localx, newHTML, scaleMultiplier, fullscreenScaleMultiplier, getIdFor, isTablet, newDiv, - newTextInput, isAndroid, getDocumentWidth, getDocumentHeight, setProps, globalx} from '../../utils/lib'; +import {frame, gn, CSSTransition, localx, newHTML, scaleMultiplier, fullscreenScaleMultiplier, + getIdFor, isTablet, newDiv, newTextInput, isAndroid, getDocumentWidth, getDocumentHeight, + setProps, globalx} from '../../utils/lib'; let projectNameTextInput = null; let info = null; @@ -156,7 +157,7 @@ export default class UI { }; } - iOS.deviceName(function (name) { + OS.deviceName(function (name) { gn('deviceName').textContent = name; }); @@ -257,7 +258,7 @@ export default class UI { setTimeout(saveAndShare, 500); // 500ms delay to wait for loading GIF to show and keyboard to hide - iOS.analyticsEvent('editor', 'share_button', (shareType == EMAILSHARE) ? 'email' : 'airdrop'); + OS.analyticsEvent('editor', 'share_button', (shareType == EMAILSHARE) ? 'email' : 'airdrop'); function saveAndShare () { // Save the project's new name @@ -276,7 +277,7 @@ export default class UI { var emailSubject = Localization.localize('SHARING_EMAIL_SUBJECT', { PROJECT_NAME: IO.shareName }); - iOS.sendSjrToShareDialog(IO.zipFileName, emailSubject, Localization.localize('SHARING_EMAIL_TEXT'), + OS.sendSjrToShareDialog(IO.zipFileName, emailSubject, Localization.localize('SHARING_EMAIL_TEXT'), shareType, contents); shareLoadingGif.style.visibility = 'hidden'; @@ -332,7 +333,7 @@ export default class UI { static handleTextFieldSave (dontHide) { // Handle story-starter mode project if (ScratchJr.isEditable() && ScratchJr.editmode == 'storyStarter' && !Project.error) { - iOS.analyticsEvent('samples', 'story_starter_edited', Project.metadata.name); + OS.analyticsEvent('samples', 'story_starter_edited', Project.metadata.name); // Get the new project name var sampleName = Localization.localize('SAMPLE_' + Project.metadata.name); IO.uniqueProjectName({ @@ -366,7 +367,7 @@ export default class UI { } Project.metadata.name = pname; ScratchJr.changed = true; - iOS.setfield(iOS.database, Project.metadata.id, 'name', pname); + OS.setfield(OS.database, Project.metadata.id, 'name', pname); if (!dontHide) { ScratchAudio.sndFX('exittap.wav'); gn('infobox').className = 'infobox fade'; @@ -757,7 +758,7 @@ export default class UI { static switchGrid () { ScratchAudio.sndFX('tap.wav'); UI.setShowGrid(Grid.hidden); - iOS.analyticsEvent('editor', Grid.hidden ? 'hide_grid' : 'show_grid'); + OS.analyticsEvent('editor', Grid.hidden ? 'hide_grid' : 'show_grid'); } static setShowGrid (b) { diff --git a/src/entry/app.js b/src/entry/app.js index bcd31bf..136b7d6 100644 --- a/src/entry/app.js +++ b/src/entry/app.js @@ -1,9 +1,9 @@ import {preprocessAndLoadCss} from '../utils/lib'; import Localization from '../utils/Localization'; import AppUsage from '../utils/AppUsage'; -import iOS from '../iPad/iOS'; -import IO from '../iPad/IO'; -import MediaLib from '../iPad/MediaLib'; +import OS from '../tablet/OS'; +import IO from '../tablet/IO'; +import MediaLib from '../tablet/MediaLib'; import {indexMain} from './index'; import {homeMain} from './home'; @@ -11,7 +11,6 @@ import {editorMain} from './editor'; import {gettingStartedMain} from './gettingstarted'; import {inappInterfaceGuide, inappAbout, inappBlocksGuide, inappPaintEditorGuide} from './inapp'; - function loadSettings (settingsRoot, whenDone) { IO.requestFromServer(settingsRoot + 'settings.json', (result) => { window.Settings = JSON.parse(result); @@ -42,7 +41,7 @@ window.onload = () => { preprocessAndLoadCss('css', 'css/thumbs.css'); /* For parental gate. These CSS properties should be refactored */ preprocessAndLoadCss('css', 'css/editor.css'); - entryFunction = () => iOS.waitForInterface(indexMain); + entryFunction = () => OS.waitForInterface(indexMain); break; case 'home': // Lobby pages @@ -50,7 +49,7 @@ window.onload = () => { preprocessAndLoadCss('css', 'css/base.css'); preprocessAndLoadCss('css', 'css/lobby.css'); preprocessAndLoadCss('css', 'css/thumbs.css'); - entryFunction = () => iOS.waitForInterface(homeMain); + entryFunction = () => OS.waitForInterface(homeMain); break; case 'editor': // Editor pages @@ -62,14 +61,14 @@ window.onload = () => { preprocessAndLoadCss('css', 'css/editormodal.css'); preprocessAndLoadCss('css', 'css/librarymodal.css'); preprocessAndLoadCss('css', 'css/paintlook.css'); - entryFunction = () => iOS.waitForInterface(editorMain); + entryFunction = () => OS.waitForInterface(editorMain); break; case 'gettingStarted': // Getting started video page preprocessAndLoadCss('css', 'css/font.css'); preprocessAndLoadCss('css', 'css/base.css'); preprocessAndLoadCss('css', 'css/gs.css'); - entryFunction = () => iOS.waitForInterface(gettingStartedMain); + entryFunction = () => OS.waitForInterface(gettingStartedMain); break; case 'inappAbout': // About ScratchJr in-app help frame diff --git a/src/entry/editor.js b/src/entry/editor.js index 74d6925..76434b1 100644 --- a/src/entry/editor.js +++ b/src/entry/editor.js @@ -1,14 +1,14 @@ import ScratchJr from '../editor/ScratchJr'; -import iOS from '../iPad/iOS'; +import OS from '../tablet/OS'; import Camera from '../painteditor/Camera'; import Record from '../editor/ui/Record'; export function editorMain () { - iOS.getsettings(doNext); - iOS.analyticsEvent('editor', 'project_editor_open'); + OS.getsettings(doNext); + OS.analyticsEvent('editor', 'project_editor_open'); function doNext (str) { var list = str.split(','); - iOS.path = list[1] == '0' ? list[0] + '/' : undefined; + OS.path = list[1] == '0' ? list[0] + '/' : undefined; if (list.length > 2) { Record.available = list[2] == 'YES' ? true : false; } diff --git a/src/entry/home.js b/src/entry/home.js index aac133c..3f49588 100644 --- a/src/entry/home.js +++ b/src/entry/home.js @@ -1,15 +1,15 @@ import {gn} from '../utils/lib'; import Localization from '../utils/Localization'; -import iOS from '../iPad/iOS'; +import OS from '../tablet/OS'; import Lobby from '../lobby/Lobby'; export function homeMain () { gn('logotab').ontouchend = homeGoBack; homeStrings(); - iOS.getsettings(doNext); + OS.getsettings(doNext); function doNext (str) { var list = str.split(','); - iOS.path = list[1] == '0' ? list[0] + '/' : undefined; + OS.path = list[1] == '0' ? list[0] + '/' : undefined; Lobby.appinit(window.Settings.scratchJrVersion); } } diff --git a/src/entry/index.js b/src/entry/index.js index 27415d5..baa849c 100644 --- a/src/entry/index.js +++ b/src/entry/index.js @@ -1,6 +1,6 @@ import ScratchAudio from '../utils/ScratchAudio'; import {gn, getUrlVars, isAndroid, isiOS} from '../utils/lib'; -import iOS from '../iPad/iOS'; +import OS from '../tablet/OS'; import UI from '../editor/ui/UI'; import Localization from '../utils/Localization'; import AppUsage from '../utils/AppUsage'; @@ -49,9 +49,9 @@ function indexFirstTime () { gn('blueguy').className = 'blue show'; gn('redguy').className = 'red show'; } - iOS.askpermission(); // ask for sound recording + OS.askpermission(); // ask for sound recording setTimeout(function () { - iOS.hidesplash(doit); + OS.hidesplash(doit); }, 500); function doit () { window.ontouchend = function () { @@ -94,7 +94,7 @@ function indexLoadStart (afterUsage) { gn('usageOther').className = 'usageOther hide'; gn('usageNoanswer').className = 'usageNoanswer hide'; } - iOS.setAnalyticsPlacePref(AppUsage.currentUsage); + OS.setAnalyticsPlacePref(AppUsage.currentUsage); } gn('gettings').className = 'gettings show'; gn('startcode').className = 'startcode show'; @@ -133,7 +133,7 @@ function indexLoadUsage () { } function indexGohome () { - iOS.setfile('homescroll.sjr', 0, function () { + OS.setfile('homescroll.sjr', 0, function () { doNext(); }); function doNext () { @@ -171,7 +171,7 @@ function indexSetUsage (e) { break; } // Send one-time analytics event about usage - iOS.analyticsEvent('lobby', 'scratchjr_usage', usageText); + OS.analyticsEvent('lobby', 'scratchjr_usage', usageText); AppUsage.setUsage(usageText); ScratchAudio.sndFX('tap.wav'); indexLoadStart(true); diff --git a/src/iPad/iOS.js b/src/iPad/iOS.js deleted file mode 100644 index 8062bfe..0000000 --- a/src/iPad/iOS.js +++ /dev/null @@ -1,375 +0,0 @@ -import {isiOS, gn} from '../utils/lib'; -import IO from './IO'; -import Lobby from '../lobby/Lobby'; -import Alert from '../editor/ui/Alert'; -import ScratchAudio from '../utils/ScratchAudio'; - -////////////////////////////////////////////////// -// Tablet interface functions -////////////////////////////////////////////////// - -// This file and object are named "iOS" for legacy reasons. -// But, it is also used for the AndroidInterface. All function calls here -// are mapped to Android/iOS native calls. - -let path; -let camera; -let database = 'projects'; -let mediacounter = 0; -let tabletInterface = null; - -export default class iOS { - // Getters/setters for properties used in other classes - static get path () { - return path; - } - - static set path (newPath) { - path = newPath; - } - - static get camera () { - return camera; - } - - static get database () { - return database; - } - - // Wait for the tablet interface to be injected into the webview - static waitForInterface (fcn) { - // Already loaded the interface - if (tabletInterface != null) { - fcn(); - return; - } - - // Android device - if (typeof AndroidInterface !== 'undefined') { - tabletInterface = AndroidInterface; - if (fcn) { - fcn(); - } - return; - } - - // iOS device - might not be loaded yet - if (typeof (window.tablet) != 'object') { - // Come back in 100ms - setTimeout(function () { - iOS.waitForInterface(fcn); - }, 100); - } else { - // All set to run commands - tabletInterface = window.tablet; - if (fcn) { - fcn(); - } - } - } - - // Database functions - static stmt (json, fcn) { - var result = tabletInterface.database_stmt(JSON.stringify(json)); - if (typeof (fcn) !== 'undefined') { - fcn(result); - } - } - - static query (json, fcn) { - var result = tabletInterface.database_query(JSON.stringify(json)); - if (typeof (fcn) !== 'undefined') { - fcn(result); - } - } - - static setfield (db, id, fieldname, val, fcn) { - var json = {}; - var keylist = [fieldname + ' = ?', 'mtime = ?']; - json.values = [val, (new Date()).getTime().toString()]; - json.stmt = 'update ' + db + ' set ' + keylist.toString() + ' where id = ' + id; - iOS.stmt(json, fcn); - } - - // IO functions - - static cleanassets (ft, fcn) { - tabletInterface.io_cleanassets(ft); fcn(); - } - - static getmedia (file, fcn) { - mediacounter++; - var nextStep = function (file, key, whenDone) { - var result = tabletInterface.io_getmedialen(file, key); - iOS.processdata(key, 0, result, '', whenDone); - }; - nextStep(file, mediacounter, fcn); - } - - static getmediadata (key, offset, len, fcn) { - var result = tabletInterface.io_getmediadata(key, offset, len); - if (fcn) { - fcn(result); - } - } - - static processdata (key, off, len, oldstr, fcn) { - if (len == 0) { - iOS.getmediadone(key); - fcn(oldstr); - return; - } - var newlen = (len < 100000) ? len : 100000; - iOS.getmediadata(key, off, newlen, function (str) { - iOS.processdata(key, off + newlen, len - newlen, oldstr + str, fcn); - }); - } - - static getsettings (fcn) { - var result = tabletInterface.io_getsettings(); - if (fcn) { - fcn(result); - } - } - - static getmediadone (file, fcn) { - var result = tabletInterface.io_getmediadone(file); - if (fcn) { - fcn(result); - } - } - - static setmedia (str, ext, fcn) { - var result = tabletInterface.io_setmedia(str, ext); - if (fcn) { - fcn(result); - } - } - - static setmedianame (str, name, ext, fcn) { - var result = tabletInterface.io_setmedianame(str, name, ext); - if (fcn) { - fcn(result); - } - } - - static getmd5 (str, fcn) { - var result = tabletInterface.io_getmd5(str); - if (fcn) { - fcn(result); - } - } - - static remove (str, fcn) { - var result = tabletInterface.io_remove(str); - if (fcn) { - fcn(result); - } - } - - static getfile (str, fcn) { - var result = tabletInterface.io_getfile(str); - if (fcn) { - fcn(result); - } - } - - static setfile (name, str, fcn) { - var result = tabletInterface.io_setfile(name, btoa(str)); - if (fcn) { - fcn(result); - } - } - - // Sound functions - - static registerSound (dir, name, fcn) { - var result = tabletInterface.io_registersound(dir, name); - if (fcn) { - fcn(result); - } - } - - static playSound (name, fcn) { - var result = tabletInterface.io_playsound(name); - if (fcn) { - fcn(result); - } - } - - static stopSound (name, fcn) { - var result = tabletInterface.io_stopsound(name); - if (fcn) { - fcn(result); - } - } - - // Web Wiew delegate call backs - - static soundDone (name) { - ScratchAudio.soundDone(name); - } - - static sndrecord (fcn) { - var result = tabletInterface.recordsound_recordstart(); - if (fcn) { - fcn(result); - } - } - - static recordstop (fcn) { - var result = tabletInterface.recordsound_recordstop(); - if (fcn) { - fcn(result); - } - } - - static volume (fcn) { - var result = tabletInterface.recordsound_volume(); - if (fcn) { - fcn(result); - } - } - - static startplay (fcn) { - var result = tabletInterface.recordsound_startplay(); - if (fcn) { - fcn(result); - } - } - - static stopplay (fcn) { - var result = tabletInterface.recordsound_stopplay(); - if (fcn) { - fcn(result); - } - } - - static recorddisappear (b, fcn) { - var result = tabletInterface.recordsound_recordclose(b); - if (fcn) { - fcn(result); - } - } - - // Record state - static askpermission () { - if (isiOS) { - tabletInterface.askForPermission(); - } - } - - // camera functions - - static hascamera () { - camera = tabletInterface.scratchjr_cameracheck(); - } - - static startfeed (data, fcn) { - var str = JSON.stringify(data); - var result = tabletInterface.scratchjr_startfeed(str); - if (fcn) { - fcn(result); - } - } - - static stopfeed (fcn) { - var result = tabletInterface.scratchjr_stopfeed(); - if (fcn) { - fcn(result); - } - } - - static choosecamera (mode, fcn) { - var result = tabletInterface.scratchjr_choosecamera(mode); - if (fcn) { - fcn(result); - } - } - - static captureimage (fcn) { - tabletInterface.scratchjr_captureimage(fcn); - } - - static hidesplash (fcn) { - if (isiOS) { - tabletInterface.hideSplash(); - } - if (fcn) { - fcn(); - } - } - - static trace (str) { - console.log(str); // eslint-disable-line no-console - } - - static parse (str) { - console.log(JSON.parse(str)); // eslint-disable-line no-console - } - - static tracemedia (str) { - console.log(atob(str)); // eslint-disable-line no-console - } - - ignore () { - } - - /////////////// - // Sharing - /////////////// - - - // Called on the JS side to trigger native UI for project sharing. - // fileName: name for the file to share - // emailSubject: subject text to use for an email - // emailBody: body HTML to use for an email - // shareType: 0 for Email; 1 for Airdrop - // b64data: base-64 encoded .SJR file to share - - static sendSjrToShareDialog (fileName, emailSubject, emailBody, shareType, b64data) { - tabletInterface.sendSjrUsingShareDialog(fileName, emailSubject, emailBody, shareType, b64data); - } - - // Called on the Objective-C side. The argument is a base64-encoded .SJR file, - // to be unzipped, processed, and stored. - static loadProjectFromSjr (b64data) { - try { - IO.loadProjectFromSjr(b64data); - } catch (err) { - var errorMessage = 'Couldn\'t load share -- project data corrupted. ' + err.message; - Alert.open(gn('frame'), gn('frame'), errorMessage, '#ff0000'); - console.log(err); // eslint-disable-line no-console - return 0; - } - return 1; - } - - // Name of the device/iPad to display on the sharing dialog page - // fcn is called with the device name as an arg - static deviceName (fcn) { - fcn(tabletInterface.deviceName()); - } - - static analyticsEvent (category, action, label) { - tabletInterface.analyticsEvent(category, action, label); - } - - static setAnalyticsPlacePref (preferredPlace) { - tabletInterface.setAnalyticsPlacePref(preferredPlace); - } - - // Web Wiew delegate call backs - - static pageError (desc) { - console.log('XCODE ERROR:', desc); // eslint-disable-line no-console - if (window.location.href.indexOf('home.html') > -1) { - if (Lobby.errorTimer) { - Lobby.errorLoading(desc); - } - } - } -} - -// Expose iOS methods for ScratchJr tablet sharing callbacks -window.iOS = iOS; diff --git a/src/lobby/Home.js b/src/lobby/Home.js index 161bb2b..6a78204 100755 --- a/src/lobby/Home.js +++ b/src/lobby/Home.js @@ -3,8 +3,8 @@ ////////////////////////////////////////////////// import Lobby from './Lobby'; -import iOS from '../iPad/iOS'; -import IO from '../iPad/IO'; +import OS from '../tablet/OS'; +import IO from '../tablet/IO'; import Project from '../editor/ui/Project'; import Localization from '../utils/Localization'; import ScratchAudio from '../utils/ScratchAudio'; @@ -135,7 +135,7 @@ export default class Home { if (md5 && (md5 == 'newproject')) { Home.createNewProject(); } else if (md5) { - iOS.setfile('homescroll.sjr', gn('wrapc').scrollTop, function () { + OS.setfile('homescroll.sjr', gn('wrapc').scrollTop, function () { doNext(md5); }); } @@ -144,10 +144,10 @@ export default class Home { ScratchAudio.sndFX('cut.wav'); Project.thumbnailUnique(Home.actionTarget.thumb, Home.actionTarget.id, function (isUnique) { if (isUnique) { - iOS.remove(Home.actionTarget.thumb, iOS.trace); + OS.remove(Home.actionTarget.thumb, OS.trace); } }); - iOS.setfield(iOS.database, Home.actionTarget.id, 'deleted', 'YES', Home.removeProjThumb); + OS.setfield(OS.database, Home.actionTarget.id, 'deleted', 'YES', Home.removeProjThumb); break; default: if (Home.actionTarget && (Home.actionTarget.childElementCount > 2)) { @@ -156,13 +156,13 @@ export default class Home { break; } function doNext () { - iOS.analyticsEvent('lobby', 'existing_project_edited'); + OS.analyticsEvent('lobby', 'existing_project_edited'); window.location.href = 'editor.html?pmd5=' + md5 + '&mode=edit'; } } static createNewProject () { - iOS.analyticsEvent('lobby', 'project_created'); + OS.analyticsEvent('lobby', 'project_created'); var obj = {}; // XXX: for localization, the new project name should likely be refactored obj.name = Home.getNextName(Localization.localize('NEW_PROJECT_PREFIX')); @@ -172,7 +172,7 @@ export default class Home { } static gotoEditor (md5) { - iOS.setfile('homescroll.sjr', gn('wrapc').scrollTop, function () { + OS.setfile('homescroll.sjr', gn('wrapc').scrollTop, function () { doNext(md5); }); function doNext (md5) { @@ -230,7 +230,7 @@ export default class Home { ////////////////////////// static displayYourProjects () { - iOS.getfile('homescroll.sjr', gotScrollsState); + OS.getfile('homescroll.sjr', gotScrollsState); function gotScrollsState (str) { var num = Number(atob(str)); scrollvalue = (num.toString() == 'NaN') ? 0 : num; @@ -239,7 +239,7 @@ export default class Home { json.items = ['name', 'thumbnail', 'id', 'isgift']; json.values = ['NO', version]; json.order = 'ctime desc'; - IO.query(iOS.database, json, Home.displayProjects); + IO.query(OS.database, json, Home.displayProjects); } } diff --git a/src/lobby/Lobby.js b/src/lobby/Lobby.js index 680a7c3..eb416e2 100644 --- a/src/lobby/Lobby.js +++ b/src/lobby/Lobby.js @@ -4,7 +4,7 @@ import {libInit, getUrlVars, gn, isAndroid, newHTML} from '../utils/lib'; import ScratchAudio from '../utils/ScratchAudio'; -import iOS from '../iPad/iOS'; +import OS from '../tablet/OS'; import Localization from '../utils/Localization'; import Cookie from '../utils/Cookie'; @@ -98,7 +98,7 @@ export default class Lobby { var doNext = function (page) { Lobby.changePage(page); }; - iOS.setfile('homescroll.sjr', gn('wrapc').scrollTop, function () { + OS.setfile('homescroll.sjr', gn('wrapc').scrollTop, function () { doNext(page); }); } else { @@ -203,7 +203,7 @@ export default class Lobby { ScratchAudio.sndFX('tap.wav'); let newLocale = window.Settings.supportedLocales[e.target.textContent]; Cookie.set('localization', newLocale); - iOS.analyticsEvent('lobby', 'language_changed', newLocale); + OS.analyticsEvent('lobby', 'language_changed', newLocale); window.location = '?place=gear'; }; } diff --git a/src/lobby/Samples.js b/src/lobby/Samples.js index 240032a..82ae417 100644 --- a/src/lobby/Samples.js +++ b/src/lobby/Samples.js @@ -3,9 +3,9 @@ ////////////////////////////////////////////////// import Lobby from './Lobby'; -import IO from '../iPad/IO'; -import iOS from '../iPad/iOS'; -import MediaLib from '../iPad/MediaLib'; +import OS from '../tablet/OS'; +import IO from '../tablet/IO'; +import MediaLib from '../tablet/MediaLib'; import ScratchAudio from '../utils/ScratchAudio'; import Localization from '../utils/Localization'; import {gn, newHTML} from '../utils/lib'; @@ -75,7 +75,7 @@ export default class Samples { e.preventDefault(); e.stopPropagation(); ScratchAudio.sndFX('tap.wav'); - iOS.analyticsEvent('samples', 'sample_opened', mt.textContent); + OS.analyticsEvent('samples', 'sample_opened', mt.textContent); var md5 = mt.md5; window.location.href = 'editor.html?pmd5=' + md5 + '&mode=' + ((window.Settings.useStoryStarters) ? 'storyStarter' : 'look'); diff --git a/src/painteditor/Camera.js b/src/painteditor/Camera.js index 2ca1720..0d4dfbd 100644 --- a/src/painteditor/Camera.js +++ b/src/painteditor/Camera.js @@ -1,5 +1,5 @@ import ScratchJr from '../editor/ScratchJr'; -import iOS from '../iPad/iOS'; +import OS from '../tablet/OS'; import ScratchAudio from '../utils/ScratchAudio'; import Paint from './Paint'; import PaintUndo from './PaintUndo'; @@ -53,7 +53,7 @@ export default class Camera { data.mw = Paint.workspaceWidth; data.mh = Paint.workspaceHeight; data.image = mask.toDataURL('image/png'); - iOS.startfeed(data, iOS.trace); + OS.startfeed(data, OS.trace); Paint.cameraToolsOn(); } @@ -80,7 +80,7 @@ export default class Camera { case 'cameraflip': ScratchAudio.sndFX('tap.wav'); view = (view == 'front') ? 'back' : 'front'; - iOS.choosecamera(view, Camera.flip); + OS.choosecamera(view, Camera.flip); break; case 'camerasnap': Camera.snapShot(); @@ -101,7 +101,7 @@ export default class Camera { target = undefined; view = 'front'; Camera.active = false; - iOS.stopfeed(); + OS.stopfeed(); Paint.cameraToolsOff(); if (isAndroid) { ScratchJr.onBackButtonCallback.pop(); @@ -109,7 +109,7 @@ export default class Camera { } static snapShot () { - iOS.captureimage('Camera.processimage'); // javascript call back; + OS.captureimage('Camera.processimage'); // javascript call back; } static getLayerMask (elem) { @@ -197,5 +197,5 @@ export default class Camera { } } -// Exposing the camera for the tablet callback in iOS.snapShot +// Exposing the camera for the tablet callback in OS.snapShot window.Camera = Camera; diff --git a/src/painteditor/Paint.js b/src/painteditor/Paint.js index 1329bc7..407bf13 100644 --- a/src/painteditor/Paint.js +++ b/src/painteditor/Paint.js @@ -3,9 +3,9 @@ import BlockSpecs from '../editor/blocks/BlockSpecs'; import SVGTools from './SVGTools'; import SVG2Canvas from '../utils/SVG2Canvas'; import Ghost from './Ghost'; -import iOS from '../iPad/iOS'; -import IO from '../iPad/IO'; -import MediaLib from '../iPad/MediaLib'; +import OS from '../tablet/OS'; +import IO from '../tablet/IO'; +import MediaLib from '../tablet/MediaLib'; import Localization from '../utils/Localization'; import Alert from '../editor/ui/Alert'; import PaintAction from './PaintAction'; @@ -169,7 +169,7 @@ export default class Paint { // log two events: // * paint editor is opened // * type of edit (edit_background, edit_character, new_character) - iOS.analyticsEvent('paint_editor', 'paint_editor_open'); + OS.analyticsEvent('paint_editor', 'paint_editor_open'); if (bkg) { action = 'edit_background'; label = (md5 in MediaLib.keys) ? md5 : 'user_background'; @@ -177,7 +177,7 @@ export default class Paint { action = sname ? 'edit_character' : 'new_character'; label = (md5 in MediaLib.keys) ? md5 : 'user_character'; } - iOS.analyticsEvent('paint_editor', action, label); + OS.analyticsEvent('paint_editor', action, label); PaintUndo.buffer = []; PaintUndo.index = 0; maxZoom = 5; @@ -356,7 +356,7 @@ export default class Paint { } static close () { - iOS.analyticsEvent('paint_editor', 'paint_editor_close'); + OS.analyticsEvent('paint_editor', 'paint_editor_close'); saving = true; paintFrame.className = 'paintframe disappear'; frame.style.display = 'block'; @@ -774,7 +774,7 @@ export default class Paint { Paint.addSidePalette(rightpal, 'selectortools', ['select', 'rotate']); Paint.addSidePalette(rightpal, 'edittools', ['stamper', 'scissors']); Paint.addSidePalette(rightpal, 'filltools', - (iOS.camera == '1' && Camera.available) ? ['camera', 'paintbucket'] : ['paintbucket']); + (OS.camera == '1' && Camera.available) ? ['camera', 'paintbucket'] : ['paintbucket']); } static addSidePalette (p, id, list) { @@ -1098,7 +1098,7 @@ export default class Paint { Paint.loadChar(md5); } else if (!MediaLib.keys[md5]) { // Load user asset - iOS.getmedia(md5, nextStep); + OS.getmedia(md5, nextStep); } else { // Load library asset Paint.getBkg(MediaLib.path + md5); @@ -1185,7 +1185,7 @@ export default class Paint { Paint.loadChar(md5); } else if (!MediaLib.keys[md5]) { // Load user asset - iOS.getmedia(md5, nextStep); + OS.getmedia(md5, nextStep); } else { // Load library asset Paint.loadChar(MediaLib.path + md5); @@ -1277,14 +1277,14 @@ export default class Paint { static addToBkgLib (fcn) { var dataurl = IO.getThumbnail(svgdata, 480, 360, 120, 90); var pngBase64 = dataurl.split(',')[1]; - iOS.setmedia(pngBase64, 'png', setBkgRecord); + OS.setmedia(pngBase64, 'png', setBkgRecord); function setBkgRecord (pngmd5) { var json = {}; var keylist = ['md5', 'altmd5', 'version', 'width', 'height', 'ext']; var values = '?,?,?,?,?,?'; json.values = [saveMD5, pngmd5, ScratchJr.version, '480', '360', 'svg']; json.stmt = 'insert into userbkgs (' + keylist.toString() + ') values (' + values + ')'; - iOS.stmt(json, fcn); + OS.stmt(json, fcn); } } @@ -1364,14 +1364,14 @@ export default class Paint { var h = box.height.toString(); var dataurl = IO.getThumbnail(svgdata, w, h, 120, 90); var pngBase64 = dataurl.split(',')[1]; - iOS.setmedia(pngBase64, 'png', setCostumeRecord); + OS.setmedia(pngBase64, 'png', setCostumeRecord); function setCostumeRecord (pngmd5) { var json = {}; var keylist = ['scale', 'md5', 'altmd5', 'version', 'width', 'height', 'ext', 'name']; var values = '?,?,?,?,?,?,?,?'; json.values = [scale, saveMD5, pngmd5, ScratchJr.version, w, h, 'svg', cname]; json.stmt = 'insert into usershapes (' + keylist.toString() + ') values (' + values + ')'; - iOS.stmt(json, fcn); + OS.stmt(json, fcn); } } diff --git a/src/tablet/Android.js b/src/tablet/Android.js new file mode 100644 index 0000000..76e695f --- /dev/null +++ b/src/tablet/Android.js @@ -0,0 +1,291 @@ +////////////////////////////////////////////////// +// Android interface functions +// AndroidInterface will be the class defined for all the native function calls +////////////////////////////////////////////////// + +let mediacounter = 0; + +export default class Android { + // // This will be set up in the OS class + // // Wait for the tablet interface to be injected into the webview + // static waitForInterface (fcn) { + // // Already loaded the interface + // if (typeof AndroidInterface !== 'undefined') { + // fcn(); + // return; + // } + // // interface not yet available, come back in 100ms + // setTimeout(function () { + // Android.waitForInterface(fcn); + // }, 100); + // } + + // Database functions + static stmt (json, fcn) { + var result = AndroidInterface.database_stmt(JSON.stringify(json)); + if (typeof (fcn) !== 'undefined') { + fcn(result); + } + } + + static query (json, fcn) { + var result = AndroidInterface.database_query(JSON.stringify(json)); + if (typeof (fcn) !== 'undefined') { + fcn(result); + } + } + + // IO functions + + static cleanassets (ft, fcn) { + AndroidInterface.io_cleanassets(ft); fcn(); + } + + static getmedia (file, fcn) { + mediacounter++; + var nextStep = function (file, key, whenDone) { + var result = AndroidInterface.io_getmedialen(file, key); + Android.processdata(key, 0, result, '', whenDone); + }; + nextStep(file, mediacounter, fcn); + } + + static getmediadata (key, offset, len, fcn) { + var result = AndroidInterface.io_getmediadata(key, offset, len); + if (fcn) { + fcn(result); + } + } + + static processdata (key, off, len, oldstr, fcn) { + if (len == 0) { + Android.getmediadone(key); + fcn(oldstr); + return; + } + var newlen = (len < 100000) ? len : 100000; + Android.getmediadata(key, off, newlen, function (str) { + Android.processdata(key, off + newlen, len - newlen, oldstr + str, fcn); + }); + } + + static getsettings (fcn) { + var result = AndroidInterface.io_getsettings(); + if (fcn) { + fcn(result); + } + } + + static getmediadone (file, fcn) { + var result = AndroidInterface.io_getmediadone(file); + if (fcn) { + fcn(result); + } + } + + static setmedia (str, ext, fcn) { + var result = AndroidInterface.io_setmedia(str, ext); + if (fcn) { + fcn(result); + } + } + + static setmedianame (str, name, ext, fcn) { + var result = AndroidInterface.io_setmedianame(str, name, ext); + if (fcn) { + fcn(result); + } + } + + static getmd5 (str, fcn) { + var result = AndroidInterface.io_getmd5(str); + if (fcn) { + fcn(result); + } + } + + static remove (str, fcn) { + var result = AndroidInterface.io_remove(str); + if (fcn) { + fcn(result); + } + } + + static getfile (str, fcn) { + var result = AndroidInterface.io_getfile(str); + if (fcn) { + fcn(result); + } + } + + static setfile (name, str, fcn) { + var result = AndroidInterface.io_setfile(name, btoa(str)); + if (fcn) { + fcn(result); + } + } + + // Sound functions + + static registerSound (dir, name, fcn) { + var result = AndroidInterface.io_registersound(dir, name); + if (fcn) { + fcn(result); + } + } + + static playSound (name, fcn) { + var result = AndroidInterface.io_playsound(name); + if (fcn) { + fcn(result); + } + } + + static stopSound (name, fcn) { + var result = AndroidInterface.io_stopsound(name); + if (fcn) { + fcn(result); + } + } + + // Web Wiew delegate call backs + + static sndrecord (fcn) { + var result = AndroidInterface.recordsound_recordstart(); + if (fcn) { + fcn(result); + } + } + + static recordstop (fcn) { + var result = AndroidInterface.recordsound_recordstop(); + if (fcn) { + fcn(result); + } + } + + static volume (fcn) { + var result = AndroidInterface.recordsound_volume(); + if (fcn) { + fcn(result); + } + } + + static startplay (fcn) { + var result = AndroidInterface.recordsound_startplay(); + if (fcn) { + fcn(result); + } + } + + static stopplay (fcn) { + var result = AndroidInterface.recordsound_stopplay(); + if (fcn) { + fcn(result); + } + } + + static recorddisappear (b, fcn) { + var result = AndroidInterface.recordsound_recordclose(b); + if (fcn) { + fcn(result); + } + } + + // camera functions + + static hascamera () { + return AndroidInterface.scratchjr_cameracheck(); + } + + static startfeed (data, fcn) { + var str = JSON.stringify(data); + var result = AndroidInterface.scratchjr_startfeed(str); + if (fcn) { + fcn(result); + } + } + + static stopfeed (fcn) { + var result = AndroidInterface.scratchjr_stopfeed(); + if (fcn) { + fcn(result); + } + } + + static choosecamera (mode, fcn) { + var result = AndroidInterface.scratchjr_choosecamera(mode); + if (fcn) { + fcn(result); + } + } + + static captureimage (fcn) { + AndroidInterface.scratchjr_captureimage(fcn); + } + + static hidesplash (fcn) { + // just call funct, splash is hidden in native code + if (fcn) { + fcn(); + } + } + + /////////////// + // Sharing + /////////////// + + + // Called on the JS side to trigger native UI for project sharing. + // fileName: name for the file to share + // emailSubject: subject text to use for an email + // emailBody: body HTML to use for an email + // shareType: 0 for Email; 1 for Airdrop + // b64data: base-64 encoded .SJR file to share + + static sendSjrToShareDialog (fileName, emailSubject, emailBody, shareType, b64data) { + AndroidInterface.sendSjrUsingShareDialog(fileName, emailSubject, emailBody, shareType, b64data); + } + + // // Called on the Objective-C side. The argument is a base64-encoded .SJR file, + // // to be unzipped, processed, and stored. + // static loadProjectFromSjr (b64data) { + // try { + // IO.loadProjectFromSjr(b64data); + // } catch (err) { + // var errorMessage = 'Couldn\'t load share -- project data corrupted. ' + err.message; + // Alert.open(gn('frame'), gn('frame'), errorMessage, '#ff0000'); + // console.log(err); // eslint-disable-line no-console + // return 0; + // } + // return 1; + // } + + // Name of the device/iPad to display on the sharing dialog page + // fcn is called with the device name as an arg + static deviceName (fcn) { + fcn(AndroidInterface.deviceName()); + } + + static analyticsEvent (category, action, label) { + AndroidInterface.analyticsEvent(category, action, label); + } + + static setAnalyticsPlacePref (preferredPlace) { + AndroidInterface.setAnalyticsPlacePref(preferredPlace); + } + + // // Web Wiew delegate call backs + // + // static pageError (desc) { + // console.log('XCODE ERROR:', desc); // eslint-disable-line no-console + // if (window.location.href.indexOf('home.html') > -1) { + // if (Lobby.errorTimer) { + // Lobby.errorLoading(desc); + // } + // } + // } +} + +// // Expose Android methods for ScratchJr tablet sharing callbacks +// window.Android = Android; diff --git a/src/iPad/IO.js b/src/tablet/IO.js similarity index 95% rename from src/iPad/IO.js rename to src/tablet/IO.js index db9727e..e378faf 100644 --- a/src/iPad/IO.js +++ b/src/tablet/IO.js @@ -1,4 +1,4 @@ -import iOS from './iOS'; +import OS from './OS'; import MediaLib from './MediaLib'; import JSZip from 'jszip'; import {setCanvasSize, drawThumbnail, gn} from '../utils/lib'; @@ -90,10 +90,10 @@ export default class IO { IO.requestFromServer(md5, gotit); // get url contents return; } - if ((IO.getExtension(md5) == 'png') && iOS.path) { - fcn(iOS.path + md5); // only if it is not in debug mode + if ((IO.getExtension(md5) == 'png') && OS.path) { + fcn(OS.path + md5); // only if it is not in debug mode } else { - iOS.getmedia(md5, nextStep); + OS.getmedia(md5, nextStep); } // get url contents function gotit (str) { @@ -109,8 +109,8 @@ export default class IO { function nextStep (dataurl) { // iOS 7 requires to read the internal base64 images before returning contents var str = atob(dataurl); - if ((str.indexOf('xlink:href') < 0) && iOS.path) { - fcn(iOS.path + md5); // does not have embedded images + if ((str.indexOf('xlink:href') < 0) && OS.path) { + fcn(OS.path + md5); // does not have embedded images } else { var base64 = IO.getImageDataURL(md5, dataurl); IO.getImagesInSVG(str, function () { @@ -210,11 +210,11 @@ export default class IO { var json = {}; json.stmt = 'select * from ' + db + ' where id = ?'; json.values = [md5]; - iOS.query(json, fcn); + OS.query(json, fcn); } static setMedia (data, type, fcn) { - iOS.setmedia(btoa(data), type, fcn); + OS.setmedia(btoa(data), type, fcn); } static query (type, obj, fcn) { @@ -222,14 +222,14 @@ export default class IO { json.stmt = 'select ' + obj.items + ' from ' + type + ' where ' + obj.cond + (obj.order ? ' order by ' + obj.order : ''); json.values = obj.values; - iOS.query(json, fcn); + OS.query(json, fcn); } static deleteobject (type, id, fcn) { var json = {}; json.stmt = 'delete from ' + type + ' where id = ?'; json.values = [id]; - iOS.stmt(json, fcn); + OS.stmt(json, fcn); } //////////////////////// @@ -258,7 +258,7 @@ export default class IO { addValue('thumbnail', JSON.stringify(obj.thumbnail)); } json.stmt = 'insert into ' + database + ' (' + keylist.toString() + ') values (' + values + ')'; - iOS.stmt(json, fcn); + OS.stmt(json, fcn); function addValue (key, str) { keylist.push(key); values += ',?'; @@ -272,7 +272,7 @@ export default class IO { json.values = [obj.version, obj.deleted, obj.name, JSON.stringify(obj.json), JSON.stringify(obj.thumbnail), (new Date()).getTime().toString()]; json.stmt = 'update ' + database + ' set ' + keylist.toString() + ' where id = ' + obj.id; - iOS.stmt(json, fcn); + OS.stmt(json, fcn); } // Since saveProject is changing the modified time of the project, @@ -282,7 +282,7 @@ export default class IO { var keylist = ['isgift = ?']; json.values = [obj.isgift]; json.stmt = 'update ' + database + ' set ' + keylist.toString() + ' where id = ' + obj.id; - iOS.stmt(json, fcn); + OS.stmt(json, fcn); } static getExtension (str) { @@ -407,7 +407,7 @@ export default class IO { }); } else { // User file - iOS.getmedia(md5, addB64ToZip); + OS.getmedia(md5, addB64ToZip); } }; @@ -496,7 +496,7 @@ export default class IO { json.cond = 'deleted = ? AND gallery IS NULL'; json.items = ['name']; json.values = ['NO']; - IO.query(iOS.database, json, function (existingProjects) { + IO.query(OS.database, json, function (existingProjects) { var newNumber = null; existingProjects = JSON.parse(existingProjects); @@ -613,14 +613,14 @@ export default class IO { if (subFolder == 'thumbnails' || subFolder == 'sounds') { // Save these immediately to the filesystem - no additional processing necessary - iOS.setmedianame(b2data, name, ext, function () { + OS.setmedianame(b2data, name, ext, function () { saveActual++; }); } else if (subFolder == 'characters') { // This code is messy - needs a refactor sometime for all the database calls/duplication for bkgs... // Save the character, generate its thumbnail, and add entry to the database - iOS.setmedianame(b2data, name, ext, function () { // Saves the SVG + OS.setmedianame(b2data, name, ext, function () { // Saves the SVG // Parse SVG to determine width/height var svgParser = new DOMParser().parseFromString(data, 'text/xml'); var width = svgParser.getElementsByTagName('svg')[0].width.baseVal.value; @@ -636,7 +636,7 @@ export default class IO { var charName = characterNames[fullName]; - iOS.setmedia(thumbnailPngBase64, 'png', function (thumbnailMD5) { + OS.setmedia(thumbnailPngBase64, 'png', function (thumbnailMD5) { // Sprite thumbnail is saved - save character to the DB // First ensure that this character doesn't already exist in the exact form @@ -660,7 +660,7 @@ export default class IO { json.stmt = 'insert into usershapes (' + keylist.toString() + ') values (' + values + ')'; - iOS.stmt(json, function () { + OS.stmt(json, function () { saveActual++; }); } else { @@ -672,13 +672,13 @@ export default class IO { }); } else if (subFolder == 'backgrounds') { // Same idea as characters, but the dimensions are fixed - iOS.setmedianame(b2data, name, ext, function () { + OS.setmedianame(b2data, name, ext, function () { IO.getImagesInSVG(data, gotSVGImages); function gotSVGImages () { var thumbnailDataURL = IO.getThumbnail(data, 480, 360, 120, 90); var thumbnailPngBase64 = thumbnailDataURL.split(',')[1]; - iOS.setmedia(thumbnailPngBase64, 'png', function (thumbnailMD5) { + OS.setmedia(thumbnailPngBase64, 'png', function (thumbnailMD5) { // First ensure that this bg doesn't already exist in the exact form var json = {}; @@ -696,7 +696,7 @@ export default class IO { json.values = [fullName, thumbnailMD5, 'iOSv01', '480', '360', 'svg']; json.stmt = 'insert into userbkgs (' + keylist.toString() + ') values (' + values + ')'; - iOS.stmt(json, function () { + OS.stmt(json, function () { saveActual++; }); } else { diff --git a/src/iPad/MediaLib.js b/src/tablet/MediaLib.js similarity index 100% rename from src/iPad/MediaLib.js rename to src/tablet/MediaLib.js diff --git a/src/tablet/OS.js b/src/tablet/OS.js new file mode 100644 index 0000000..ecfc482 --- /dev/null +++ b/src/tablet/OS.js @@ -0,0 +1,287 @@ +import {isiOS, isAndroid, gn} from '../utils/lib'; +import IO from './IO'; +import iOS from './iOS'; +import Android from './Android'; +import Lobby from '../lobby/Lobby'; +import Alert from '../editor/ui/Alert'; +import ScratchAudio from '../utils/ScratchAudio'; + +////////////////////////////////////////////////// +// Tablet interface functions +////////////////////////////////////////////////// + +let path; +let camera; +let database = 'projects'; +let tabletInterface = null; + +export default class OS { + // Getters/setters for properties used in other classes + static get path () { + return path; + } + + static set path (newPath) { + path = newPath; + } + + static get camera () { + return camera; + } + + static get database () { + return database; + } + + // Wait for the tablet interface to be injected into the webview + static waitForInterface (fcn) { + // Already loaded the interface + if (tabletInterface != null) { + fcn(); + return; + } + if ((isAndroid && typeof AndroidInterface === 'undefined') || (isiOS && typeof (window.tablet) !== 'object')) { + // interface not loaded - come back in 100ms + setTimeout(function () { + OS.waitForInterface(fcn); + }, 100); + } + + tabletInterface = isiOS ? iOS : Android; + if (fcn) { + fcn(); + } + return; + } + + // Database functions + static stmt (json, fcn) { + tabletInterface.stmt(json, fcn); + } + + static query (json, fcn) { + tabletInterface.query(json, fcn); + } + + // DB helper - shared by both + static setfield (db, id, fieldname, val, fcn) { + var json = {}; + var keylist = [fieldname + ' = ?', 'mtime = ?']; + json.values = [val, (new Date()).getTime().toString()]; + json.stmt = 'update ' + db + ' set ' + keylist.toString() + ' where id = ' + id; + OS.stmt(json, fcn); + } + + // IO functions + + static cleanassets (ft, fcn) { + tabletInterface.cleanassets(ft, fcn); + } + + static getsettings (fcn) { + tabletInterface.getsettings(fcn); + } + + static getmedia (file, fcn) { + tabletInterface.getmedia(file, fcn); + } + + // static getmediadata (key, offset, len, fcn) { + // tabletInterface.getmediadata(key, offset, len, fcn); + // } + // + // static processdata (key, off, len, oldstr, fcn) { + // if (len == 0) { + // OS.getmediadone(key); + // fcn(oldstr); + // return; + // } + // var newlen = (len < 100000) ? len : 100000; + // OS.getmediadata(key, off, newlen, function (str) { + // OS.processdata(key, off + newlen, len - newlen, oldstr + str, fcn); + // }); + // } + // + // static getmediadone (file, fcn) { + // tabletInterface.getmediadone(file, fcn); + // } + + static setmedia (str, ext, fcn) { + tabletInterface.setmedia(str, ext, fcn); + } + + static setmedianame (str, name, ext, fcn) { + tabletInterface.setmedianame(str, name, ext, fcn); + } + + static getmd5 (str, fcn) { + tabletInterface.getmd5(str, fcn); + } + + static remove (str, fcn) { + tabletInterface.remove(str, fcn); + } + + static getfile (str, fcn) { + tabletInterface.getfile(str, fcn); + } + + static setfile (name, str, fcn) { + tabletInterface.setfile(name, str, fcn); + } + + // Sound functions + + static registerSound (dir, name, fcn) { + tabletInterface.registerSound(dir, name, fcn); + } + + static playSound (name, fcn) { + tabletInterface.playSound(name, fcn); + } + + static stopSound (name, fcn) { + tabletInterface.stopSound(name, fcn); + } + + // Web Wiew delegate call backs + + static soundDone (name) { + ScratchAudio.soundDone(name); + } + + static sndrecord (fcn) { + tabletInterface.sndrecord(fcn); + } + + static recordstop (fcn) { + tabletInterface.recordstop(fcn); + } + + static volume (fcn) { + tabletInterface.volume(fcn); + } + + static startplay (fcn) { + tabletInterface.startplay(fcn); + } + + static stopplay (fcn) { + tabletInterface.stopplay(fcn); + } + + static recorddisappear (b, fcn) { + tabletInterface.recorddisappear(b, fcn); + } + + // Record state + static askpermission () { + if (isiOS) { + iOS.askpermission(); + } + } + + // camera functions + + static hascamera () { + camera = tabletInterface.hascamera(); + } + + static startfeed (data, fcn) { + tabletInterface.startfeed(data, fcn); + } + + static stopfeed (fcn) { + tabletInterface.stopfeed(fcn); + } + + static choosecamera (mode, fcn) { + tabletInterface.choosecamera(mode, fcn); + } + + static captureimage (fcn) { + tabletInterface.captureimage(fcn); + } + + static hidesplash (fcn) { + if (isiOS) { + iOS.hidesplash(); + } + if (fcn) { + fcn(); + } + } + + static trace (str) { + console.log(str); // eslint-disable-line no-console + } + + static parse (str) { + console.log(JSON.parse(str)); // eslint-disable-line no-console + } + + static tracemedia (str) { + console.log(atob(str)); // eslint-disable-line no-console + } + + ignore () { + } + + /////////////// + // Sharing + /////////////// + + + // Called on the JS side to trigger native UI for project sharing. + // fileName: name for the file to share + // emailSubject: subject text to use for an email + // emailBody: body HTML to use for an email + // shareType: 0 for Email; 1 for Airdrop + // b64data: base-64 encoded .SJR file to share + + static sendSjrToShareDialog (fileName, emailSubject, emailBody, shareType, b64data) { + tabletInterface.sendSjrToShareDialog(fileName, emailSubject, emailBody, shareType, b64data); + } + + // Called on the Objective-C side. The argument is a base64-encoded .SJR file, + // to be unzipped, processed, and stored. + static loadProjectFromSjr (b64data) { + try { + IO.loadProjectFromSjr(b64data); + } catch (err) { + var errorMessage = 'Couldn\'t load share -- project data corrupted. ' + err.message; + Alert.open(gn('frame'), gn('frame'), errorMessage, '#ff0000'); + console.log(err); // eslint-disable-line no-console + return 0; + } + return 1; + } + + // Name of the device/iPad to display on the sharing dialog page + // fcn is called with the device name as an arg + static deviceName (fcn) { + tabletInterface.deviceName(fcn); + } + + static analyticsEvent (category, action, label) { + tabletInterface.analyticsEvent(category, action, label); + } + + static setAnalyticsPlacePref (preferredPlace) { + tabletInterface.setAnalyticsPlacePref(preferredPlace); + } + + // Web Wiew delegate call backs + + static pageError (desc) { + console.log('XCODE ERROR:', desc); // eslint-disable-line no-console + if (window.location.href.indexOf('home.html') > -1) { + if (Lobby.errorTimer) { + Lobby.errorLoading(desc); + } + } + } +} + +// Expose OS methods for ScratchJr tablet sharing callbacks +window.OS = OS; diff --git a/src/tablet/iOS.js b/src/tablet/iOS.js new file mode 100644 index 0000000..20fc4d7 --- /dev/null +++ b/src/tablet/iOS.js @@ -0,0 +1,301 @@ +////////////////////////////////////////////////// +// iOS interface functions +// window.tablet is the class where native functions are injected for calling in +// javascript. It will be initialized prior to calling any functions in this class +////////////////////////////////////////////////// + +let mediacounter = 0; + +export default class iOS { + + // waitForInterface is defined in OS, and should make sure that the correct + // interface is available. + // static waitForInterface (fcn) { + // // iOS device - interface might not be loaded yet + // if (typeof (window.tablet) != 'object') { + // // Come back in 100ms + // setTimeout(function () { + // iOS.waitForInterface(fcn); + // }, 100); + // } else { + // // All set to run commands + // if (fcn) { + // fcn(); + // } + // } + // } + + // Database functions + static stmt (json, fcn) { + var result = window.tablet.database_stmt(JSON.stringify(json)); + if (typeof (fcn) !== 'undefined') { + fcn(result); + } + } + + static query (json, fcn) { + var result = window.tablet.database_query(JSON.stringify(json)); + if (typeof (fcn) !== 'undefined') { + fcn(result); + } + } + + // IO functions + + static cleanassets (ft, fcn) { + window.tablet.io_cleanassets(ft); fcn(); + } + + static getsettings (fcn) { + var result = window.tablet.io_getsettings(); + if (fcn) { + fcn(result); + } + } + + static getmedia (file, fcn) { + mediacounter++; + var nextStep = function (file, key, whenDone) { + var result = window.tablet.io_getmedialen(file, key); + iOS.processdata(key, 0, result, '', whenDone); + }; + nextStep(file, mediacounter, fcn); + } + + static getmediadata (key, offset, len, fcn) { + var result = window.tablet.io_getmediadata(key, offset, len); + if (fcn) { + fcn(result); + } + } + + static processdata (key, off, len, oldstr, fcn) { + if (len == 0) { + iOS.getmediadone(key); + fcn(oldstr); + return; + } + var newlen = (len < 100000) ? len : 100000; + iOS.getmediadata(key, off, newlen, function (str) { + iOS.processdata(key, off + newlen, len - newlen, oldstr + str, fcn); + }); + } + + static getmediadone (file, fcn) { + var result = window.tablet.io_getmediadone(file); + if (fcn) { + fcn(result); + } + } + + static setmedia (str, ext, fcn) { + var result = window.tablet.io_setmedia(str, ext); + if (fcn) { + fcn(result); + } + } + + static setmedianame (str, name, ext, fcn) { + var result = window.tablet.io_setmedianame(str, name, ext); + if (fcn) { + fcn(result); + } + } + + static getmd5 (str, fcn) { + var result = window.tablet.io_getmd5(str); + if (fcn) { + fcn(result); + } + } + + static remove (str, fcn) { + var result = window.tablet.io_remove(str); + if (fcn) { + fcn(result); + } + } + + static getfile (str, fcn) { + var result = window.tablet.io_getfile(str); + if (fcn) { + fcn(result); + } + } + + static setfile (name, str, fcn) { + var result = window.tablet.io_setfile(name, btoa(str)); + if (fcn) { + fcn(result); + } + } + + // Sound functions + + static registerSound (dir, name, fcn) { + var result = window.tablet.io_registersound(dir, name); + if (fcn) { + fcn(result); + } + } + + static playSound (name, fcn) { + var result = window.tablet.io_playsound(name); + if (fcn) { + fcn(result); + } + } + + static stopSound (name, fcn) { + var result = window.tablet.io_stopsound(name); + if (fcn) { + fcn(result); + } + } + + // Web Wiew delegate call backs + + static sndrecord (fcn) { + var result = window.tablet.recordsound_recordstart(); + if (fcn) { + fcn(result); + } + } + + static recordstop (fcn) { + var result = window.tablet.recordsound_recordstop(); + if (fcn) { + fcn(result); + } + } + + static volume (fcn) { + var result = window.tablet.recordsound_volume(); + if (fcn) { + fcn(result); + } + } + + static startplay (fcn) { + var result = window.tablet.recordsound_startplay(); + if (fcn) { + fcn(result); + } + } + + static stopplay (fcn) { + var result = window.tablet.recordsound_stopplay(); + if (fcn) { + fcn(result); + } + } + + static recorddisappear (b, fcn) { + var result = window.tablet.recordsound_recordclose(b); + if (fcn) { + fcn(result); + } + } + + // Record state + static askpermission () { + window.tablet.askForPermission(); + } + + // camera functions + + static hascamera () { + return window.tablet.scratchjr_cameracheck(); + } + + static startfeed (data, fcn) { + var str = JSON.stringify(data); + var result = window.tablet.scratchjr_startfeed(str); + if (fcn) { + fcn(result); + } + } + + static stopfeed (fcn) { + var result = window.tablet.scratchjr_stopfeed(); + if (fcn) { + fcn(result); + } + } + + static choosecamera (mode, fcn) { + var result = window.tablet.scratchjr_choosecamera(mode); + if (fcn) { + fcn(result); + } + } + + static captureimage (fcn) { + window.tablet.scratchjr_captureimage(fcn); + } + + static hidesplash (fcn) { + window.tablet.hideSplash(); + if (fcn) { + fcn(); + } + } + + /////////////// + // Sharing + /////////////// + + + // Called on the JS side to trigger native UI for project sharing. + // fileName: name for the file to share + // emailSubject: subject text to use for an email + // emailBody: body HTML to use for an email + // shareType: 0 for Email; 1 for Airdrop + // b64data: base-64 encoded .SJR file to share + + static sendSjrToShareDialog (fileName, emailSubject, emailBody, shareType, b64data) { + window.tablet.sendSjrUsingShareDialog(fileName, emailSubject, emailBody, shareType, b64data); + } + + // // Called on the Objective-C side. The argument is a base64-encoded .SJR file, + // // to be unzipped, processed, and stored. + // static loadProjectFromSjr (b64data) { + // try { + // IO.loadProjectFromSjr(b64data); + // } catch (err) { + // var errorMessage = 'Couldn\'t load share -- project data corrupted. ' + err.message; + // Alert.open(gn('frame'), gn('frame'), errorMessage, '#ff0000'); + // console.log(err); // eslint-disable-line no-console + // return 0; + // } + // return 1; + // } + + // Name of the device/iPad to display on the sharing dialog page + // fcn is called with the device name as an arg + static deviceName (fcn) { + fcn(window.tablet.deviceName()); + } + + static analyticsEvent (category, action, label) { + window.tablet.analyticsEvent(category, action, label); + } + + static setAnalyticsPlacePref (preferredPlace) { + window.tablet.setAnalyticsPlacePref(preferredPlace); + } + + // // Web Wiew delegate call backs + // + // static pageError (desc) { + // console.log('XCODE ERROR:', desc); // eslint-disable-line no-console + // if (window.location.href.indexOf('home.html') > -1) { + // if (Lobby.errorTimer) { + // Lobby.errorLoading(desc); + // } + // } + // } +} + +// Expose iOS methods for ScratchJr tablet sharing callbacks +// window.iOS = iOS; diff --git a/src/utils/Localization.js b/src/utils/Localization.js index c009a22..a11b00c 100644 --- a/src/utils/Localization.js +++ b/src/utils/Localization.js @@ -1,6 +1,6 @@ import Cookie from './Cookie'; import Intl from 'intl'; -import IO from '../iPad/IO'; +import IO from '../tablet/IO'; if (!window.Intl) { window.Intl = Intl; diff --git a/src/utils/ScratchAudio.js b/src/utils/ScratchAudio.js index 7add306..1330654 100755 --- a/src/utils/ScratchAudio.js +++ b/src/utils/ScratchAudio.js @@ -1,6 +1,6 @@ import {isAndroid} from './lib'; import Sound from './Sound'; -import iOS from '../iPad/iOS'; +import OS from '../tablet/OS'; //////////////////////////////////////////////////// /// Sound Playing @@ -64,7 +64,7 @@ export default class ScratchAudio { fcn(name); } }; - iOS.registerSound(url, snd, whenDone); + OS.registerSound(url, snd, whenDone); } else { // In Android, this is handled outside of JavaScript, so just place a stub here. dict[snd] = new Sound(url + snd); diff --git a/src/utils/Sound.js b/src/utils/Sound.js index 1d7aba9..66ede8f 100644 --- a/src/utils/Sound.js +++ b/src/utils/Sound.js @@ -1,5 +1,5 @@ import {isAndroid} from './lib'; -import iOS from '../iPad/iOS'; +import OS from '../tablet/OS'; export default class Sound { constructor (name, time) { @@ -23,7 +23,7 @@ export default class Sound { if (this.playing) { this.stop(); } - iOS.playSound(this.name); + OS.playSound(this.name); this.playing = true; } } @@ -51,7 +51,7 @@ export default class Sound { } this.soundPlayId = null; } else { - iOS.stopSound(this.name); + OS.stopSound(this.name); this.playing = false; } } diff --git a/src/utils/lib.js b/src/utils/lib.js index 99d5673..a2664a2 100755 --- a/src/utils/lib.js +++ b/src/utils/lib.js @@ -8,8 +8,21 @@ export const WINDOW_INNER_WIDTH = window.innerWidth; export const scaleMultiplier = WINDOW_INNER_HEIGHT / 768.0; export const fullscreenScaleMultiplier = 136; -export const isiOS = (typeof AndroidInterface == 'undefined'); -export const isAndroid = (typeof AndroidInterface != 'undefined'); +export function detectOS () { + var userAgent = window.navigator.userAgent.toLowerCase(); + const ios = /iphone|ipod|ipad/.test( userAgent ); + // safari = /safari/.test( userAgent ), // currently do not need to detect browser vs webview + // android = /android/.text.(userAgent); + + if( ios ) { + return 'iOS'; + } else { + // for now assume Android, this could be further refined to detect Chromium etc. + return 'android'; + } +} +export const isiOS = (detectOS() == 'iOS'); +export const isAndroid = (detectOS() == 'android'); export function libInit () { frame = document.getElementById('frame'); From 09c52f370f2a96d2a84e86ffbdf80df401ca6e9c Mon Sep 17 00:00:00 2001 From: Chris Garrity Date: Mon, 10 Aug 2020 09:22:33 -0400 Subject: [PATCH 2/3] Android studio changes Android Studio added some more config files. --- .../ScratchJr/.idea/codeStyles/Project.xml | 116 ++++++++++++++++++ android/ScratchJr/.idea/misc.xml | 8 +- 2 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 android/ScratchJr/.idea/codeStyles/Project.xml diff --git a/android/ScratchJr/.idea/codeStyles/Project.xml b/android/ScratchJr/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..681f41a --- /dev/null +++ b/android/ScratchJr/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/android/ScratchJr/.idea/misc.xml b/android/ScratchJr/.idea/misc.xml index e8ee3d9..d5d4a5c 100644 --- a/android/ScratchJr/.idea/misc.xml +++ b/android/ScratchJr/.idea/misc.xml @@ -5,7 +5,7 @@ From dd1f48f5e33d976ab6df24812b12983f9a6bd064 Mon Sep 17 00:00:00 2001 From: Chris Garrity Date: Mon, 10 Aug 2020 16:07:14 -0400 Subject: [PATCH 3/3] Remove dead code for real removing dead code that was just commented in the previous commit, --- src/editor/ScratchJr.js | 20 -------------------- src/tablet/Android.js | 14 -------------- src/tablet/OS.js | 22 ++-------------------- src/tablet/iOS.js | 17 ----------------- 4 files changed, 2 insertions(+), 71 deletions(-) diff --git a/src/editor/ScratchJr.js b/src/editor/ScratchJr.js index d5ee5ea..d6852cb 100644 --- a/src/editor/ScratchJr.js +++ b/src/editor/ScratchJr.js @@ -905,26 +905,6 @@ export default class ScratchJr { ///////////////// //Application on the background - - // // XXX: does this ever happen? - // // I'm pretty sure this is dead code -TM - // static saveProjectState () { - // ScratchAudio.sndFX('tap.wav'); - // if (frame.style.display == 'none') { - // Paint.saveEditState(ScratchJr.stopServer); - // } else { - // ScratchJr.unfocus(); - // ScratchJr.stopStrips(); - // if (ScratchJr.isEditable() && currentProject && !Project.error && changed) { - // Project.save(currentProject, ScratchJr.stopServer); - // } - // } - // } - // - // static stopServer () { - // OS.stopserver(OS.trace); - // } - /** * The functions that are invokved when the Android back button is clicked. * Methods are called from the rear and popped off after each invocation. diff --git a/src/tablet/Android.js b/src/tablet/Android.js index 76e695f..90d0fee 100644 --- a/src/tablet/Android.js +++ b/src/tablet/Android.js @@ -6,20 +6,6 @@ let mediacounter = 0; export default class Android { - // // This will be set up in the OS class - // // Wait for the tablet interface to be injected into the webview - // static waitForInterface (fcn) { - // // Already loaded the interface - // if (typeof AndroidInterface !== 'undefined') { - // fcn(); - // return; - // } - // // interface not yet available, come back in 100ms - // setTimeout(function () { - // Android.waitForInterface(fcn); - // }, 100); - // } - // Database functions static stmt (json, fcn) { var result = AndroidInterface.database_stmt(JSON.stringify(json)); diff --git a/src/tablet/OS.js b/src/tablet/OS.js index ecfc482..794b7ef 100644 --- a/src/tablet/OS.js +++ b/src/tablet/OS.js @@ -82,30 +82,12 @@ export default class OS { tabletInterface.getsettings(fcn); } + // note the interfaces (iOS and Android) are responsible for deciding how + // to manage getting media (e.g. whether it needs to be done in chunks etc) static getmedia (file, fcn) { tabletInterface.getmedia(file, fcn); } - // static getmediadata (key, offset, len, fcn) { - // tabletInterface.getmediadata(key, offset, len, fcn); - // } - // - // static processdata (key, off, len, oldstr, fcn) { - // if (len == 0) { - // OS.getmediadone(key); - // fcn(oldstr); - // return; - // } - // var newlen = (len < 100000) ? len : 100000; - // OS.getmediadata(key, off, newlen, function (str) { - // OS.processdata(key, off + newlen, len - newlen, oldstr + str, fcn); - // }); - // } - // - // static getmediadone (file, fcn) { - // tabletInterface.getmediadone(file, fcn); - // } - static setmedia (str, ext, fcn) { tabletInterface.setmedia(str, ext, fcn); } diff --git a/src/tablet/iOS.js b/src/tablet/iOS.js index 20fc4d7..747751e 100644 --- a/src/tablet/iOS.js +++ b/src/tablet/iOS.js @@ -8,23 +8,6 @@ let mediacounter = 0; export default class iOS { - // waitForInterface is defined in OS, and should make sure that the correct - // interface is available. - // static waitForInterface (fcn) { - // // iOS device - interface might not be loaded yet - // if (typeof (window.tablet) != 'object') { - // // Come back in 100ms - // setTimeout(function () { - // iOS.waitForInterface(fcn); - // }, 100); - // } else { - // // All set to run commands - // if (fcn) { - // fcn(); - // } - // } - // } - // Database functions static stmt (json, fcn) { var result = window.tablet.database_stmt(JSON.stringify(json));