This commit is contained in:
Renan LE CARO 2025-03-18 15:26:56 +01:00
parent ffdbd71a88
commit 83b9b8b9e8
12 changed files with 7661 additions and 80 deletions

View file

@ -11,8 +11,8 @@ android {
applicationId = "me.lecaro.breakout" applicationId = "me.lecaro.breakout"
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 29038230 versionCode = 29038466
versionName = "29038230" versionName = "29038466"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true

View file

@ -1,10 +1,6 @@
<?xml version="1.0" encoding ="utf-8"?> <?xml version="1.0" encoding ="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<application <application
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:allowBackup="true" android:allowBackup="true"
@ -27,5 +23,6 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>
</manifest> </manifest>

File diff suppressed because one or more lines are too long

View file

@ -2,12 +2,15 @@ package me.lecaro.breakout
import android.app.Activity import android.app.Activity
import android.app.DownloadManager import android.app.DownloadManager
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.provider.MediaStore
import android.util.Log import android.util.Log
import android.view.Window import android.view.Window
import android.view.WindowManager import android.view.WindowManager
@ -35,8 +38,7 @@ class MainActivity : android.app.Activity() {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
filePathCallback?.onReceiveValue( filePathCallback?.onReceiveValue(
WebChromeClient.FileChooserParams.parseResult( WebChromeClient.FileChooserParams.parseResult(
resultCode, resultCode, data
data
) )
) )
filePathCallback = null filePathCallback = null
@ -44,68 +46,81 @@ class MainActivity : android.app.Activity() {
} }
} }
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
when (requestCode) {
PERM_REQUEST_CODE -> {
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
downloadFile()
} else {
Toast.makeText(this, "We cant make a save file without that permission", Toast.LENGTH_SHORT).show()
}
return
}
}
}
var filePathCallback: ValueCallback<Array<Uri>>? = null
var fileToDownload:String? = null
fun downloadFile(){ var filePathCallback: ValueCallback<Array<Uri>>? = null
val url = fileToDownload ?: return
try{ private fun downloadFile(url: String) {
try {
if (!url.startsWith("data:")) { if (!url.startsWith("data:")) {
Log.w("DL", "url ignored because it does not start with data:") Log.w("DL", "url ignored because it does not start with data:")
return return
} }
val sdf = SimpleDateFormat("yyyy-M-dd-hh-mm") val sdf = SimpleDateFormat("yyyy-M-dd-hh-mm")
val currentDate = sdf.format(Date()) val currentDate = sdf.format(Date())
// Extract filename from contentDisposition if available val base64Data = url.substringAfterLast(',')
val decodedBytes = android.util.Base64.decode(base64Data, android.util.Base64.DEFAULT)
if (url.startsWith("data:application/json;base64,")) { if (url.startsWith("data:application/json;base64,")) {
Log.d("DL", "saving application/json ") writeFile(decodedBytes, "breakout-71-save-$currentDate.b71", "application/b71")
val base64Data = url.substringAfterLast(',')
val decodedBytes =
android.util.Base64.decode(base64Data, android.util.Base64.DEFAULT)
val jsonData = String(decodedBytes);
val dir =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val fileName = "breakout-71-save-$currentDate.b71"
val file = File(dir, fileName)
file.writeText(jsonData)
Toast.makeText(this, "Saved in $dir", Toast.LENGTH_LONG).show()
Log.d("DL", "finished saving application/json ")
} else if (url.startsWith("data:video/webm;base64,")) { } else if (url.startsWith("data:video/webm;base64,")) {
Log.d("DL", "saving video/webm ") writeFile(decodedBytes, "breakout-71-gameplay-capture-$currentDate.webm", "application/b71")
// TODO
Log.d("DL", "finished savign video/webm ")
} else { } else {
Log.w("DL", "unexpected type " + url) Log.w("DL", "unexpected type " + url)
} }
}catch (e:Exception){ } catch (e: Exception) {
Log.e("DL", "Error ${e.message}") Log.e("DL", "Error ${e.message}")
Toast.makeText(this, "Error ${e.message}", Toast.LENGTH_LONG).show() Toast.makeText(this, "Error ${e.message}", Toast.LENGTH_LONG).show()
}
}
} }
fun writeFile(decodedBytes:ByteArray,fileName:String, mime:String){
val jsonData = String(decodedBytes);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, fileName)
put(MediaStore.Downloads.MIME_TYPE,mime )
put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
val uri: Uri? = contentResolver.insert(
MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues
)
uri?.let {
contentResolver.openOutputStream(it)?.use { outputStream ->
outputStream.write(decodedBytes)
}
}
val shareIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
// Example: content://com.google.android.apps.photos.contentprovider/...
putExtra(Intent.EXTRA_STREAM, uri)
type = mime
}
startActivity(Intent.createChooser(shareIntent, null))
} else {
val dir = getExternalFilesDir(null)
val file = File(dir, fileName)
file.writeText(jsonData)
Toast.makeText(this, "Saved in $dir", Toast.LENGTH_LONG).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
window.setFlags( window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN
WindowManager.LayoutParams.FLAG_FULLSCREEN
); );
val webView = WebView(this) val webView = WebView(this)
webView.settings.javaScriptEnabled = true webView.settings.javaScriptEnabled = true
@ -113,13 +128,13 @@ class MainActivity : android.app.Activity() {
webView.settings.setSupportZoom(false) webView.settings.setSupportZoom(false)
webView.loadUrl("file:///android_asset/index.html?isInWebView=true") webView.loadUrl("file:///android_asset/index.html?isInWebView=true")
val activity=this; val activity = this;
webView.webChromeClient = object : WebChromeClient() { webView.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
Log.d( Log.d(
"WebView", "${consoleMessage.message()} -- From line " + "WebView",
"${consoleMessage.lineNumber()} of ${consoleMessage.sourceId()}" "${consoleMessage.message()} -- From line " + "${consoleMessage.lineNumber()} of ${consoleMessage.sourceId()}"
) )
return true return true
} }
@ -129,13 +144,15 @@ class MainActivity : android.app.Activity() {
filePathCallback: ValueCallback<Array<Uri>>?, filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: FileChooserParams? fileChooserParams: FileChooserParams?
): Boolean { ): Boolean {
try{ try {
startActivityForResult(fileChooserParams?.createIntent(), CHOOSE_FILE_REQUEST_CODE) startActivityForResult(
fileChooserParams?.createIntent(), CHOOSE_FILE_REQUEST_CODE
)
this@MainActivity.filePathCallback = filePathCallback this@MainActivity.filePathCallback = filePathCallback
return true return true
}catch (e:Exception){ } catch (e: Exception) {
Log.e("DL", "Error ${e.message}") Log.e("DL", "Error ${e.message}")
Toast.makeText(activity, "Error ${e.message}", Toast.LENGTH_LONG).show() Toast.makeText(activity, "Error ${e.message}", Toast.LENGTH_LONG).show()
return false return false
@ -144,14 +161,7 @@ class MainActivity : android.app.Activity() {
} }
webView.setDownloadListener(DownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> webView.setDownloadListener(DownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
downloadFile(url)
fileToDownload = url
if (activity.checkSelfPermission( android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
activity.requestPermissions(arrayOf( android.Manifest.permission.WRITE_EXTERNAL_STORAGE), PERM_REQUEST_CODE)
}else{
downloadFile()
}
}) })

33
dist/PWA/sw-b71.js vendored
View file

@ -1,2 +1,33 @@
function e(e,t,n,r,a,i,c){try{var o=e[i](c),u=o.value}catch(e){n(e);return}o.done?t(u):Promise.resolve(u).then(r,a)}function t(t){return function(){var n=this,r=arguments;return new Promise(function(a,i){var c=t.apply(n,r);function o(t){e(c,a,i,o,u,"next",t)}function u(t){e(c,a,i,o,u,"throw",t)}o(void 0)})}}function n(e,t){var n,r,a,i,c={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]};return i={next:o(0),throw:o(1),return:o(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function o(i){return function(o){return function(i){if(n)throw TypeError("Generator is already executing.");for(;c;)try{if(n=1,r&&(a=2&i[0]?r.return:i[0]?r.throw||((a=r.return)&&a.call(r),0):r.next)&&!(a=a.call(r,i[1])).done)return a;switch(r=0,a&&(i=[2&i[0],a.value]),i[0]){case 0:case 1:a=i;break;case 4:return c.label++,{value:i[1],done:!1};case 5:c.label++,r=i[1],i=[0];continue;case 7:i=c.ops.pop(),c.trys.pop();continue;default:if(!(a=(a=c.trys).length>0&&a[a.length-1])&&(6===i[0]||2===i[0])){c=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]<a[3])){c.label=i[1];break}if(6===i[0]&&c.label<a[1]){c.label=a[1],a=i;break}if(a&&c.label<a[2]){c.label=a[2],c.ops.push(i);break}a[2]&&c.ops.pop(),c.trys.pop();continue}i=t.call(e,c)}catch(e){i=[6,e],r=0}finally{n=a=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,o])}}}var r="breakout-71-".concat("29038230"),a=["/"];self.addEventListener("install",function(e){e.waitUntil(t(function(){return n(this,function(e){switch(e.label){case 0:return[4,caches.open(r)];case 1:return e.sent().addAll(a),[2]}})})())}),self.addEventListener("activate",function(e){e.waitUntil(t(function(){return n(this,function(e){switch(e.label){case 0:return[4,caches.keys()];case 1:return[4,Promise.all(e.sent().map(function(e){if(e!==r)return caches.delete(e)}))];case 2:return e.sent(),[4,clients.claim()];case 3:return e.sent(),[2]}})})())}),self.addEventListener("fetch",function(e){if("navigate"===e.request.mode&&e.request.url.endsWith("/index.html?isPWA=true")){e.respondWith(caches.match("/"));return}}); // The version of the cache.
const VERSION = "29038466";
// The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`;
// The static resources that the app needs to function.
const APP_STATIC_RESOURCES = [
"/"
];
// On install, cache the static resources
self.addEventListener("install", (event)=>{
event.waitUntil((async ()=>{
const cache = await caches.open(CACHE_NAME);
cache.addAll(APP_STATIC_RESOURCES);
})());
});
// delete old caches on activate
self.addEventListener("activate", (event)=>{
event.waitUntil((async ()=>{
const names = await caches.keys();
await Promise.all(names.map((name)=>{
if (name !== CACHE_NAME) return caches.delete(name);
}));
await clients.claim();
})());
});
self.addEventListener("fetch", (event)=>{
if (event.request.mode === "navigate" && event.request.url.endsWith("/index.html?isPWA=true")) {
event.respondWith(caches.match("/"));
return;
}
});
//# sourceMappingURL=sw-b71.js.map //# sourceMappingURL=sw-b71.js.map

File diff suppressed because one or more lines are too long

3765
dist/index.html vendored

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
// The version of the cache. // The version of the cache.
const VERSION = "29038230"; const VERSION = "29038466";
// The name of the cache // The name of the cache
const CACHE_NAME = `breakout-71-${VERSION}`; const CACHE_NAME = `breakout-71-${VERSION}`;

View file

@ -1 +1 @@
"29038230" "29038466"

View file

@ -1284,9 +1284,7 @@ export function append<T>(
where.list[where.indexMin].destroyed = false; where.list[where.indexMin].destroyed = false;
makeItem(where.list[where.indexMin]); makeItem(where.list[where.indexMin]);
where.indexMin++; where.indexMin++;
console.log("Reused item " + where.indexMin);
} else { } else {
console.log("Created item " + where.indexMin);
const p = { destroyed: false }; const p = { destroyed: false };
makeItem(p); makeItem(p);
where.list.push(p); where.list.push(p);
@ -1321,5 +1319,3 @@ export function forEachLiveOne<T>(
} }
}); });
} }
//TODO check destroyed usage in code

View file

@ -117,20 +117,42 @@ export function startRecordingGame(gameState: GameState) {
video.loop = true; video.loop = true;
video.muted = true; video.muted = true;
video.playsInline = true; video.playsInline = true;
video.src = URL.createObjectURL(blob); video.src = URL.createObjectURL(blob);
targetDiv.appendChild(video);
const a = document.createElement("a"); const a = document.createElement("a");
a.download = captureFileName("webm"); a.download = captureFileName("webm");
a.target = "_blank"; a.target = "_blank";
a.href = video.src; if (window.location.href.endsWith("index.html?isInWebView=true")) {
a.href = await blobToBase64(blob);
} else {
a.href = video.src;
}
a.textContent = t("main_menu.record_download", { a.textContent = t("main_menu.record_download", {
size: (blob.size / 1000000).toFixed(2), size: (blob.size / 1000000).toFixed(2),
}); });
targetDiv.appendChild(video);
targetDiv.appendChild(a); targetDiv.appendChild(a);
}; };
} }
function blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = function () {
resolve(reader.result);
};
reader.onerror = function (e) {
console.error(e);
reject(new Error("Failed to readAsDataURL of the video "));
};
reader.readAsDataURL(blob);
});
}
export function pauseRecording() { export function pauseRecording() {
if (!isOptionOn("record")) { if (!isOptionOn("record")) {
return; return;

1
src/types.d.ts vendored
View file

@ -150,7 +150,6 @@ export type PerksMap = {
[k in PerkId]: number; [k in PerkId]: number;
}; };
// TODO ensure T has a destroyed;boolean field
export type ReusableArray<T> = { export type ReusableArray<T> = {
// All items below that index should not be destroyed // All items below that index should not be destroyed
indexMin: number; indexMin: number;