Prosty przykład w React od zera

React to biblioteka JavaScript, stworzona w 2013 roku i rozwijana przez Facebook. Fakt, że stoi za nią ogromna korporacja ma swoje wady i zalety, z jednej strony możemy być pewni, że pracują nad nią świetni programiści i wszystkie decyzje dotyczące jej rozwoju są podejmowane przez profesjonalistów, z drugiej jednak możemy mieć pewne opory moralne ze względu na specyficzne środki bezpieczeństwa związane z licencjonowaniem, o których niedawno było dość głośno.

Pomijając jednak sprawy polityczne, spróbujemy stworzyć od zera prostą aplikację wyświetlającą tabelkę z listą kryptowalut z Bittrex. Projekt w zasadzie nikomu do niczego nie potrzebny, ale jako pierwszy przykład z pewnością wystarczy.

Wykorzystujemy:

  • React v15.6
  • Express v4.15
  • Babel v6.26
  • Webpack v3.5
  • Axios v0.16
  • Bootstrap v3.3

Inicjalizacja projektu

Zaczynamy od klasycznego „npm init” i generujemy nowy plik package.json.

{
  "name": "rekt",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Następnie pobieramy potrzebne biblioteki:

npm install --save-dev axios babel-cli babel-core babel-loader babel-preset-env babel-preset-react bootstrap react react-dom webpack
npm install --save express

Może się wydawać dziwne dodawanie Bootstrapa do devDependiences, ale dla uproszczenia skopiujemy z niego tylko zawartość katalogu dist i nie będzie nam później potrzebny (opcjonalnie można wykorzystać CDN).

Do package.json dopisujemy linijki odpowiedzialne za skrypty i pluginy babel:

{
  "name": "rekt",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "DEBUG=express:* node index.js",
    "build": "webpack -p",
    "watch": "webpack --progress --watch",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "axios": "^0.16.2",
    "babel-cli": "^6.26.0",
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-env": "^1.6.0",
    "babel-preset-react": "^6.24.1",
    "bootstrap": "^3.3.7",
    "react": "^15.6.1",
    "react-dom": "^15.6.1",
    "webpack": "^3.5.6"
  },
  "dependencies": {
    "express": "^4.15.4"
  },
  "babel": {
    "presets": [
      "env",
      "react"
    ]
  }
}

Tworzymy katalogi i kopiujemy Bootstrapa:

.
├── node_modules/
│   └── (bibliteki z npm)
├── package.json
├── public/
│   ├── css/
│   │   ├── bootstrap.css
│   │   ├── bootstrap.css.map
│   │   ├── bootstrap.min.css
│   │   ├── bootstrap.min.css.map
│   │   ├── bootstrap-theme.css
│   │   ├── bootstrap-theme.css.map
│   │   ├── bootstrap-theme.min.css
│   │   └── bootstrap-theme.min.css.map
│   ├── fonts/
│   │   ├── glyphicons-halflings-regular.eot
│   │   ├── glyphicons-halflings-regular.svg
│   │   ├── glyphicons-halflings-regular.ttf
│   │   ├── glyphicons-halflings-regular.woff
│   │   └── glyphicons-halflings-regular.woff2
│   └── js/
│       ├── bootstrap.js
│       ├── bootstrap.min.js
│       └── npm.js
└── src/

Konfiguracja Webpack

W naszym przypadku, Webpack będzie potrzebny do przetłumaczenia przy pomocy Babel kodu ES6+JSX na klasyczny JavaScript rozumiany przez przeglądarki oraz do sklejenia wszystkich wymaganych plików w jeden bundle.js. Ostateczny webpack.config.js wygląda następująco:

var path = require('path');
require("babel-polyfill");

module.exports = {
  entry: ['babel-polyfill', './src/main.js'],
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'public')
  },
  module: {
    loaders: [{
      test: /\.(js|jsx)$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }]
  }
};

Czas na HTML

Cała logika aplikacji będzie realizowana przez JavaScript, dlatego też wystarczy nam minimalistyczny plik HTML. Tworzymy /public/index.html:

<!DOCTYPE html>
<html>
  <head>
    <title>REkT</title>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="/css/bootstrap.min.css"/>
  </head>
  <body>
    <noscript>(╯°□°)╯︵ ┻━┻</noscript>
    <div class="container-fluid">
      <div class="row">
        <div id="root"></div>
      </div>
    </div>
    <script src="/bundle.js"></script>
  </body>
</html>

Expressem do przodu

Kierując się zasadą strzelania z armaty do wróbli, framework Express posłuży wyłącznie do serwowania statycznych plików. Tworzymy index.js:

const express = require('express');
const app = express();

app.use(express.static('public'));

const listen = app.listen(process.env.PORT || 3000, () => {
  console.log(listen.address());
});

Akcja, reakcja

W tym momencie jesteśmy już gotowi do napisania pierwszego kodu wykorzystującego bibliotekę React. Umieścimy go w pliku /src/main.js i po uruchomieniu Webpacka i Express zobaczymy wynik w przeglądarce.

Żeby za długo nie zwlekać i jednocześnie szybko przetestować czy wszystko działa zgodnie z założeniami, zaczniemy od klasycznego „Hello world”:

import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {

  render() {
    return (
      <p>Hello world</p>
    );
  }

}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

Jak to w ogóle działa? Na początku importowane są biblioteki react oraz react-dom. Później deklarujemy klasę App wywodzącą się z React.Component, której jedynym zadaniem na tę chwilę jest udostępnienie metody render zwracającej kod HTML. Na koniec, wywołujemy metodę render z ReactDOM, zlecając jej podpięcie komponentu <App> (którego działanie jest zdefiniowane właśnie w klasie App) do elementu o identyfikatorze „root” (w naszym przypadku DIV w index.html).

Umieszczanie HTML bezpośrednio w kodzie JavaScript może się na pierwszy rzut oka wydawać dziwne, ale od tego właśnie mamy Babel, który przetłumaczy go na mniej więcej coś takiego:

var App = function (_React$Component) {
  _inherits(App, _React$Component);

  function App() {
    _classCallCheck(this, App);

    return _possibleConstructorReturn(this, (App.__proto__ || Object.getPrototypeOf(App)).apply(this, arguments));
  }

  _createClass(App, [{
    key: 'render',
    value: function render() {
      return _react2.default.createElement(
        'p',
        null,
        'Hello world'
      );
    }
  }]);

  return App;
}(_react2.default.Component);

_reactDom2.default.render(_react2.default.createElement(App, null), document.getElementById('root'));

Startujemy

Uruchamiamy dwa osobne terminale, przechodzimy w nich do folderu z naszym projektem i uruchamiamy w pierwszym z nich „npm run watch” a w drugim „npm run dev”.

Jeśli wszystko pójdzie z planem, w pierwszym uruchomi się Webpack i utworzy plik /public/bundle.js, po czym zacznie na bieżąco w pętli monitorować zmiany w plikach źródłowych.

Version: webpack 3.5.6
Time: 3250ms
    Asset     Size  Chunks                    Chunk Names
bundle.js  1.05 MB       0  [emitted]  [big]  main
 [140] (webpack)/buildin/global.js 509 bytes {0} [built]
 [212] multi babel-polyfill ./src/main.js 40 bytes {0} [built]
 [415] ./src/main.js 2.39 kB {0} [built]
    + 534 hidden modules

W drugim uruchomi się Express w trybie DEBUG i wyświetli informację o porcie na którym nasłuchuje serwer HTTP.

{ address: '::', family: 'IPv6', port: 3000 }

Pozostaje wpisać adres http://localhost:3000 do przeglądarki i zobaczyć efekt działania aplikacji. Dla pewności, w konsoli DevTools możemy zweryfikować czy paragraf z „Hello world” podpiął się przy pomocy React do DIV:

<html>
<head>
    <title>REkT</title>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="/css/bootstrap.min.css">
  </head>
  <body>
    <noscript>(╯°□°)╯︵ ┻━┻</noscript>
    <div class="container-fluid">
      <div class="row">
        <div id="root"><p data-reactroot="">Hello world</p></div>
      </div>
    </div>
    <script src="/bundle.js"></script>
  

</body>
</html>

Gratulacje. Udało się nam wyświetlić dwa słowa przy pomocy pliku JS, który zajmuje aktualnie ponad 1MB w projekcie zawierającym ponad 60MB bibliotek.

I co dalej?

Zaczynamy od przebudowania /src/main.js:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';

const url = 'https://bittrex.com/api/v1.1/public/getmarketsummaries';

ReactDOM.render(
  <App url={url} />,
  document.getElementById('root')
);

Po pierwsze, wyrzuciliśmy klasę App do zewnętrznego pliku /src/app.js (zaraz go utworzymy). Po drugie, do komponentu <App> przekazujemy parametr url zawierający adres API strony Bittrex, zwracający JSON z listą kryptowalut w formacie:

{
  "success": true,
  "message": "",
  "result": [{
    "MarketName": "BTC-1ST",
    "High": 0.00008108,
    "Low": 0.00007205,
    "Volume": 1441030.45893439,
    "Last": 0.00007391,
    "BaseVolume": 110.40565565,
    "TimeStamp": "2017-09-10T15:09:38.907",
    "Bid": 0.00007359,
    "Ask": 0.00007468,
    "OpenBuyOrders": 143,
    "OpenSellOrders": 5537,
    "PrevDay": 0.00007645,
    "Created": "2017-06-06T01:22:35.727"
  }, {
    "MarketName": "BTC-2GIVE",
    "High": 0.00000143,
    "Low": 0.00000115,
    "Volume": 12007718.13364286,
    "Last": 0.00000120,
    "BaseVolume": 15.42232806,
    "TimeStamp": "2017-09-10T15:08:28.83",
    "Bid": 0.00000120,
    "Ask": 0.00000121,
    "OpenBuyOrders": 335,
    "OpenSellOrders": 2249,
    "PrevDay": 0.00000142,
    "Created": "2016-05-16T06:44:15.287"
  }, {

[...]

  }]
}

Tworzymy klasę App w /src/app.js:

import React from 'react';
import axios from 'axios';
import CoinsTable from './coinstable';

class App extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      coins: []
    };
  }

  componentDidMount() {
    axios.get(this.props.url)
      .then((res) => {
        if (res && res.data && res.data.result && res.data.result.length) {
          this.setState({
            coins: res.data.result
          });
        }
      });
  }

  render() {
    return (
      <div className="panel-body">
        <CoinsTable coins={this.state.coins} />
      </div>
    );
  }

}

export default App;

Tutaj zaczynają się dziać ciekawe rzeczy. Po pierwsze, w konstruktorze definiujemy this.state i wrzucamy do niego pustą tablicę o nazwie coins. Będziemy w niej później przechowywać dane pobrane z API Bittrex.

Metoda componentDidMount jest automatycznie wywoływana przez React po przypięciu komponentu <App> na stronie. W naszym przypadku, jej zadaniem jest wykonanie przy pomocy biblioteki Axios zapytania AJAX i wczytanie danych z adresu przekazanego do komponentu w parametrze url (jest dostępny w obiekcie this.props). Po przejściu zgrubnej weryfikacji, dane trafiają do this.state.coins.

Metoda render zwraca DIV zawierający kolejny komponent, tym razem CoinsTable, któremu przekazana zostaje tablica danych w parametrze coins. Należy zauważyć, że ze względu na fakt że „class” jest zastrzeżonym słowem w JavaScript, zmuszeni jesteśmy wpisywać w jego miejscu „className”.

Gwoli wyjaśnienia, w nomenklaturze React obiekt this.state zawiera podręczne dane komponentu w którym się znajdujemy i które możemy zmieniać przy pomocy metody this.setState. Obiekt this.props zawiera parametry przekazane przez komponent nadrzędny i jego elementy są tylko do odczytu.

Następny krok to klasa CoinsTable w /app/coinstable.js:

import React from 'react';
import CoinsRow from './coinsrow';

class CoinsTable extends React.Component {

  render() {
    if (this.props.coins && this.props.coins.length) {

      const rows = [];

      this.props.coins.forEach((coin) => {
        rows.push(
          <CoinsRow
            key={coin.MarketName}
            marketname={coin.MarketName}
            high={coin.High}
            low={coin.Low}
            volume={coin.Volume}
          />
        );
      });

      return (
        <table className="table table-striped">
          <thead>
            <tr>
              <th>MarketName</th>
              <th>High</th>
              <th>Low</th>
              <th>Volume</th>
            </tr>
          </thead>
          <tbody>
            {rows}
          </tbody>
        </table>
      );

    } else {

      return (
        <p>loading...</p>
      );

    }
  }

}

export default CoinsTable;

W tym komponencie nie potrzebujemy przechowywać stanu, bo jego działanie polega wyłącznie na wyświetleniu tego co otrzyma na liście parametrów w obiekcie this.props.

Metoda render na początku sprawdza, czy this.props.coins zawiera jakieś dane. Jeżeli tak jest, przygotowana jest nowa tablica o nazwie rows, składająca się z listy komponentów CoinsRow, do których są przekazane wartości poszczególnych komórek każdego z wierszy. Dodatkowo, każdy z nich otrzymuje parametr o nazwie „key”, zawierający unikatowy symbol umożliwiający bibliotece React szybką identyfikację elementów wymagających odświeżenia na wypadek zmian w danych. Na koniec zwracany jest kod gotowej tabeli w HTML.

W przypadku, gdy this.props.coins jest puste, otrzymamy tylko paragraf z treścią „loading…”.

Pozostała już tylko klasa CoinsRow w /app/coinsrow.js:

import React from 'react';

class CoinsRow extends React.Component {

  render() {
    return (
      <tr>
        <td>{this.props.marketname}</td>
        <td>{this.props.high.toFixed(8)}</td>
        <td>{this.props.low.toFixed(8)}</td>
        <td>{this.props.volume.toFixed(3)}</td>
      </tr>
    );
  }

}

export default CoinsRow;

Tutaj nie ma już nawet czego wyjaśniać, komponent zwraca pojedynczy wiersz tabeli z danymi w komórkach.

Gotowe!

Stworzyliśmy prostą aplikację, pobierającą dane z zewnętrznego API i wyświetlającą je w tabeli na stronie. W rzeczywistości nawet nie dotknęliśmy czubka możliwości biblioteki React, ale jak na pierwszy raz powinno to wystarczyć.

Kolejnym krokiem mogło by być dodanie możliwości przeszukiwania tabeli oraz sortowania danych w kolumnach. Przydała by się również opcja odświeżenia danych.

Uwagi

Metoda render w komponencie <App> jest wywoływana dwukrotnie. Za pierwszym razem z pustą tabelą this.state.coins, co skutkuje wyświetleniem napisu „loading”. Zaraz po tym zdarzeniu, React uruchamia metodę componentDidMount, która asynchronicznie pobiera dane z API. W chwili, gdy dane są gotowe, aplikacja modyfikuje this.state.coins przy pomocy this.setState a to automatycznie wymusza odświeżenie komponentu i ponowne uruchomienie metody render, tym razem już z gotową tabelą.

Ostateczna struktura projektu wygląda następująco:

.
├── index.js
├── node_modules/
│   └── (biblioteki z npm)
├── package.json
├── public/
│   ├── bundle.js (generowany przez webpack)
│   ├── css/
│   │   ├── bootstrap.css
│   │   ├── bootstrap.css.map
│   │   ├── bootstrap.min.css
│   │   ├── bootstrap.min.css.map
│   │   ├── bootstrap-theme.css
│   │   ├── bootstrap-theme.css.map
│   │   ├── bootstrap-theme.min.css
│   │   └── bootstrap-theme.min.css.map
│   ├── fonts/
│   │   ├── glyphicons-halflings-regular.eot
│   │   ├── glyphicons-halflings-regular.svg
│   │   ├── glyphicons-halflings-regular.ttf
│   │   ├── glyphicons-halflings-regular.woff
│   │   └── glyphicons-halflings-regular.woff2
│   ├── index.html
│   └── js/
│       ├── bootstrap.js
│       ├── bootstrap.min.js
│       └── npm.js
├── src/
│   ├── app.js
│   ├── coinsrow.js
│   ├── coinstable.js
│   └── main.js
└── webpack.config.js

Inspiracja

Poprawka poprawki shortcodes w WordPressie

Niedawno miałem okazję tworzyć od podstaw skórkę do WordPressa, co dość szybko zmieniło się w proces wyszukiwania sposobów na obejście problemów spowodowanych założeniami jakimi kierowali się twórcy tego CMS. Jednym z kłopotów na które się natknąłem, była niedopracowana obsługa tzw. „shortcodes” w edytorze, spowodowana wstawianiem paragrafów do każdej nowej linii.

W skrócie, jeżeli w treści bloga wpiszemy przykładowo:

tekst 1
[shortcode]
tekst 2

WordPress przekształci to w kod html:

<p>tekst 1</p>
<p>wynik działania shortcode</p>
<p>tekst 2</p>

Ma to swój sens, ale tylko jeżeli wynikiem działania shortcode jest tekst lub kod, który powinien znaleźć się wewnątrz nowego paragrafu. W moim jednak przypadku, skróty generowały nowe elementy blokowe i umieszczanie ich wewnątrz tagu „p” dawało w wyniku wadliwy kod html:

tekst 1
[shortcode]
tekst 2
[/shortcode]
tekst 3

zmieniało się w:

<p>tekst 1</p>
<p><div class="xyz"></p>
<p>tekst 2</p>
<p></div></p>
<p>tekst 3</p>

Wtyczką na ratunek

Powyższy problem miała rozwiązać wtyczka „Shortcode Empty Paragraph Fix” z oficjalnego repozytorium WordPressa, z ponad 4000 aktywnych instalacji i 18 ocenami 5 gwiazdek. Jej sercem była prosta funkcja modyfikująca treść wpisu:

function shortcode_empty_paragraph_fix( $content ) {

    // define your shortcodes to filter, '' filters all shortcodes
    $shortcodes = array( 'your_shortcode_1', 'your_shortcode_2' );

    foreach ( $shortcodes as $shortcode ) {

        $array = array (
            '<p>[' . $shortcode => '[' .$shortcode,
            '<p>[/' . $shortcode => '[/' .$shortcode,
            $shortcode . ']</p>' => $shortcode . ']',
            $shortcode . ']<br />' => $shortcode . ']'
        );

        $content = strtr( $content, $array );
    }

    return $content;
}

add_filter( 'the_content', 'shortcode_empty_paragraph_fix' );

Z pozoru jest to rozwiązanie doskonałe, możemy określić które skróty będą poprawiane, obsługiwane są zarówno skróty otwierające jak i zamykające, dla każdego skrótu wywoływana jest tylko jeden raz funkcja strtr co powinno wpłynąć pozytywnie na wydajność. Jest tylko jeden minus… powyższa funkcja nie działa.

strtr nie jest tym czym się wydaje

Autor nie wziął pod uwagę sposobu działania strtr wywoływanego z parametrem w postaci tablicy. Okazuje się, że jeżeli dwie pary ciągów do podmiany będą dotyczyły tego samego fragmentu źródłowego, użyta będzie tylko jedna z nich. Na przykład:

<?php

$content = 'ala [kot] bela [kot] ala [kot test1 kot] test2';
var_dump($content);

$arr = array(
    '[kot' => '(kot',
    'kot] ' => 'kot) ',
);

$result = strtr($content, $arr);
var_dump($result);

$result = strtr($result, $arr);
var_dump($result);

da w wyniku:

string(46) "ala [kot] bela [kot] ala [kot test1 kot] test2"
string(46) "ala (kot] bela (kot] ala (kot test1 kot) test2"
string(46) "ala (kot) bela (kot) ala (kot test1 kot) test2"

Pierwsze strtr zamieniło „[kot” na „(kot” oraz „kot]” na „kot)”, ale w przypadku „[kot]” druga reguła już nie zadziałała. Konieczność ponownego wywołania funkcji na ciągu wynikowym powoduje, że wykorzystanie tablicy w tym przypadku traci cały sens z punktu widzenia wydajności rozwiązania.

Poprawka poprawki

Lepszym rozwiązaniem jest użycie kodu autorstwa maxxscho z GitHuba:

function shortcode_empty_paragraph_fix($content)
{  
    $array = array (
        '<p>[' => '[',
        ']</p>' => ']',
        ']<br />' => ']'
    );

    $content = strtr($content, $array);

    return $content;
}

add_filter('the_content', 'shortcode_empty_paragraph_fix');

Niestety, w tym przypadku kosztem braku możliwości wyboru listy obsługiwanych skrótów, co może w wyniku powodować, że paragrafy i znaki końca linii będą usuwane nawet w miejscach, gdzie byśmy sobie tego nie życzyli.

Godot na Linuxie

Godot to jeden z tych projektów, które z pewnością nie cieszą się takim zainteresowaniem na jakie zasługują. Zapoczątkowany przez dwie osoby i rozwijany od kilkunastu lat, w pełni darmowy i z otwartym kodem źródłowym, silnik 2D/3D wraz z edytorem do tworzenia multiplatformowych gier, wydaje się idealny dla małych zespołów indie.

W poniższym krótkim poradniku zainstalujemy Godot 2.1.3 na Ubuntu.

Minimalistycznie

Jedną z zaskakujących cech Godota jest to, że składa się tylko z jednego pliku i można go uruchomić z dowolnego miejsca na dysku. Dla porządku jednak, przygotujemy dla niego folder w /opt. Z poziomu administratora:

mkdir -p /opt/godot
cd /opt/godot
wget https://downloads.tuxfamily.org/godotengine/2.1.3/Godot_v2.1.3-stable_x11.64.zip
unzip Godot_v2.1.3-stable_x11.64.zip
rm Godot_v2.1.3-stable_x11.64.zip
chmod -R a+rX /opt/godot
ln -s /opt/godot/Godot_v2.1.3-stable_x11.64 /usr/bin/godot

Przydał by się jeszcze skrót do programu w menu systemowym, tworzymy więc plik $HOME/.local/share/applications/godot.desktop

[Desktop Entry]
Name=Godot
Comment=Application for making games
Exec=/usr/bin/godot %U
Icon=godot
Terminal=false
Type=Application
Categories=Utility;Game;

Ikonę w formacie PNG wgrywamy do katalogu /usr/share/icons

Multiplatformowość

Godot jest w zasadzie gotowy do pracy, jedyne czego brakuje to szablonów eksportu, służących do zapisywania wyników naszej pracy jako samodzielnych aplikacji. W tym celu pobieramy plik Godot_v2.1.3-stable_export_templates.tpz ze strony projektu i importujemy go w edytorze (Ustawienia -> Zainstaluj Szablony Eksportu).

Od tego momentu, przycisk Eksport będzie wyświetlał listę docelowych platform:

  • Android
  • Blackberry 10
  • HTML5
  • Linux X11
  • Mac OSX
  • Windows Desktop

Kompilacja dla iOS i UWP jest niestety bardziej skomplikowana i wymaga zapoznania się z dokumentacją.

Sznurki

Windows na Maku

W teorii, instalacja Windows na komputerze od Apple nie powinna sprawiać większego kłopotu. Uruchamiamy dedykowane do tego celu narzędzie o nazwie Boot Camp, tworzymy partycję, restartujemy system i wgrywamy Windows z płyty jak na zwykłym PC. Schody pojawiają się jednak wtedy, gdy próbujemy to zrobić na iMacu albo Macbooku, który z jakiegoś powodu nie ma napędu DVD.

iMac na którym niedawno instalowałem macOS Sierra jest w pewnym sensie jedyny w swoim rodzaju. Jego czytnik DVD został odłączony od płyty głównej, a kabel z wtyczką SATA i zasilaniem wyprowadzony na zewnątrz obudowy, tak by umożliwić podpięcie drugiego dysku. Dzięki tej przeróbce, cały oryginalny dysk talerzowy miał być przeznaczony na macOS, natomiast dodatkowy SSD wyłącznie na Windows. Małżeństwo idealne w całkowitej separacji.

Z czym do ludzi:

  • iMac 27″ z końca 2009 roku (symbol iMac11,1)
  • na dysku wewnętrznym zainstalowany system macOS Sierra 10.12.6
  • w miejscu napędu DVD podłączony dysk SSD 240GB
  • nadzieja na uruchomienie Windows 10

Pierwsza przeszkoda

Boot Camp z Sierry miał w głębokim poważaniu fakt, że w systemie nie rozpoznano napędu DVD. Kierując się symbolem komputera, uznał że czytnik musi być i już, a wyświetlany komunikat o braku płyty z instalatorem Windows w zasadzie na samym początku przekreślił powodzenie całej operacji.

Naturalnie, zwykłe zamontowanie obrazu ISO w systemie nie wystarczyło do oszukania Boot Campa. W internecie znalazłem jednak dwa obiecujące sposoby obejścia problemu, z których pierwszy polegał po prostu na stworzeniu wirtualnego napędu przy pomocy DAEMON Tools. Nie chcąc iść na łatwiznę, wybrałem sposób drugi:

  1. skopiowałem aplikację „Boot Camp Assistant.app” z /Applications/Utilities na pulpit (pliki w /Applications są tylko do odczytu)
  2. w pliku Info.plist znadującym się w podkatalogu Contents zmieniłem nazwę klucza „PreUSBBootSupportedModels” na „USBBootSupportedModels” i dopisałem do listy nowy ciąg z symbolem „iMac11,1”
<key>USBBootSupportedModels</key>
<array>
  <string>MacBook7,1</string>
  <string>MacBookAir3,2</string>
  <string>MacBookPro8,3</string>
  <string>MacPro5,1</string>
  <string>Macmini4,1</string>
  <string>iMac12,2</string>
  <string>iMac11,1</string>
</array>

Po tej magicznej sztuczce, Boot Camp uruchomiony z pulpitu przestał wymagać płyty z Windows i jednocześnie pozwolił na utworzenie startowego pendrive z obrazu ISO.

Firmware nie oszukasz

Po przygotowaniu pendrive z Windows 10 i założeniu na dysku SSD partycji „BOOTCAMP”, iMac się zrestartował i po chwili… wyświetlił komunikat o braku napędu startowego. Okazało się, że w modelach z napędem DVD (nawet jeżeli jest fizycznie odpięty) wbudowany firmware nie pozwala na uruchomienie instalatora Windows z USB z poziomu Boot Camp. Można co prawda próbować go zaktualizować ze strony Apple, ale osoby które to robiły twierdzą, że nie ma to żadnego wpływu na powyższą blokadę.

Spróbowałem więc inaczej. Uruchomiłem komputer z wciśniętym klawiszem Option i wybrałem z listy urządzeń startowych dysk USB. O dziwo iMac nie protestował i pozwolił na rozpoczęcie instalacji. Ze względu jednak na fakt, że działała ona teraz w trybie EFI, partycja założona wcześniej przez Boot Camp okazała się niewystarczająca i trzeba było ponownie sformatować dysk SSD.

Windows 10 zainstalował się bezproblemowo. Po restarcie, wstępnym skonfigurowaniu i założeniu konta administratora, rozpoczął się pierwszy proces aktualizacji. System wyświetlił komunikat o pobieraniu nowego sterownika karty graficznej, po czym… wyłączył się monitor.

Ciemność widzę

Z jakiegoś powodu, Windows nie lubi karty graficznej Radeon 4850 w iMacach z 2009 roku. To uczucie jest na tyle silne, że każda próba instalacji nowych sterowników kończy się utratą obrazu. Nawet instalacja Windows 7 z płyty przy pomocy Boot Campa może się nie powieść, jeśli nie przygotujemy wcześniej pendrive ze specjalnie przygotowaną paczką sterowników.

Spróbowałem więc zainstalować ponownie Windows 10, tym razem z odłączonym kablem od internetu i rozpocząłem poszukiwania działających driverów. Program instalacyjny z oprogramowaniem Boot Camp wyświetlił tylko komunikat, że nie wspiera nic powyżej Windows 7. Sterowniki do ATI z jednego z podkatalogów nie zadziałały. Najnowsze sterowniki ze strony ATI poinformowały, że nie zainstalują się bez dostępu do internetu. Inne sterowniki od ATI, przeznaczone rzekomo specjalnie do iMaców z 2009 roku też nie dawały sobie rady i ekran gasnął w momencie ich inicjalizacji.

Opcja używania Windows bez dostępu do internetu albo z wyłączonymi aktualizacjami i jednocześnie na domyślnym sterowniku karty graficznej od Microsoft nie wchodziła w grę.

Nie kijem go to pałą

Trzeba było pożegnać się z Windows 10 i spróbować czegoś starszego. Zacząłem od obrazu Windows 8, jednak pendrive startowy nie był widoczny ani pod Boot Campem ani w trybie EFI. Obraz z Windows 7? Efekt identyczny – brak możliwości uruchomienia instalatora.

W internecie natrafiłem na sposób, polegający na wgraniu Windows przy pomocy maszyny wirtualnej VMware. Zamiast wirtualnego dysku miało się wskazywać fizyczną partycję, dzięki czemu instalator kopiował pliki od razu we właściwe miejsce. Później, taki dysk nadawał się do uruchamiania systemu bezpośrednio z menu wyboru urządzeń startowych iMaca.

Rozwiązanie było genialne w swojej prostocie, dlatego też nie wahając się ani chwili, postąpiłem inaczej:

  1. ponownie sformatowałem dysk SSD przy pomocy Boot Campa
  2. odpiąłem dysk SSD od iMaca i podłączyłem do stacjonarnego PC jako jedyny napęd w systemie
  3. uruchomiłem PC z pendrive z Windows 7 w trybie legacy
  4. włączyłem instalację Windows 7 i wybrałem jako miejsce docelowe partycję BOOTCAMP (trzeba było jeszcze ją przeformatować z FAT32 na NTFS)
  5. w momencie, gdy instalator pierwszy raz zrestartował PC, odpiąłem dysk i podłączyłem go ponownie do iMaca
  6. dokończyłem instalację Windows na iMacu, bezpośrednio z SSD (wszystkie pliki kopiują się na dysk podczas pierwszej fazy i pendrive nie jest już później potrzebny)

Jeszcze jeden krok

Wgrany w powyższy sposób system działał bez zarzutu. Oprogramowanie Boot Camp zainstalowało się bez sprzeciwu, wszystkie urządzenia od bluetooth po kamerę iSight zostały rozpoznane. Sterowniki do karty graficznej Radeon również funkcjonowały poprawnie.

Pozostało więc na koniec spróbować uruchomić upgrade do Windows 10 przy pomocy Windows Media Creation Tool. Operacja przebiegła pomyślnie i po kolejnym restarcie iMac uruchomił się w najnowszej wersji systemu.

Podsumowując, korzystanie z Windows 10 na iMacu z 2009 roku jest możliwe, pomimo że nie jest oficjalnie wspierane przez Apple. Trzeba się jednak pogodzić z tym, że czysta instalacja będzie miała najprawdopodobniej problemy ze sterownikami do karty graficznej. Te same sterowniki zainstalowane wcześniej na Windows 7 funkcjonują poprawnie po upgrade.

Pantera Śnieżna w Sierra Nevada

W ubiegłym tygodniu przytrafiła mi się sposobność aktualizowania systemu operacyjnego na iMacu. Na pierwszy rzut oka mogło by się wydawać, że zadanie jest trywialne i polega na kliknięciu jednego przycisku w AppStore, jednak czynnikiem podnoszącym poziom trudności okazała się różnica numerów wersji, którą trzeba było pokonać.

Na warsztacie:

  • iMac 27″ z końca 2009 roku (symbol iMac11,1)
  • zainstalowany system Mac OS X 10.6 Snow Leopard z 2011 roku
  • docelowy system macOS Sierra 10.12.6 z 2017 roku

Podejście pierwsze

Po wejściu do działu aktualizacji systemu w AppStore wyświetliła się propozycja automatycznej instalacji macOS Sierra. Niestety, próba jej uruchomienia zakończyła się komunikatem, że wymagane jest posiadanie co najmniej OS X w wersji 10.7.5 Lion. Nie było nigdzie jednak wyjaśnione, w jaki sposób podnieść 10.6 do 10.7.

Pomyślałem, że można by spróbować zdobyć obraz płyty instalacyjnej macOS Sierra, wystartować z niej komputer i zainstalować system „na czysto”. W odmętach internetu znalazłem plik w formacie PKG, niestety za nic w świecie nie dawał się rozpakować na 10.6, ani wbudowanym domyślnym archiwizatorem ani komendami w terminalu (pkgutil, xar, tar, zip).

Podejście drugie

Zdecydowałem się na pójść inną drogą i zacząć od instalacji OS X 10.7 Lion. Z nieoficjalnych źródeł w internecie pobrałem obraz płyty, która w przeciwieństwie do PKG z Sierrą pozwoliła się rozpakować i uruchomić. Niestety, w połowie wgrywania plików wyświetlił się błąd pakietu i aktualizacja została przerwana.

Należy tu nadmienić, że wewnątrz każdej aplikacji instalującej system znajduje się plik InstallESD.dmg, który jest obrazem dysku zawierającym samodzielny instalator z partycją startową EFI. Przy pomocy wbudowanego w 10.6 narzędzia dyskowego wgrałem plik DMG na pendrive i uruchomiłem iMaca z USB (klawisz Option podczas startu).

Ponownie, w połowie aktualizacji pojawił się błąd informujący o uszkodzonym pakiecie i proces kopiowania się zatrzymał. Po restarcie, uruchomił się co prawda stary system 10.6, jednakże ciężko go już było móc określać mianem stabilnego. W szczególności Finder wykazywał dziwne skłonności do zawieszania się co chwilę. W tym momencie wiadomo już było, że Pantera Śnieżna skrzyżowana z chorym Lwem nie wydała zdrowego potomstwa.

Podejście trzecie

Przesiadłem się na inny komputer z zainstalowanym Windows w celu znalezienia lepszego źródła 10.7. Jako, że i tak Lion miał być tylko krokiem przejściowym, potrzebowałem czegoś co się da zainstalować na chwilę i co pozwoli na pobranie oficjalnego instalatora 10.12 z AppStore.

Do trzech razy sztuka. Pobrałem nowy obraz dysku 10.7, wypakowałem z niego plik InstallESD.dmg i ze względu na to, że nie ufałem już systemowi działającemu na iMacu, startowy pendrive przygotowałem pod Windows przy pomocy programu TransMac (trial).

Po wystartowaniu iMaca z USB, sformatowaniu dysku twardego (przy okazji podzieliłem go na dwie partycje HFS+ i ExFAT) i czystej instalacji od zera, miałem w końcu dostęp do działającego OS X Lion.

Czas na Sierrę

To nie koniec problemów. Po wejściu do AppStore i uruchomieniu procesu pobierania macOS Sierra, znów wyświetlił się komunikat o braku odpowiedniej wersji systemu operacyjnego. Na szczęście chodziło tylko o różnicę w ostatniej cyfrze i dało się to naprawić pobierając 2GB aktualizacji od Apple.

Po kolejnym restarcie, wszystkie wymagania instalatora 10.12 zostały wreszcie spełnione.

Tym razem AppStore nie protestował i pobrał 5GB oficjalnej paczki z macOS Sierra, którą na wszelki wypadek skopiowałem sobie od razu na zewnętrzny dysk. Plan był taki, żeby przygotować nowy pendrive startowy i wgrać ostateczny system na czysty dysk, bez żadnych śladów po Lionie z nieoficjalnego źródła.

DMG jak damaged

Niestety, narzędzie dyskowe z 10.7.5 nie potrafiło zapisać pliku InstallESD.dmg z 10.12 na USB.

Wyświetlał się komunikat o konieczności przeskanowania obrazu dysku, po czym po chwili występował „błąd wewnętrzny”.

Próba utworzenia dysku z poziomu terminala, przy pomocy zawartego w pakiecie programu „createinstallmedia” kończyła się dosłownie niczym, po wprowadzeniu hasła administratora system wracał bez słowa do znaku zachęty wiersza polecenia.

Nawet nagranie DMG przez TransMac z poziomu Windows nie dawało pozytywnych rezultatów. Przygotowany w ten sposób pendrive nie był widziany na liście dostępnych dysków startowych podczas uruchamiania iMaca.

Sierrę Lionem, Sierrę Sierrą

Pozostało pozwolić AppStore, żeby uruchomił instalator Sierry pod kontrolą Liona. Niespodziewanie, proces aktualizacji przebiegł bez najmniejszych problemów i po restarcie uruchomił się system w wersji 10.12.5.

To jednak nie wszystko. Ze względu na to, że na dysku wciąż znajdowały się pozostałości po 10.7, zdecydowałem się jeszcze raz spróbować utworzyć pendrive, by móc uruchomić z niego iMaca i wgrać 10.12 na sformatowaną partycję.

Naturalnie, cały pakiet instalacyjny został automatycznie usunięty od razu po aktualizacji, ale na szczęście miałem jego kopię na zewnętrznym dysku. Tutaj czekała na mnie kolejna niespodzianka, komenda „createinstallmedia” tym razem dała się uruchomić w terminalu i poprawnie wykonała swoje zadanie.

Dalej już było z górki. Restart z wciśniętym klawiszem Option, wybranie startowego dysku USB, format dysku, utworzenie partycji, instalacja systemu, restart, wstępna konfiguracja i gotowe.

Koniec?

Jaka była pierwsza rzecz, którą zgłosił macOS Sierra po uruchomieniu i podłączeniu do internetu? Że akurat tego dnia (20 lipca 2017) została wydana wersja 10.12.6 i należy uruchomić aktualizację…

Instalacja edytora Unity 3D w Linuxie

Pod koniec 2015 roku, Unity Technologies wydało pierwszą, testową wersję edytora Unity 3D dla systemu Linux. Od tamtego momentu minęło już trochę czasu i można powiedzieć, że większość początkowych problemów zostało już usuniętych, dzięki czemu aplikacja nadaje się do poważnej pracy.

W poniższym przykładzie zainstalujemy wersję 2017.2.0 Beta 2 na Ubuntu.

Przygotowania

Pierwszą rzeczą którą należy sprawdzić, to wersja OpenGL obsługiwana przez nasz system. Unity wymaga co najmniej OpenGL 3.2 i w przypadku jego braku będzie zamykane z błędem w momencie dodawania nowego obiektu do sceny albo w ogóle się nie uruchomi.

Wpisujemy w terminalu komendę „glxinfo -B” (pakiet Mesa 3D) i szukamy numerów wersji. W moim przypadku:

name of display: :1
display: :1  screen: 0
direct rendering: Yes
Extended renderer info (GLX_MESA_query_renderer):
    Vendor: Intel Open Source Technology Center (0x8086)
    Device: Mesa DRI Intel(R) Ivybridge Mobile  (0x166)
    Version: 17.1.2
    Accelerated: yes
    Video memory: 1536MB
    Unified memory: yes
    Preferred profile: core (0x1)
    Max core profile version: 4.2
    Max compat profile version: 3.0
    Max GLES1 profile version: 1.1
    Max GLES[23] profile version: 3.0
OpenGL vendor string: Intel Open Source Technology Center
OpenGL renderer string: Mesa DRI Intel(R) Ivybridge Mobile 
OpenGL core profile version string: 4.2 (Core Profile) Mesa 17.1.2
OpenGL core profile shading language version string: 4.20
OpenGL core profile context flags: (none)
OpenGL core profile profile mask: core profile
OpenGL version string: 3.0 Mesa 17.1.2
OpenGL shading language version string: 1.30
OpenGL context flags: (none)
OpenGL ES profile version string: OpenGL ES 3.0 Mesa 17.1.2
OpenGL ES profile shading language version string: OpenGL ES GLSL ES 3.00

Pomimo tego, że linia „OpenGL version” wskazuje na 3.0, Unity automatycznie wybierze profil „core” i uruchomi się w trybie 4.2.

Domyślnym edytorem kodów źródłowych w Unity jest MonoDevelop, które jest co prawda zawarte w pakiecie instalacyjnym, ale ze względu na lepszą integrację z systemem wygodniej jest używać wersji dostępnej w oficjalnym repozytorium. Uruchamiamy z poziomu administratora:

apt install mono-complete mono-reference-* monodevelop

Instalacja

Lista wszystkich dostępnych paczek znajduje się na forum Unity w wątku „Unity on Linux: Release Notes and Known Issues„. Znajdujemy interesujący nas Build #20170707 i pobieramy:

wget http://beta.unity3d.com/download/b6e0e521da90/unity-editor_amd64-2017.2.0b2.deb

Przy pomocy komendy „dpkg –info unity-editor_amd64-2017.2.0b2.deb” możemy sprawdzić, jakie dodatkowe pakiety będą potrzebne do uruchomienia programu:

Package: unity-editor
Version: 2017.2.0b2
Architecture: amd64
Maintainer: Linux Team <linux-team@unity3d.com>
Installed-Size: 7895832
Depends: debconf (>= 0.5) | debconf-2.0, gconf-service, lib32gcc1 (>= 1:4.1.1), lib32stdc++6 (>= 4.6), libasound2 (>= 1.0.23), libc6 (>> 2.15), libc6-i386 (>= 2.15), libcairo2 (>= 1.6.0), libcap2 (>= 2.10), libcups2 (>= 1.4.0), libdbus-1-3 (>= 1.2.14), libexpat1 (>= 1.95.8), libfontconfig1 (>= 2.8.0), libfreetype6 (>= 2.3.9), libgcc1 (>= 1:4.1.1), libgconf-2-4 (>= 2.31.1), libgdk-pixbuf2.0-0 (>= 2.22.0), libgl1-mesa-glx | libgl1, libglib2.0-0 (>= 2.31.8), libglu1-mesa | libglu1, libgtk2.0-0 (>= 2.24.0), libnspr4 (>= 1.8.0.10), libnss3 (>= 3.14.3), libpango1.0-0 (>= 1.22.0), libstdc++6 (>= 4.6), libx11-6 (>= 2:1.4.99.1), libxcomposite1 (>= 1:0.3-1), libxcursor1 (>> 1.1.2), libxdamage1 (>= 1:1.1), libxext6, libxfixes3, libxi6 (>= 2:1.2.99.4), libxrandr2 (>= 2:1.2.99.2), libxrender1, libxtst6, zlib1g (>= 1:1.1.4), libpng12-0 | libpng16-16, libpq5, lsb-release, xdg-utils, npm
Recommends: ffmpeg | libav-tools, nodejs, java6-runtime, gzip, monodevelop, java7-jdk | java7-sdk, mono-reference-assemblies-2.0, mono-reference-assemblies-3.5
Section: devel
Priority: optional
Homepage: http://unity3d.com

Instalacja pliku DEB przy pomocy dpkg nie bierze pod uwagę wymaganych zależności, trzeba ją więc uzupełnić o wywołanie komendy apt. Z poziomu administratora:

dpkg -i unity-editor_amd64-2017.2.0b2.deb
apt install -f

To wszystko, Unity zostało zainstalowane w katalogu /opt i jest gotowe do pracy.

Dodatkowe informacje na temat działania i ewentualnych problemów na które natrafili użytkownicy, znajdują się na forum w dziale „Linux Editor„.

Konfiguracja libGDX w IntelliJ IDEA

Utworzenie nowego projektu opartego na bibliotece libGDX i wczytanie go do IntelliJ IDEA nie sprawia większych problemów, jednak ze względu na integrację z Gradle oraz fakt, że na początku nie wszystko konfiguruje się automatycznie, należy pamiętać o wykonaniu wszystkich wymaganych kroków.

Poniższy przykład wykorzystuje:

  • IntelliJ IDEA w wersji 2017.1.4
  • libGDX w wersji 1.9.6
  • Android SDK z API 20 oraz Build Tools 23.0.1
  • system operacyjny Linux (Ubuntu)

Instalator libGDX

Zaczynamy od pobrania i uruchomienia najnowszej wersji instalatora libGDX ze strony www.

cd ~/Pobrane
wget https://libgdx.badlogicgames.com/nightlies/dist/gdx-setup.jar
java -jar gdx-setup.jar

Następnie podajemy instalatorowi kilka podstawowych informacji o projekcie:

Name: nazwa aplikacji (zostanie zapisana do pliku build.gradle jako allprojects.ext.appName oraz do android/res/values/strings.xml jako app_name)

Package: unikatowa nazwa pakietu dla aplikacji, zgodna ze standardem Java

Game class: główna klasa aplikacji, która będzie w zależności od platformy uruchamiana przez DesktopLauncher lub AndroidLauncher (zostanie utworzona w katalogu core/nazwa_pakietu)

Destination: docelowy katalog, w którym zostanie utworzony projekt

Android SDK: katalog z zainstalowanym Android SDK

Sub Projects: docelowe platformy, na które będziemy kompilować aplikację (w tym przypadku zostawiamy tylko Desktop i Android)

Extensions: opcjonalne biblioteki, z których będzie dodatkowo korzystać aplikacja (ten przykład nie wykorzystuje żadnej z nich)

Show Third Party Extensions: kolejna lista dodatkowych bibliotek, których na razie nie będziemy używać

Advanced: opcje zaawansowane, możemy zostawić je wyłączone

Przed kliknięciem przycisku Generate, warto się jeszcze upewnić, że mamy zainstalowany Android SDK w odpowiedniej wersji. Minimalne zalecane wersje można znaleźć na stronie libGDX:

Wersje zainstalowane w systemie sprawdzamy przy pomocy skryptu znajdującego się w katalogu Android SDK ($ANDROID_HOME/tools/bin):

$ANDROID_HOME/tools/bin/sdkmanager --list

W moim przypadku:

Installed packages:
  Path                        | Version | Description                       | Location                    
  -------                     | ------- | -------                           | -------                     
  build-tools;23.0.1          | 23.0.1  | Android SDK Build-Tools 23.0.1    | build-tools/23.0.1/         
  build-tools;25.0.2          | 25.0.2  | Android SDK Build-Tools 25.0.2    | build-tools/25.0.2/         
  build-tools;26.0.0          | 26.0.0  | Android SDK Build-Tools 26        | build-tools/26.0.0/         
  emulator                    | 26.0.3  | Android Emulator                  | emulator/                   
  extras;android;m2repository | 47.0.0  | Android Support Repository, re... | extras/android/m2repository/
  extras;google;m2repository  | 54      | Google Repository                 | extras/google/m2repository/ 
  patcher;v4                  | 1       | SDK Patch Applier v4              | patcher/v4/                 
  platform-tools              | 26.0.0  | Android SDK Platform-Tools        | platform-tools/             
  platforms;android-20        | 2       | Android SDK Platform 20, rev 2    | platforms/android-20/       
  platforms;android-25        | 3       | Android SDK Platform 25           | platforms/android-25/       
  sources;android-20          | 1       | Sources for Android 20            | sources/android-20/         
  sources;android-25          | 1       | Sources for Android 25            | sources/android-25/         
  tools                       | 26.0.2  | Android SDK Tools                 | tools/

Uruchamiamy generowanie projektu i czekamy na rozwój wydarzeń. Istnieje duże prawdopodobieństwo, że pojawi się nam ostrzeżenie:

Wybranie „No” spowoduje wymuszenie korzystania z narzędzi w wersji 23.0.1.

Analogicznie, kolejne pytanie będzie dotyczyło wersji API:

Wybranie „No” wymusi API 20.

W przypadku, gdy zdecydujemy się później na zmianę tych ustawień, wystarczy poprawić pliki w katalogu projektu:

android/AndroidManifest.xml:

<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="20" />

android/build.gradle:

android {
    buildToolsVersion "23.0.1"
    compileSdkVersion 20

[...]

    defaultConfig {
        minSdkVersion 9
        targetSdkVersion 20

Parametr minSdkVersion oznacza minimalną wersję systemu Android, na którym będzie można uruchomić aplikację. W przypadku libGDX jest to API 9 czyli Android 2.3 GINGERBREAD.

Po chwili projekt powinien być gotowy:

Wczytanie do IntelliJ IDEA

Kierując się podpowiedzią z instalatora, w głównym oknie IntelliJ IDEA wybieramy „Open” i wskazujemy plik build.gradle.

Następnie potwierdzamy, że chcemy go otworzyć jako projekt.

W ustawieniach importu Gradle ważne jest odznaczenie opcji „Create separate module per source set”.

Wszystkie moduły zostawiamy włączone.

Teraz musimy poczekać, aż wszystkie pliki zostaną zindeksowane. Dymkami z informacjami nie musimy się na razie przejmować.

Na liście konfiguracji powinien pojawić się „android”. Cel „desktop” trzeba dodać ręcznie.

W edytorze konfiguracji klikamy zielony plus i wybieramy „Application”.

Jako „Main class” wskazujemy klasę DesktopLauncher.

Katalog „Working directory” jest wspólny dla obu platform i powinien wskazywać na „android/assets”. W „Use classpath of module” wybieramy moduł desktop.

W podobny sposób, możemy sobie dodać skróty do zadań zdefiniowanych w Gradle:

Zadanie „clean” usuwa wszystkie pliki powstałe podczas kompilacji projektu:

Analogicznie możemy dodać android -> assembleRelease tworzące plik APK dla Androida (przed opublikowaniem trzeba go jeszcze podpisać cyfrowo).

Zadanie desktop -> dist tworzy paczkę JAR zawierającą wszystkie elementy potrzebne do uruchomienia projektu na Linux/Windows/MacOS.

Tak ostatecznie wygląda przykładowa lista konfiguracji:

Uwagi

Plugin Gradle dołączany do libGDX nie jest najnowszy, więc prędzej czy później wyświetli się nam komunikat z propozycją aktualizacji. Wystarczy kliknąć Update i poczekać na zakończenie instalacji.

Jeżeli korzystamy z dodatku FindBugs, podczas weryfikowania projektu pojawi się ostrzeżenie o błędach w generowanym automatycznie pliku R.java. Jako, że nie mamy wpływu na jego zawartość, najlepiej dodać go do wyjątków.

Listy wykluczeń są zapisywane w formacie XML a ich położenie można znaleźć w konfiguracji dodatku FindBugs w zakładce Filter.

Przykładowy plik findbugs-android-exclude.xml:

<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>
    <Match>
        <Or>
            <Class name="~.*\.R\$.*"/>
            <Class name="~.*\.Manifest\$.*"/>
        </Or>
    </Match>
</FindBugsFilter>

To w zasadzie wszystko. Możemy rozpocząć pracę nad naszą aplikacją.

Polecane wtyczki do IntelliJ IDEA

Save Actions

Jedną z funkcjonalności, które są dostępne w Eclipse a których brakuje w IntelliJ IDEA, jest możliwość automatycznego wykonywania wybranych czynności przed zapisaniem pliku w edytorze. Tę niedoskonałość rozwiązuje plugin Save Actions.

Configure -> Plugins -> Browse repositories -> Save Actions:

W podstawowej konfiguracji, dodatek zatroszczy się o usunięcie nieużywanych importów oraz sformatowanie kodu zgodnie ze standardami ustawionymi w Settings -> Editor -> Code Style.

FindBugs

FindBugs to jeden z najlepszych darmowych analizatorów statycznych kodu źródłowego dla języka Java. Dzięki regularnie aktualizowanej liście reguł i zasad pozwala na wykrycie potencjalnych błędów w programie jeszcze przed jego uruchomieniem.

Configure -> Plugins -> Browse repositories -> FindBugs:

Po zainstalowaniu pluginu, aktywujemy dodatkowe źródła reguł w zakładce General -> Plugins:

Włączanie i wyłączanie pojedynczych reguł znajduje się w zakładce Detector:

Proponuję włączyć wszystkie i przeskanować swój kod a później ewentualnie zdecydować na jakie ustępstwa od narzuconych zasad się godzimy i dezaktywować tylko te wybrane.

Wstępna konfiguracja IntelliJ IDEA

Moją przygodę z programowaniem w Java zacząłem w środowisku Eclipse, jednak po tym jak w 2013 roku Google wydało Android Studio oparte na IntelliJ IDEA, zdecydowałem się na przesiadkę. Początkowo na nowej platformie brakowało mi kilku elementów z których korzystałem wcześniej, ale na szczęście dodatki dostępne w oficjalnym repozytorium rozwiązały te problemy.

Instalacja

Instalacja IntelliJ IDEA w systemie Linux nie sprawia większych problemów. Na początku musimy zadbać o dostępność środowiska Java. W przypadku Ubuntu, uruchamiamy z poziomu administratora:

apt install default-jre default-jdk

Następnym krokiem jest pobranie najnowszej wersji IntelliJ IDEA ze strony JetBrains (w moim przypadku 2017.1.4 Community Edition w formacie TAR.GZ) i rozpakowanie jej do docelowego katalogu.

cd /opt
wget https://download-cf.jetbrains.com/idea/ideaIC-2017.1.4.tar.gz
tar xvfz ideaIC-2017.1.4.tar.gz
mv idea-IC-171.4694.23 idea
chmod a+rX -R idea

Po przelogowaniu się na użytkownika ze standardowymi uprawnieniami, uruchamiamy program:

/opt/idea/bin/idea.sh

Pierwsze ekrany akceptujemy w zasadzie bez żadnych zmian (najodważniejsi oraz osoby o skłonnościach masochistycznych mogą sobie włączyć plugin emulujący edytor VIM). Konfigurator utworzy skrót do uruchamiania programu z poziomu systemowego menu, dzięki czemu później nie trzeba będzie pamiętać o położeniu pliku idea.sh.

Jeżeli wszystko pójdzie zgodnie z planem, zobaczymy ekran startowy:

Ustawienia

Na początku, proponuję wyłączyć opcję automatycznego otwierania ostatniego projektu po uruchomieniu programu. W przypadku, gdy planujemy utworzyć nowy projekt lub pracować na innym niż poprzednio, zaoszczędzi nam to trochę czasu.

Configure -> Settings -> Apperance & Behavior -> System Settings:

Każdy nowy plik źródłowy dodawany do projektu powinien zaczynać się od komentarza z informacjami na jego przeznaczenia, autora oraz warunków licencji. W ustawieniach IntelliJ IDEA możemy przygotować szablony takich bloków, które później będą wstawiane do kodu:

Configure -> Settings -> Editor -> Copyright-> Copyright Profiles:

Configure -> Settings -> Editor -> Copyright:

Konfiguracja SDK

Przed skompilowaniem i uruchomieniem pierwszego programu, należy skonfigurować SDK.

Configure -> Project Defaults -> Project Structure:

Z listy SDKs wybieramy JDK (Java Development Kit) i znajdujemy katalog na dysku z zainstalowanym środowiskiem.

W moim przypadku, ścieżką do najnowszej zainstalowanej Javy jest /usr/lib/jvm/default-java wskazującą na katalog OpenJDK w wersji 1.8.

Przy okazji możemy podpiąć SDK pozwalające na tworzenie aplikacji na Androida. Analogicznie jak wyżej, wybieramy Android SDK i wskazujemy katalog w którym znajdują się biblioteki (naturalnie wcześniej trzeba je zainstalować np. z Android Studio).

Build Target to wersja API dla której będą kompilowane aplikacje.

Na koniec, w polu Project SDK ustawiamy domyślne SDK które będzie wykorzystywane w naszych projektach.

Po tych krokach, IntelliJ IDEA jest gotowa do pracy.

Wirtualne zakupy dla naiwnych

Niedawno, przeglądając galerię coraz to bardziej wymyślnych bannerów reklamowych, trafiłem na stronę promującą kolejny rewolucyjny specyfik przeciwzmarszczkowy. Profesor, który firmuje go swoim nazwiskiem ma prawdopodobnie grubo ponad 100 lat, biorąc pod uwagę fakt, że dzięki swoim magicznym kremom na zdjęciu wygląda na 70-latka.

Nie to jednak zainteresowało mnie na jego stronie. Zgaduję, że dotychczasowe metody przekonywania potencjalnych klientów do złożenia zamówienia straciły na skuteczności i zwykłe wychwalanie produktu nie zdawało już egzaminu, dział marketingu zdecydował się więc na nowatorskie posunięcie. Na stronie umieszczony został popup, wyświetlający co chwilę informacje o rzekomych kolejnych zamówieniach. Co bowiem lepiej przekonuje do zakupu, niż świadomość że inni już się zdecydowali? Na dodatek, gdy zrobili to „przed chwilą”, bądź „minutę temu” a przeglądających witrynę jest aktualnie ponad 200 osób.

Sęk w tym, że po załadowaniu strona nie wykonuje już żadnych zapytań do serwera, skąd zatem ma nowe informacje na temat zamówień? Odpowiedzią jest skrypt odpowiedzialny za generowanie treści wyświetlanej w okienku, umieszczony bez najmniejszej żenady w nagłówku kodu strony. Oto jego fragment:

var locali_mian = new Array("Warszawa", "Kraków", "Poznań", "Szczecin", "Radom", "Gdańsk", "Giżycko", "Szczawnica", "Łódź", "Kielce", "Żywiec", "Zakopane", "Lublin", "Jelenie Góra" , "Szklarska Poręba", "Malbork" ,"Tarnów", "Bytom", "Pisz", "Ryn", "Brodnica", "Ełk", "Legnica");

var locali_dop = new Array("z Ciechanowa", "z Radomia", "z Mińska Maz.", "z Warki", "z Torunia", "z Przemyśla", "z Warszawy", "z Katowic", "z Wrocławia", "z Gdyni", "z Bydgoszczy", "z Sopotu", "z Łomży", "z Koszalina" , "z Suwałk", "z Legionowa", "z Zamościa", "z Rzeszowa" , "z Sanoka", "z Krosna" , "z Darłowa",
"z Warszawy", "z Krakowa", "z Poznania", "ze Szczecina", "z Radomia", "z Gdańska", "z Giżycka", "ze Szczawnicy", "z Łodzi", "z Kielc", "z Żywca", "z Zakopanego", "z Lublina", "z Jeleniej Góry" , "ze Szklarskiej Poręby", "z Malborka" ,"z Tarnowa", "z Bytomia",  "z Rynu", "z Brodnicy", "z Legnicy");

var locali_ppl2 = new Array("panią Anię", "panią Katarzynę", "panią Marię", "panią Mariannę", "panią Marzenę", "panią Ewę", "panią Grażynę", "panią Ludmiłę", "panią Weronikę", "panią Zofię", "panią Czesławę", "panią Alicję", "panią Paulinę", "panią Krystynę", "panią Ilonę", "panią Barbarę", "panią Bożenę",
"pana Mariana", "pana Henryka", "pana Miłosza", "pana Marka", "pana Dariusza", "pana Macieja", "pana Romana", "pana Wiesława", "pana Piotra", "pana Jana"
);

var locali_ppl = new Array(
"pani Ania", "pani Alicja", "pani Agata", "pani Agnieszka",
"pani Barbara", "pani Bożena",
"pani Czesława",
"pani Daria", "pani Danuta",
"pani Ewa", "pani Edyta", "pani Ewelina",
"pani Grażyna",
"pani Halina", "pani Hanna",
"pani Ilona", "pani Izabela", "pani Iwona",
"pani Krystyna", "pani Katarzyna",
"pani Ludmiła",
"pani Maria", "pani Marianna", "pani Marzena",
"pani Natalia",
"pani Paulina",
"pani Sonia",
"pani Teresa",
"pani Urszula",
"pani Weronika",
"pani Zofia", "pani Zuzanna",
"pan Adam", "pan Andrzej",
"pan Bartosz", "pan Bogusław",
"pan Dariusz",
"pan Edward",
"pan Henryk",
"pan Jan", "pan Janusz",
"pan Marian", "pan Miłosz", "pan Marek", "pan Maciej",
"pan Piotr", "pan Paweł",
"pan Roman",
"pan Wiesław"
);

var init_usrs = Math.floor((Math.random()*100)+200);
var buyers = new Array();

[...]

function genPpl()
{
  var peopl_diff = Math.floor((Math.random()*40));
  msg ="W tym momencie na stronie jest "+(init_usrs+peopl_diff)+' '+"użytkowników";
  $.pnotify({
      title: '',
      text: msg,
      animation: 'fade',
      type: 'success',
      addclass: "stack-bottomright",
      stack: stack_bottomright
    });
  setTimeout(genPpl, 60000);
}

function genBuy()
{
  //alert('smth');
  var where = Math.floor((Math.random()*locali_dop.length));
  var who = Math.floor((Math.random()*locali_ppl.length));
  var when = new Date().getTime();

  var temp_buy = new Array(when, who, where);
  buyers.push(temp_buy);
  showLast3Buyers();

  setTimeout(genBuy, Math.floor((Math.random()*30000)+15000));
}

function showLast3Buyers()
{
  var lastBuys='';
  var l = buyers.length;

  for(var i=0; i&lt;3; i++){
    if(l&gt;i)
    {
      var time = new Date().getTime() - buyers[l-i-1][0];
      var time_in_sec = Math.floor(time/1000);
      var time_str= '';
      if(time_in_sec&lt;3)
        time_str ="przed chwilą"
      else
      if(time_in_sec&gt;60)
      {
        var time_in_min = Math.floor(time_in_sec/60);
        time_in_sec -= time_in_min*60;
        time_str = time_in_min+ "min. "+time_in_sec+' '+"sekund temu"
      }
      else
        time_str = time_in_sec+' '+"sekund temu"
      lastBuys +=  time_str + ' '+ locali_ppl[buyers[l-i-1][1]]+' '+ locali_dop[buyers[l-i-1][2]]+" ";
    }
  }

  $.pnotify({
      title: "Ostatnio dokonane zakupy:",
      text: lastBuys,
      animation: 'fade',
      type: 'success',
      addclass: "stack-bottomright",
      stack: stack_bottomright
  });
  //setTimeout(showLast3Buyers, 30000);
}

Pogratulować pomysłowości.