feat: ✨ working prototype
This commit is contained in:
parent
c8d5e899f7
commit
75f6656c38
@ -41,7 +41,6 @@ jobs:
|
||||
- run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run stylelint
|
||||
- run: npm run build
|
||||
|
||||
release:
|
||||
|
@ -9,40 +9,35 @@ use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
|
||||
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
|
||||
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Util;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
private readonly IRootFolder $rootFolder,
|
||||
private readonly IUserSession $userSession
|
||||
) {
|
||||
public function __construct(IRequest $request) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
}
|
||||
|
||||
#[NoCSRFRequired]
|
||||
#[NoAdminRequired]
|
||||
#[FrontpageRoute(verb: 'GET', url: '/{path}', requirements: ['path' => '.+'])]
|
||||
public function run(string $path): TemplateResponse {
|
||||
$user = $this->userSession->getUser();
|
||||
|
||||
if (!$user) {
|
||||
throw new \Exception('User not logged in');
|
||||
}
|
||||
|
||||
$folder = $this->rootFolder->getUserFolder($user->getUID());
|
||||
$folder->get($path);
|
||||
|
||||
#[FrontpageRoute(verb: 'GET', url: '/')]
|
||||
public function run(): TemplateResponse {
|
||||
Util::addScript(Application::APP_ID, Application::APP_ID.'-main');
|
||||
Util::addStyle(Application::APP_ID, Application::APP_ID.'-main');
|
||||
|
||||
return new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
'index',
|
||||
);
|
||||
$csp = new ContentSecurityPolicy();
|
||||
$csp->addAllowedConnectDomain('cdn.emulatorjs.org');
|
||||
$csp->addAllowedConnectDomain('blob:');
|
||||
$csp->addAllowedWorkerSrcDomain('blob:');
|
||||
|
||||
/** @psalm-suppress DeprecatedMethod */
|
||||
$csp->allowEvalScript(true);
|
||||
|
||||
$response = new TemplateResponse(Application::APP_ID, 'index');
|
||||
$response->setContentSecurityPolicy($csp);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
1232
package-lock.json
generated
1232
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,9 +10,7 @@
|
||||
"dev": "vite --mode development build",
|
||||
"watch": "vite --mode development build --watch",
|
||||
"lint": "tsc && eslint src",
|
||||
"lint:fix": "tsc && eslint src --fix",
|
||||
"stylelint": "stylelint src/**/*.scss src/**/*.css",
|
||||
"stylelint:fix": "stylelint src/**/*.scss src/**/*.css --fix"
|
||||
"lint:fix": "tsc && eslint src --fix"
|
||||
},
|
||||
"type": "module",
|
||||
"browserslist": [
|
||||
@ -20,6 +18,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@emulatorjs/emulatorjs": "^4.2.0",
|
||||
"@nextcloud/auth": "^2.4.0",
|
||||
"@nextcloud/files": "^3.10.1",
|
||||
"@nextcloud/initial-state": "^2.2.0",
|
||||
"@nextcloud/l10n": "^3.1.0",
|
||||
@ -31,7 +30,6 @@
|
||||
"@nextcloud/browserslist-config": "^3.0.1",
|
||||
"@nextcloud/eslint-config": "^8.4.1",
|
||||
"@nextcloud/prettier-config": "^1.1.0",
|
||||
"@nextcloud/stylelint-config": "^3.0.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"typescript": "~5.5.4"
|
||||
|
37
src/init.ts
37
src/init.ts
@ -1,25 +1,42 @@
|
||||
import { FileAction, FileType, Node, registerFileAction } from '@nextcloud/files'
|
||||
import {
|
||||
FileAction,
|
||||
FileType,
|
||||
Node,
|
||||
Permission,
|
||||
registerFileAction,
|
||||
} from '@nextcloud/files'
|
||||
import cores from './mime.json'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
||||
const getCore = (file: Node) => {
|
||||
if (
|
||||
!file.fileid ||
|
||||
file.type !== FileType.File ||
|
||||
!(file.permissions & Permission.READ)
|
||||
)
|
||||
return false
|
||||
const extension = file.extension?.substring(1)
|
||||
for (const core in cores) {
|
||||
if (cores[core].types.includes(file.mime)) return core
|
||||
if (cores[core].extensions.includes(extension)) return core
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const action = new FileAction({
|
||||
id: 'retro',
|
||||
displayName: () => t('repod', 'Play'),
|
||||
iconSvgInline: () => '',
|
||||
enabled: (files: Node[]) => {
|
||||
if (files.length !== 1) return false
|
||||
const file = files[0]
|
||||
if (!file.fileid || file.type !== FileType.File) return false
|
||||
const extension = file.extension?.substring(1)
|
||||
for (const core in cores) {
|
||||
if (cores[core].types.includes(file.mime)) return true
|
||||
if (cores[core].extensions.includes(extension)) return true
|
||||
}
|
||||
return false
|
||||
return !!getCore(files[0])
|
||||
},
|
||||
exec: async (file: Node) => {
|
||||
location.href = generateUrl(`/apps/retro/${file.path}`)
|
||||
location.href = generateUrl(`/apps/retro/?core={core}&file={file}`, {
|
||||
core: getCore(file),
|
||||
file: file.encodedSource,
|
||||
})
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
323
src/main.ts
323
src/main.ts
@ -0,0 +1,323 @@
|
||||
import '@emulatorjs/emulatorjs/data/src/emulator.js'
|
||||
import '@emulatorjs/emulatorjs/data/src/nipplejs.js'
|
||||
import '@emulatorjs/emulatorjs/data/src/shaders.js'
|
||||
import '@emulatorjs/emulatorjs/data/src/storage.js'
|
||||
import '@emulatorjs/emulatorjs/data/src/gamepad.js'
|
||||
import '@emulatorjs/emulatorjs/data/src/GameManager.js'
|
||||
import '@emulatorjs/emulatorjs/data/src/socket.io.min.js'
|
||||
import '@emulatorjs/emulatorjs/data/src/compression.js'
|
||||
import '@emulatorjs/emulatorjs/data/emulator.css'
|
||||
|
||||
import af from '@emulatorjs/emulatorjs/data/localization/af-FR.json'
|
||||
import ar from '@emulatorjs/emulatorjs/data/localization/ar-AR.json'
|
||||
import ben from '@emulatorjs/emulatorjs/data/localization/ben-BEN.json'
|
||||
import de from '@emulatorjs/emulatorjs/data/localization/de-GER.json'
|
||||
import el from '@emulatorjs/emulatorjs/data/localization/el-GR.json'
|
||||
import en from '@emulatorjs/emulatorjs/data/localization/en.json'
|
||||
import es from '@emulatorjs/emulatorjs/data/localization/es-ES.json'
|
||||
import fa from '@emulatorjs/emulatorjs/data/localization/fa-AF.json'
|
||||
import { getRequestToken } from '@nextcloud/auth'
|
||||
import hi from '@emulatorjs/emulatorjs/data/localization/hi-HI.json'
|
||||
import it from '@emulatorjs/emulatorjs/data/localization/it-IT.json'
|
||||
import ja from '@emulatorjs/emulatorjs/data/localization/ja-JA.json'
|
||||
import jv from '@emulatorjs/emulatorjs/data/localization/jv-JV.json'
|
||||
import ko from '@emulatorjs/emulatorjs/data/localization/ko-KO.json'
|
||||
import pt from '@emulatorjs/emulatorjs/data/localization/pt-BR.json'
|
||||
import ru from '@emulatorjs/emulatorjs/data/localization/ru-RU.json'
|
||||
import tr from '@emulatorjs/emulatorjs/data/localization/tr-TR.json'
|
||||
import vi from '@emulatorjs/emulatorjs/data/localization/vi-VN.json'
|
||||
import zh from '@emulatorjs/emulatorjs/data/localization/zh-CN.json'
|
||||
|
||||
type Config = {
|
||||
gameUrl?: string
|
||||
dataPath?: string
|
||||
system?: string
|
||||
biosUrl?: string
|
||||
gameName?: string
|
||||
color?: string
|
||||
adUrl?: string
|
||||
adMode?: 0 | 1 | 2
|
||||
adTimer?: number
|
||||
adSize?: string[]
|
||||
alignStartButton?: 'top' | 'center' | 'bottom'
|
||||
VirtualGamepadSettings?: object
|
||||
buttonOpts?: { [key: string]: boolean }
|
||||
volume?: number
|
||||
defaultControllers?: { [key: number]: { [key: number]: string } }
|
||||
startOnLoad?: boolean
|
||||
fullscreenOnLoad?: boolean
|
||||
filePaths?: { [key: string]: string }
|
||||
loadState?: string
|
||||
cacheLimit?: number
|
||||
cheats?: { [key: string]: string }
|
||||
defaultOptions?: { [key: string]: string }
|
||||
gamePatchUrl?: string
|
||||
gameParentUrl?: string
|
||||
netplayUrl?: string
|
||||
gameId?: number
|
||||
backgroundImg?: string
|
||||
backgroundBlur?: boolean
|
||||
backgroundColor?: string
|
||||
controlScheme?:
|
||||
| 'nes'
|
||||
| 'gb'
|
||||
| 'gba'
|
||||
| 'snes'
|
||||
| 'n64'
|
||||
| 'gba'
|
||||
| 'nds'
|
||||
| 'vb'
|
||||
| 'segaMD'
|
||||
| 'segaCD'
|
||||
| 'sega32x'
|
||||
| 'segaMS'
|
||||
| 'segaGG'
|
||||
| 'segaSaturn'
|
||||
| '3do'
|
||||
| 'atari2600'
|
||||
| 'atari7800'
|
||||
| 'lynx'
|
||||
| 'jaguar'
|
||||
| 'arcade'
|
||||
| 'mame'
|
||||
threads?: boolean
|
||||
disableCue?: boolean
|
||||
startBtnName?: string
|
||||
softLoad?: number
|
||||
screenRecording?: { [key: string]: number }
|
||||
externalFiles?: { [key: string]: string }
|
||||
disableDatabases?: boolean
|
||||
disableLocalStorage?: boolean
|
||||
forceLegacyCores?: boolean
|
||||
noAutoFocus?: boolean
|
||||
videoRotation?: 0 | 1 | 2 | 3
|
||||
shaders?: { [key: string]: string }
|
||||
language?:
|
||||
| 'af'
|
||||
| 'ar'
|
||||
| 'ben'
|
||||
| 'de'
|
||||
| 'el'
|
||||
| 'en'
|
||||
| 'es'
|
||||
| 'fa'
|
||||
| 'hi'
|
||||
| 'it'
|
||||
| 'ja'
|
||||
| 'jv'
|
||||
| 'ko'
|
||||
| 'pt'
|
||||
| 'ru'
|
||||
| 'tr'
|
||||
| 'vi'
|
||||
| 'zh'
|
||||
langJson?: { [key: string]: string }
|
||||
}
|
||||
|
||||
const config: Config = {}
|
||||
|
||||
switch (document.documentElement.lang) {
|
||||
case 'af':
|
||||
config.language = 'af'
|
||||
config.langJson = af
|
||||
break
|
||||
case 'ar':
|
||||
config.language = 'ar'
|
||||
config.langJson = ar
|
||||
break
|
||||
case 'ben':
|
||||
config.language = 'ben'
|
||||
config.langJson = ben
|
||||
break
|
||||
case 'de':
|
||||
config.language = 'de'
|
||||
config.langJson = de
|
||||
break
|
||||
case 'el':
|
||||
config.language = 'el'
|
||||
config.langJson = el
|
||||
break
|
||||
case 'es':
|
||||
config.language = 'es'
|
||||
config.langJson = es
|
||||
break
|
||||
case 'fa':
|
||||
config.language = 'fa'
|
||||
config.langJson = fa
|
||||
break
|
||||
case 'hi':
|
||||
config.language = 'hi'
|
||||
config.langJson = hi
|
||||
break
|
||||
case 'it':
|
||||
config.language = 'it'
|
||||
config.langJson = it
|
||||
break
|
||||
case 'ja':
|
||||
config.language = 'ja'
|
||||
config.langJson = ja
|
||||
break
|
||||
case 'jv':
|
||||
config.language = 'jv'
|
||||
config.langJson = jv
|
||||
break
|
||||
case 'ko':
|
||||
config.language = 'ko'
|
||||
config.langJson = ko
|
||||
break
|
||||
case 'pt':
|
||||
config.language = 'pt'
|
||||
config.langJson = pt
|
||||
break
|
||||
case 'ru':
|
||||
config.language = 'ru'
|
||||
config.langJson = ru
|
||||
break
|
||||
case 'tr':
|
||||
config.language = 'tr'
|
||||
config.langJson = tr
|
||||
break
|
||||
case 'vi':
|
||||
config.language = 'vi'
|
||||
config.langJson = vi
|
||||
break
|
||||
case 'zh':
|
||||
config.language = 'zh'
|
||||
config.langJson = zh
|
||||
break
|
||||
default:
|
||||
config.language = 'en'
|
||||
config.langJson = en
|
||||
}
|
||||
|
||||
interface EmulatorJS {
|
||||
new (element: string, config: Config)
|
||||
downloadFile(path, progressCB, notWithPath, opts)
|
||||
on(event: string, func: () => void)
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
EmulatorJS: EmulatorJS
|
||||
}
|
||||
}
|
||||
|
||||
class MyEmulatorJS extends window.EmulatorJS {
|
||||
downloadFile(path, progressCB, notWithPath, opts) {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve) => {
|
||||
const data = this.toData(path) // check other data types
|
||||
if (data) {
|
||||
data.then((game) => {
|
||||
if (opts.method === 'HEAD') {
|
||||
resolve({ headers: {} })
|
||||
} else {
|
||||
resolve({ headers: {}, data: game })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
const basePath = notWithPath ? '' : this.config.dataPath
|
||||
path = basePath + path
|
||||
if (
|
||||
!notWithPath &&
|
||||
this.config.filePaths &&
|
||||
typeof this.config.filePaths[path.split('/').pop()] === 'string'
|
||||
) {
|
||||
path = this.config.filePaths[path.split('/').pop()]
|
||||
}
|
||||
let url
|
||||
try {
|
||||
url = new URL(path)
|
||||
} catch (e) {}
|
||||
if (url && !['http:', 'https:'].includes(url.protocol)) {
|
||||
// Most commonly blob: urls. Not sure what else it could be
|
||||
if (opts.method === 'HEAD') {
|
||||
resolve({ headers: {} })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await fetch(path)
|
||||
let ret
|
||||
if (
|
||||
(opts.type && opts.type.toLowerCase() === 'arraybuffer') ||
|
||||
!opts.type
|
||||
) {
|
||||
ret = await res.arrayBuffer()
|
||||
} else {
|
||||
ret = await res.text()
|
||||
try {
|
||||
ret = JSON.parse(ret)
|
||||
} catch (e) {}
|
||||
}
|
||||
if (path.startsWith('blob:')) URL.revokeObjectURL(path)
|
||||
resolve({ data: ret, headers: {} })
|
||||
} catch (e) {
|
||||
resolve(-1)
|
||||
}
|
||||
return
|
||||
}
|
||||
const xhr = new XMLHttpRequest()
|
||||
if (progressCB instanceof Function) {
|
||||
xhr.addEventListener('progress', (e) => {
|
||||
const progress = e.total
|
||||
? ' ' +
|
||||
Math.floor((e.loaded / e.total) * 100).toString() +
|
||||
'%'
|
||||
: ' ' + (e.loaded / 1048576).toFixed(2) + 'MB'
|
||||
progressCB(progress)
|
||||
})
|
||||
}
|
||||
xhr.onload = function () {
|
||||
if (xhr.readyState === xhr.DONE) {
|
||||
let data = xhr.response
|
||||
if (
|
||||
xhr.status.toString().startsWith('4') ||
|
||||
xhr.status.toString().startsWith('5')
|
||||
) {
|
||||
resolve(-1)
|
||||
return
|
||||
}
|
||||
try {
|
||||
data = JSON.parse(data)
|
||||
} catch (e) {}
|
||||
resolve({
|
||||
data,
|
||||
headers: {
|
||||
'content-length':
|
||||
xhr.getResponseHeader('content-length'),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
if (opts.responseType) xhr.responseType = opts.responseType
|
||||
xhr.onerror = () => resolve(-1)
|
||||
xhr.open(opts.method, path, true)
|
||||
// => PATCH <=
|
||||
if (path.includes(location.host)) {
|
||||
xhr.setRequestHeader('requesttoken', getRequestToken() || '')
|
||||
}
|
||||
// => PATCH <=
|
||||
xhr.send()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const gameUrl = new URL(location.href).searchParams.get('file')
|
||||
|
||||
if (gameUrl) {
|
||||
config.gameUrl = gameUrl
|
||||
}
|
||||
|
||||
const system = new URL(location.href).searchParams.get('core')
|
||||
|
||||
if (system) {
|
||||
config.system = system
|
||||
}
|
||||
|
||||
config.dataPath = 'https://cdn.emulatorjs.org/stable/data/'
|
||||
config.startOnLoad = true
|
||||
config.threads = !!window.SharedArrayBuffer
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new MyEmulatorJS('#game', config)
|
@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
extends: 'stylelint-config-recommended-vue',
|
||||
}
|
@ -1 +1 @@
|
||||
<div id="retro"></div>
|
||||
<div id="game"></div>
|
||||
|
@ -10,8 +10,8 @@ const config = defineConfig({
|
||||
|
||||
export default createAppConfig(
|
||||
{
|
||||
main: resolve(join('src', 'main.ts')),
|
||||
init: resolve(join('src', 'init.ts')),
|
||||
main: resolve(join('src', 'main.ts')),
|
||||
},
|
||||
{
|
||||
config,
|
||||
|
Loading…
x
Reference in New Issue
Block a user