Skip to content

Commit a2cd95f

Browse files
committed
Android support
1 parent 26c8e4b commit a2cd95f

File tree

5 files changed

+153
-6
lines changed

5 files changed

+153
-6
lines changed

src/locales/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"p4": {
33
"description1": {
4-
"string": "Converts Scratch projects into HTML files, zip archives, or executable programs for Windows, macOS, and Linux."
4+
"string": "Converts Scratch projects into HTML files, zip archives, or executable programs for Windows, macOS, Linux, and Android."
55
},
66
"description2": {
77
"string": "If you just want an easy way to embed a project into your website, you may be interested in {embedding}."
@@ -363,6 +363,9 @@
363363
"string": "{type} Linux application (64-bit only)",
364364
"context": "type will become something like 'NW.js' or 'Electron'. Do not translate 'Linux'."
365365
},
366+
"application-android": {
367+
"string": "Android app (Highly experimental)"
368+
},
366369
"otherEnvironments": {
367370
"string": "Other environments (Click to open)",
368371
"context": "Text that can be clicked to expand list of environments to include some unrecommended ones."

src/p4/P4.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
:global([theme="dark"] .is-not-safari select:hover) {
8484
border-color: #bbb;
8585
}
86-
:global(p), :global(h1), :global(h2), :global(h3) {
86+
:global(p), :global(pre), :global(h1), :global(h2), :global(h3) {
8787
margin: 12px 0;
8888
}
8989
:global(summary) {

src/p4/PackagerOptions.svelte

Lines changed: 130 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,9 @@
180180
font-weight: bold;
181181
background: yellow;
182182
color: black;
183-
padding: 10px;
184-
border-radius: 10px;
183+
padding: 12px;
184+
border-radius: 12px;
185+
margin: 12px 0;
185186
}
186187
</style>
187188

@@ -609,6 +610,13 @@
609610
</label>
610611
</div>
611612

613+
<div class="group">
614+
<label class="option">
615+
<input type="radio" name="environment" bind:group={$options.target} value="android">
616+
{$_('options.application-android')}
617+
</label>
618+
</div>
619+
612620
<details open={otherEnvironmentsInitiallyOpen}>
613621
<summary>{$_('options.otherEnvironments')}</summary>
614622
<p>{$_('options.otherEnvironmentsHelp')}</p>
@@ -660,7 +668,8 @@
660668
{#if $options.target.startsWith('zip')}
661669
<h2>Zip</h2>
662670
<p>The zip environment is intended to be used for publishing to a website. Other uses such as sending your project to a friend over a chat app or email should use "Plain HTML" instead as zip will not work.</p>
663-
{:else}
671+
{/if}
672+
{#if !$options.target.startsWith('zip')}
664673
<h2>{$_('options.applicationSettings')}</h2>
665674
<label class="option">
666675
{$_('options.packageName')}
@@ -715,6 +724,124 @@
715724
<p class="warning">NW.js support is deprecated and may be removed in the future. Use Electron instead if possible.</p>
716725
<p>For further help and steps, see <a href="https://docs.nwjs.io/en/latest/For%20Users/Package%20and%20Distribute/#linux">NW.js Documentation</a>.</p>
717726
{/if}
727+
{:else if $options.target === 'android'}
728+
<h2>Android</h2>
729+
730+
<div class="warning">
731+
Unlike the other environments, Android support is not fully automated. You must manually create an app. This section will try to guide you through the process.
732+
</div>
733+
734+
<p>This section assumes you have complete access to a Windows, macOS, or Linux computer.</p>
735+
736+
<h3>Install Android Studio</h3>
737+
<p><a href="https://developer.android.com/studio/">Install Android Studio.</a></p>
738+
<p>This quite large and may take a while.</p>
739+
740+
<h3>Create a new project</h3>
741+
<p>Create a new project in Android studio.</p>
742+
<ul>
743+
<li>Use the "Empty Activity" template</li>
744+
<li>Set Name to your app's name, for example "<code>{$options.app.windowTitle}</code>"</li>
745+
<li>Set Package name to "<code>org.turbowarp.packager.userland.{$options.app.packageName}</code>"</li>
746+
<li>Choose a save location that you won't forget</li>
747+
<li>Set Language to Kotlin</li>
748+
<li>Set Minimum SDK to "API 21: Android 5.0 (Lollipop)"</li>
749+
</ul>
750+
751+
<h3>Create assets folder</h3>
752+
<p>In the sidebar on the left, right click on "app", then select "New" > "Folder" > "Assets folder". Use the default settings.</p>
753+
754+
<h3>Prepare project</h3>
755+
<p>
756+
<Button on:click={pack} text={'Package the project as a zip'} />
757+
</p>
758+
<p>Extract the zip and drag its files into "assets" folder you created. (You can directly drag and drop files over the assets folder in Android Studio)</p>
759+
760+
<h3>Making the app</h3>
761+
<p>In the sidebar on the left, navigate to app > src > main > MainActivity. This will open a short Kotlin file.</p>
762+
<p>Replace everything after line 2 with the following:</p>
763+
<pre>
764+
{[
765+
'import android.annotation.SuppressLint',
766+
'import androidx.appcompat.app.AppCompatActivity',
767+
'import android.os.Bundle',
768+
'import android.webkit.WebView',
769+
'',
770+
'class MainActivity : AppCompatActivity() {',
771+
' private lateinit var web: WebView',
772+
'',
773+
' @SuppressLint("SetJavaScriptEnabled")',
774+
' override fun onCreate(savedInstanceState: Bundle?) {',
775+
' super.onCreate(savedInstanceState)',
776+
' web = WebView(this)',
777+
' web.settings.javaScriptEnabled = true',
778+
' web.loadUrl("file:///android_asset/index.html")',
779+
' setContentView(web)',
780+
' actionBar?.hide()',
781+
' supportActionBar?.hide()',
782+
' }',
783+
'',
784+
' override fun onDestroy() {',
785+
' super.onDestroy()',
786+
' web.destroy()',
787+
' }',
788+
'}'
789+
].join('\n')}
790+
</pre>
791+
<p>At this point, you now have a fully functional Android app. However, there are still a few more things you should change.</p>
792+
793+
<h3>Fixing screen orientation issues</h3>
794+
<p>In the sidebar on the left, open app > main > AndroidManifest.xml</p>
795+
<p>Find the section that looks like this:</p>
796+
<pre>
797+
{[
798+
' <activity',
799+
' android:name=".MainActivity"',
800+
' android:exported="true">',
801+
].join('\n')}
802+
</pre>
803+
<p>And replace it with this:</p>
804+
<pre>
805+
{[
806+
' <activity',
807+
' android:configChanges="orientation|screenSize"',
808+
' android:screenOrientation="sensor"',
809+
' android:name=".MainActivity"',
810+
' android:exported="true">',
811+
].join('\n')}
812+
</pre>
813+
814+
<h3>Updating colors</h3>
815+
<p>If you ran the app now, it would have a purple color scheme, which may not be what you want. This can be changed.</p>
816+
<p>In the sidebar on the left, open app > main > res > values > color.xml.</p>
817+
<p>You will see these lines:</p>
818+
<pre>
819+
{[
820+
' <color name="purple_200">#FFBB86FC</color>',
821+
' <color name="purple_500">#FF6200EE</color>',
822+
' <color name="purple_700">#FF3700B3</color>',
823+
].join('\n')}
824+
</pre>
825+
<p>Replace those lines with:</p>
826+
<pre>
827+
{[
828+
` <color name="purple_200">#FF${$options.appearance.background.substr(1)}</color>`,
829+
` <color name="purple_500">#FF${$options.appearance.background.substr(1)}</color>`,
830+
` <color name="purple_700">#FF${$options.appearance.background.substr(1)}</color>`,
831+
].join('\n')}
832+
</pre>
833+
<p>Do not change the other lines.</p>
834+
<p>The above snippet will make the status bar match your app's background color. For advanced users, note that these color codes are a bit unusual in that the "alpha" or "transparency" byte goes first instead of last.</p>
835+
<p>Ignore the bits about <code>purple_yyy</code> -- just them as is. While it would be a good idea to these colors, you will be making more work for yourself because you'll have to update some other files to reflect the new names.</p>
836+
837+
<h3>Updating the project</h3>
838+
<p>It's likely that at some point you will want to update the project without redoing this entire guide. Updating is much simpler:</p>
839+
<ol>
840+
<li>Open Android Studio and open the project</li>
841+
<li>Delete everything inside the assets folder</li>
842+
<li>Re-run the packager</li>
843+
<li>Extract the zip and put all of its files into the assets folder</li>
844+
</ol>
718845
{/if}
719846
</div>
720847
</Section>

src/p4/template.ejs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1">
66
<% const {LONG_NAME, WEBSITE} = require('../packager/brand.js'); %>
77
<title><%= LONG_NAME %></title>
8-
<meta name="description" content="Converts Scratch projects into HTML files, zip archives, or executable programs for Windows, macOS, and Linux.">
8+
<meta name="description" content="Converts Scratch projects into HTML files, zip archives, or executable programs for Windows, macOS, Linux, and Android.">
99
<% if (process.env.STANDALONE) { %>
1010
<%
1111
const fs = require('fs');

src/packager/packager.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,23 @@ cd "$(dirname "$0")"
11051105
vm.extensionManager.loadExtensionURL(extension);
11061106
}
11071107
1108+
${this.options.target === 'android' ? `
1109+
// By default, storage will try to use fetch() in a Worker
1110+
// That doesn't work in an Android WebView, but XHR does, so...
1111+
scaffolding.storage.addHelper({
1112+
load: (assetType, assetId, dataFormat) => new Promise((resolve, reject) => {
1113+
const xhr = new XMLHttpRequest();
1114+
xhr.responseType = "arraybuffer";
1115+
xhr.onerror = () => reject(new Error("Request failed"));
1116+
xhr.onload = () => resolve(new scaffolding.storage.Asset(assetType, assetId, dataFormat, new Uint8Array(xhr.response)));
1117+
xhr.open("GET", './assets/' + assetId + '.' + dataFormat);
1118+
xhr.send();
1119+
}),
1120+
// Above default tool, below "builtin" tool
1121+
priority: 50
1122+
});
1123+
` : ''}
1124+
11081125
${this.options.target.startsWith('nwjs-') ? `
11091126
if (typeof nw !== 'undefined') {
11101127
const win = nw.Window.get();

0 commit comments

Comments
 (0)