added project files
This commit is contained in:
parent
b3d759b8f7
commit
33e18d8f2a
9
app/build.hxml
Normal file
9
app/build.hxml
Normal file
@ -0,0 +1,9 @@
|
||||
-lib haxe-loader
|
||||
-lib react
|
||||
-lib react-router-4
|
||||
|
||||
-cp src
|
||||
-js app.js
|
||||
-main App
|
||||
|
||||
-D react_hot
|
||||
14
app/hmm.json
Normal file
14
app/hmm.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"dependencies": [
|
||||
{
|
||||
"name": "haxe-loader",
|
||||
"type": "haxelib",
|
||||
"version": "0.9.0"
|
||||
},
|
||||
{
|
||||
"name": "react",
|
||||
"type": "haxelib",
|
||||
"version": "1.4.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
25
app/package.json
Normal file
25
app/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "webpack-haxe-example",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-loader": "^2.0.2",
|
||||
"file-loader": "^3.0.1",
|
||||
"haxe-loader": "^0.9.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.7.0",
|
||||
"react-dom": "^16.7.0",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"webpack": "^4.28.2",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.1.12"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server",
|
||||
"build:dev": "webpack",
|
||||
"build": "webpack"
|
||||
}
|
||||
}
|
||||
131
app/readme.md
Normal file
131
app/readme.md
Normal file
@ -0,0 +1,131 @@
|
||||
# Modular Haxe JS with Webpack
|
||||
|
||||
This project demonstrates the creation of a Haxe-JavaScript modular project leveraging Webpack
|
||||
for bundling (code and assets) and lazy loading.
|
||||
|
||||
It's really easy and absolutely transparent in the code!
|
||||
|
||||
**Note: this is a "vanilla DOM" example - no 3rd party library involved!**
|
||||
|
||||
|
||||
## How it works
|
||||
|
||||
### Leveraging Webpack features
|
||||
|
||||
You will need to get familiar with how Webpack works - thankfully the documentation
|
||||
is excellent nowadays: [https://webpack.js.org/](https://webpack.js.org/)
|
||||
|
||||
A project aims at creating a Webpack loader for Haxe:
|
||||
[https://github.com/jasononeil/webpack-haxe-loader](https://github.com/jasononeil/webpack-haxe-loader)
|
||||
|
||||
Every asset dependency should be explictely required, so Webpack knows what to include
|
||||
in the output folder. With the right configuration, small assets can even be inlined in
|
||||
the bundle to reduce the number of requests.
|
||||
|
||||
```haxe
|
||||
Webpack.require('./index.css');
|
||||
|
||||
var img = new Image();
|
||||
img.src = Webpack.require('./logo.png');
|
||||
```
|
||||
|
||||
Even the HTML page is generated by a plugin, so everything has to go through Webpack.
|
||||
|
||||
### Haxe JS code splitting
|
||||
|
||||
Haxe JS isn't normally capable of code splitting, but the core functionality was
|
||||
developped within the (Webpack-free) Haxe Modular project:
|
||||
[https://github.com/elsassph/haxe-modular](https://github.com/elsassph/haxe-modular)
|
||||
|
||||
The Haxe Webpack loader will leverage the code splitting feature when you request
|
||||
modules asynchronously:
|
||||
|
||||
```haxe
|
||||
import com.Foo;
|
||||
...
|
||||
// Extract Foo (and dependencies) into a separate bundle
|
||||
Webpack.load(Foo).then(function(_) {
|
||||
// Foo is now loaded
|
||||
var foo = new Foo();
|
||||
});
|
||||
```
|
||||
|
||||
### Webpack config
|
||||
|
||||
Webpack's "magic" is configured in the `webpack.config.js`. It is a very powerful and
|
||||
flexible system which is documented here: [https://webpack.js.org/](https://webpack.js.org/)
|
||||
|
||||
The basics is that the `haxe-loader` allow to "require" an
|
||||
[HXML file](https://haxe.org/manual/compiler-usage-hxml.html),
|
||||
which in turn will provide the (splitted) JS output to Webpack.
|
||||
|
||||
This feature is added as a "rule" in the config:
|
||||
```
|
||||
{
|
||||
test: /\.hxml$/,
|
||||
loader: 'haxe-loader',
|
||||
options: {
|
||||
extra: `-D some_extra=arguments`,
|
||||
debug: debugMode
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
HXMLs can be require directly from JS code, or as an entry point, which allows to
|
||||
make a pure Haxe Webpack project:
|
||||
```
|
||||
entry: {
|
||||
app: './build.hxml'
|
||||
},
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Tools
|
||||
|
||||
Using [yarn](https://yarnpkg.com) for node modules is recommended:
|
||||
|
||||
npm install yarn -g
|
||||
|
||||
Using [hmm](https://github.com/andywhite37/hmm) for haxelibs is recommended:
|
||||
|
||||
haxelib --global install hmm
|
||||
haxelib --global run hmm setup
|
||||
|
||||
### Installation
|
||||
|
||||
Install npm and haxe dependencies:
|
||||
|
||||
yarn install
|
||||
hmm install
|
||||
|
||||
### Running
|
||||
|
||||
Then start Webpack webserver, open `http://localhost:9000`, and enjoy live reload:
|
||||
|
||||
yarn start
|
||||
|
||||
### Hot Module Replacement
|
||||
|
||||
Hot-Module Replacement is the technique allowing, while your browser is showing your
|
||||
live application, to hot-reload CSS and the Haxe modules!
|
||||
|
||||
React views in asynchronous modules will automatically refresh themselves if you edit
|
||||
and save: Webpack will recompile the project, reload the module, and re-render the
|
||||
views without losing state. un and try editing `Foo.hx`!
|
||||
|
||||
For that you only need to:
|
||||
- add `-D react_hot` in your `hxml`,
|
||||
- call `ReactHMR.autoRefresh` after the main render (see `App.hx`),
|
||||
- run in live debug mode (`yarn start`).
|
||||
|
||||
### Releasing
|
||||
|
||||
To build the project statically, run:
|
||||
|
||||
yarn build
|
||||
|
||||
For a production release:
|
||||
|
||||
export NODE_ENV=production
|
||||
yarn build -p
|
||||
15
app/src/App.css
Normal file
15
app/src/App.css
Normal file
@ -0,0 +1,15 @@
|
||||
body {
|
||||
font-family: Arial;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
/* margin: 10px; */
|
||||
}
|
||||
|
||||
main{
|
||||
margin: 20px;
|
||||
background-color: #eee;
|
||||
min-height: 300px;
|
||||
padding: 10px;
|
||||
}
|
||||
43
app/src/App.hx
Normal file
43
app/src/App.hx
Normal file
@ -0,0 +1,43 @@
|
||||
import react.ReactMacro.jsx;
|
||||
import react.router.ReactRouter;
|
||||
import react.router.BrowserRouter;
|
||||
import react.router.Route;
|
||||
import react.router.Switch;
|
||||
import react.router.bundle.Bundle;
|
||||
import Webpack.*;
|
||||
import Root;
|
||||
|
||||
|
||||
class App {
|
||||
static var STYLES = require('./App.css');
|
||||
static var Rewt = ReactRouter.withRouter(Root);
|
||||
static public function main() {
|
||||
new App();
|
||||
}
|
||||
|
||||
public function new() {
|
||||
var root = createRoot();
|
||||
|
||||
var rootComponent = react.ReactDOM.render(jsx('
|
||||
<$BrowserRouter>
|
||||
<$Switch>
|
||||
<$Rewt/>
|
||||
</$Switch>
|
||||
</$BrowserRouter>
|
||||
|
||||
'), root);
|
||||
|
||||
#if debug
|
||||
ReactHMR.autoRefresh(rootComponent);
|
||||
#end
|
||||
}
|
||||
|
||||
function createRoot() {
|
||||
var current = js.Browser.document.getElementById('root');
|
||||
if (current != null) return current;
|
||||
current = Dom.div();
|
||||
current.id = 'root';
|
||||
Dom.body().appendChild(current);
|
||||
return current;
|
||||
}
|
||||
}
|
||||
16
app/src/Dom.hx
Normal file
16
app/src/Dom.hx
Normal file
@ -0,0 +1,16 @@
|
||||
class Dom {
|
||||
static var TEMP = js.Browser.document.createDivElement();
|
||||
|
||||
inline static public function div() {
|
||||
return js.Browser.document.createDivElement();
|
||||
}
|
||||
|
||||
inline static public function html(html: String) {
|
||||
TEMP.innerHTML = html;
|
||||
return TEMP.firstElementChild;
|
||||
}
|
||||
|
||||
inline static public function body() {
|
||||
return js.Browser.document.body;
|
||||
}
|
||||
}
|
||||
83
app/src/Root.hx
Normal file
83
app/src/Root.hx
Normal file
@ -0,0 +1,83 @@
|
||||
import com.Foo;
|
||||
import com.Foo2;
|
||||
import components.Header;
|
||||
import components.HomeContent;
|
||||
import react.ReactMacro.jsx;
|
||||
import react.ReactComponent;
|
||||
import react.React.CreateElementType;
|
||||
import react.router.ReactRouter;
|
||||
import react.router.Route.RouteRenderProps;
|
||||
import react.router.Route;
|
||||
|
||||
|
||||
private typedef RootState = {
|
||||
route: String,
|
||||
?component: react.React.CreateElementType
|
||||
}
|
||||
|
||||
private typedef RootProps = {
|
||||
> RouteRenderProps,
|
||||
}
|
||||
|
||||
class Root extends react.ReactComponentOf<RootProps,RootState> {
|
||||
|
||||
public function new() {
|
||||
super();
|
||||
state = { route:'' };
|
||||
}
|
||||
|
||||
override function componentDidMount() {
|
||||
trace("wattfak");
|
||||
switch (state.route) {
|
||||
default:
|
||||
Webpack.load(Foo).then(function(_) {
|
||||
setState(cast { component:Foo });
|
||||
trace("foo");
|
||||
});
|
||||
}
|
||||
}
|
||||
function yeet(){
|
||||
//state.route="yeet";
|
||||
//trace(state);
|
||||
//setState({route:"yeetnt"});
|
||||
}
|
||||
|
||||
override function render() {
|
||||
return jsx('
|
||||
<div>
|
||||
<Header/>
|
||||
<!--Current path is ${props.location.pathname} and component is ${state.route}-->
|
||||
<!--${renderContent()}-->
|
||||
<main>
|
||||
<$Route exact="true" path="/">
|
||||
<HomeContent/>
|
||||
</$Route>
|
||||
<$Route path="/projects">
|
||||
<h1>Projects</h1>
|
||||
</$Route>
|
||||
<$Route path="/links">
|
||||
<h1>Links</h1>
|
||||
</$Route>
|
||||
<$Route path="/gameservers">
|
||||
<h1>Game Servers</h1>
|
||||
<p></p>
|
||||
</$Route>
|
||||
</main>
|
||||
</div>
|
||||
');
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
if(state.route != props.location.pathname){
|
||||
state.route = props.location.pathname;
|
||||
}
|
||||
if (state.component == null)
|
||||
return jsx('
|
||||
<span>Loading...</span>
|
||||
');
|
||||
else
|
||||
return jsx('
|
||||
<state.component />
|
||||
');
|
||||
}
|
||||
}
|
||||
12
app/src/bundles/MyBundle.hx
Normal file
12
app/src/bundles/MyBundle.hx
Normal file
@ -0,0 +1,12 @@
|
||||
import react.ReactComponent;
|
||||
import react.router.Route.RouteRenderProps;
|
||||
|
||||
@:expose('default')
|
||||
class MyBundle extends ReactComponentOfProps<RouteRenderProps> {
|
||||
// If you want to execute code when this bundle is _first_ loaded:
|
||||
public static function onLoad() {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
14
app/src/com/Foo.css
Normal file
14
app/src/com/Foo.css
Normal file
@ -0,0 +1,14 @@
|
||||
.foo {
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.foo .yeah {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.foo .yeah p {
|
||||
margin: 0;
|
||||
border-bottom: solid 1px red;
|
||||
}
|
||||
27
app/src/com/Foo.hx
Normal file
27
app/src/com/Foo.hx
Normal file
@ -0,0 +1,27 @@
|
||||
package com;
|
||||
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Webpack.*;
|
||||
|
||||
class Foo extends ReactComponent {
|
||||
|
||||
static var STYLES = require('./Foo.css');
|
||||
static var IMG = require('./bug.png');
|
||||
static var CONFIG = require('../config.json');
|
||||
|
||||
public function yeet(){
|
||||
trace(state);
|
||||
}
|
||||
|
||||
override function render() {
|
||||
return jsx('
|
||||
<div className="foo">
|
||||
<img src=$IMG/> ${CONFIG.hello}!
|
||||
<p onClick=${yeet}> ${CONFIG.yeet}!</p>
|
||||
<hr/>
|
||||
Let\'s do some HRM guys<br/>
|
||||
</div>
|
||||
');
|
||||
}
|
||||
}
|
||||
24
app/src/com/Foo2.hx
Normal file
24
app/src/com/Foo2.hx
Normal file
@ -0,0 +1,24 @@
|
||||
package com;
|
||||
|
||||
import react.ReactComponent;
|
||||
import react.ReactMacro.jsx;
|
||||
import Webpack.*;
|
||||
|
||||
class Foo2 extends ReactComponent {
|
||||
|
||||
static var STYLES = require('./Foo.css');
|
||||
static var IMG = require('./bug.png');
|
||||
static var CONFIG = require('../config.json');
|
||||
|
||||
|
||||
override function render() {
|
||||
return jsx('
|
||||
<div className="foo2">
|
||||
nooo
|
||||
<img src=$IMG/> ${CONFIG.hello}!
|
||||
<hr/>
|
||||
Let\'s do some HRM guys<br/>
|
||||
</div>
|
||||
');
|
||||
}
|
||||
}
|
||||
BIN
app/src/com/bug.png
Normal file
BIN
app/src/com/bug.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 774 B |
14
app/src/components/Header.css
Normal file
14
app/src/components/Header.css
Normal file
@ -0,0 +1,14 @@
|
||||
header {
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
background: #eee;
|
||||
}
|
||||
header nav a{
|
||||
margin-right: 10px;
|
||||
}
|
||||
.selected{
|
||||
color:red;
|
||||
}
|
||||
.logo{
|
||||
font-size: 3vw;
|
||||
}
|
||||
36
app/src/components/Header.hx
Normal file
36
app/src/components/Header.hx
Normal file
@ -0,0 +1,36 @@
|
||||
package components;
|
||||
|
||||
import react.ReactMacro.jsx;
|
||||
import react.ReactComponent;
|
||||
import Webpack.*;
|
||||
import react.router.NavLink;
|
||||
|
||||
class Header extends ReactComponent{
|
||||
static var STYLES = require('./Header.css');
|
||||
public function yeet(){
|
||||
props.foo();
|
||||
}
|
||||
override function render() {
|
||||
return jsx('
|
||||
<header>
|
||||
<div className="logo">
|
||||
subsonics
|
||||
</div>
|
||||
<nav>
|
||||
<$NavLink exact="true" to="/" activeClassName="selected">
|
||||
Home
|
||||
</NavLink>
|
||||
<$NavLink to="/links" activeClassName="selected">
|
||||
Links
|
||||
</NavLink>
|
||||
<$NavLink to="/projects" activeClassName="selected">
|
||||
Projects
|
||||
</NavLink>
|
||||
<$NavLink to="/gameservers" activeClassName="selected">
|
||||
Game Servers
|
||||
</NavLink>
|
||||
</nav>
|
||||
</header>
|
||||
');
|
||||
}
|
||||
}
|
||||
0
app/src/components/HomeContent.css
Normal file
0
app/src/components/HomeContent.css
Normal file
22
app/src/components/HomeContent.hx
Normal file
22
app/src/components/HomeContent.hx
Normal file
@ -0,0 +1,22 @@
|
||||
package components;
|
||||
import react.ReactMacro.jsx;
|
||||
import react.ReactComponent;
|
||||
import js.Browser;
|
||||
import Webpack.*;
|
||||
|
||||
class HomeContent extends ReactComponent {
|
||||
|
||||
static var STYLES = require('./HomeContent.css');
|
||||
|
||||
public function yeet(){
|
||||
trace(state);
|
||||
}
|
||||
|
||||
override public function render() {
|
||||
return jsx('
|
||||
<p onClick=${yeet}>
|
||||
kak
|
||||
</p>
|
||||
');
|
||||
}
|
||||
}
|
||||
34
app/src/components/MainRouter.hx
Normal file
34
app/src/components/MainRouter.hx
Normal file
@ -0,0 +1,34 @@
|
||||
package components;
|
||||
|
||||
import react.ReactMacro.jsx;
|
||||
import react.ReactComponent;
|
||||
import react.router.BrowserRouter;
|
||||
import react.router.Route;
|
||||
import react.router.Switch;
|
||||
import react.router.bundle.Bundle;
|
||||
|
||||
class MainRouter extends ReactComponent {
|
||||
override public function render() {
|
||||
return jsx('
|
||||
<$BrowserRouter>
|
||||
<$Switch>
|
||||
<!-- Using default loader component (`<div className="loader" />`) -->
|
||||
<!-- and default error component (`<div className="error" />`) -->
|
||||
<!-- /!\\ Warning: your component should have the `@:expose("default")` meta -->
|
||||
<!-- See example below in "Bundle initialization code" -->
|
||||
<$Route
|
||||
path="/bundle1"
|
||||
component=${Bundle.load(first.FirstBundle)}
|
||||
/>
|
||||
|
||||
<!-- Using custom loader and/or error component -->
|
||||
<!-- The error component will get an `error` prop with the load error as `Dynamic` -->
|
||||
<$Route
|
||||
path="/bundle2"
|
||||
component=${Bundle.load(second.SecondBundle, CustomLoader, CustomError)}
|
||||
/>
|
||||
</$Switch>
|
||||
</$BrowserRouter>
|
||||
');
|
||||
}
|
||||
}
|
||||
4
app/src/config.json
Normal file
4
app/src/config.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"hello": "This is an asynchronous module" ,
|
||||
"yeet": "yote"
|
||||
}
|
||||
122
app/webpack.config.js
Normal file
122
app/webpack.config.js
Normal file
@ -0,0 +1,122 @@
|
||||
//
|
||||
// Webpack documentation is fairly extensive,
|
||||
// just search on https://webpack.js.org/
|
||||
//
|
||||
// Be careful: there are a lot of outdated examples/samples,
|
||||
// so always check the official documentation!
|
||||
//
|
||||
|
||||
// Plugins
|
||||
const webpack = require('webpack');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
// Options
|
||||
const buildMode = process.env.NODE_ENV || 'development';
|
||||
const debugMode = buildMode !== 'production';
|
||||
const dist = __dirname + '/www/';
|
||||
|
||||
// Sourcemaps: https://webpack.js.org/configuration/devtool/
|
||||
// - 'cheap-module-source-map': fastest in Haxe-only setup
|
||||
// - 'eval-source-map': fast, but JS bundle is somewhat obfuscated
|
||||
// - 'source-map': slow, but JS bundle is readable
|
||||
// - undefined: no map, and JS bundle is readable
|
||||
const sourcemapsMode = debugMode ? 'cheap-module-source-map' : undefined;
|
||||
|
||||
//
|
||||
// Configuration:
|
||||
// This configuration is still relatively minimalistic;
|
||||
// each section has many more options
|
||||
//
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
// List all the JS modules to create
|
||||
// They will all be linked in the HTML page
|
||||
entry: {
|
||||
app: './build.hxml'
|
||||
},
|
||||
// Generation options (destination, naming pattern,...)
|
||||
output: {
|
||||
path: dist,
|
||||
publicPath: '/',
|
||||
filename: '[name].[hash:7].js'
|
||||
},
|
||||
// Module resolution options (alias, default paths,...)
|
||||
resolve: {
|
||||
extensions: ['.js', '.json']
|
||||
},
|
||||
// Sourcemaps option for development
|
||||
devtool: sourcemapsMode,
|
||||
// Live development server (serves from memory)
|
||||
devServer: {
|
||||
contentBase: dist,
|
||||
compress: true,
|
||||
host: "0.0.0.0",
|
||||
inline: true,
|
||||
historyApiFallback: true,
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization"},
|
||||
public: "https://dev.subsonics.nl",
|
||||
port: 9000,
|
||||
overlay: true,
|
||||
hot: true,
|
||||
disableHostCheck: true
|
||||
},
|
||||
// List all the processors
|
||||
module: {
|
||||
rules: [
|
||||
// Haxe loader (through HXML files for now)
|
||||
{
|
||||
test: /\.hxml$/,
|
||||
loader: 'haxe-loader',
|
||||
options: {
|
||||
// Additional compiler options added to all builds
|
||||
extra: '-D build_mode=' + buildMode,
|
||||
debug: debugMode,
|
||||
logCommand: true
|
||||
}
|
||||
},
|
||||
// Static assets loader
|
||||
// - you will need to adjust for webfonts
|
||||
// - you may use 'url-loader' instead which can replace
|
||||
// small assets with data-urls
|
||||
{
|
||||
test: /\.(gif|png|jpg|svg)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[hash:7].[ext]'
|
||||
}
|
||||
},
|
||||
// CSS processor/loader
|
||||
// - this is where you can add sass/less processing,
|
||||
// - also consider adding postcss-loader for autoprefixing
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
// Plugins can hook to the compiler lifecycle and handle extra tasks
|
||||
plugins: [
|
||||
// HMR: enable globally
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
// HMR: prints more readable module names in the browser console on updates
|
||||
new webpack.NamedModulesPlugin(),
|
||||
// HMR: do not emit compiled assets that include errors
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
|
||||
// Like generating the HTML page with links the generated JS files
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Webpack + Haxe example'
|
||||
})
|
||||
// You may want to also:
|
||||
// - finer control of minify/uglify process using UglifyJSPlugin,
|
||||
// - extract the small CSS chunks into a single file using ExtractTextPlugin
|
||||
// - avoid modules duplication using CommonsChunkPlugin
|
||||
// - inspect your JS output weight using BundleAnalyzerPlugin
|
||||
],
|
||||
};
|
||||
4435
app/yarn.lock
Normal file
4435
app/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user