Jak za jeden týden připravit kompletní aplikaci pro Windows 8 s použitím technologií HTML5, CSS3 a JavaScript – den 0

MyWindows.cz

home archív foto video win8dev kontakt

Jak za jeden týden připravit kompletní aplikaci pro Windows 8 s použitím technologií HTML5, CSS3 a JavaScript – den 0

Cílem této série celkem pěti článků (den 0 až den 4) je poskytnout praktický recept, podle něhož lze zcela od začátku vytvořit aplikaci pro Windows 8. Také si přečtěte předchozí článek Začínáme s vývojem Metro aplikací.

Jako příklad použiji aplikaci s názvem UrzaGatherer, která sběratelům karet hry Magic: The Gathering pomáhá pracovat s jejich sbírkou karet.

Aplikace UrzaGatherer byla původně vyvinuta s použitím technologie WPF 4.0, ale při vývoji verze pro Windows 8 jsem se rozhodl použít technologie HTML5, CSS3 a JavaScript.

Abyste mohli aplikaci začít vytvářet, budete potřebovat následující:

  • Počítač s Windows 8 Release Preview (můžete jej stáhnout zde)
  • Visual Studio 2012 (verzi Express RC pro Windows 8 je možné stáhnout zde)

Kompletní řešení pro "den 0" naleznete zde - http://www.catuhe.com/msdn/urza/day0.zip.

Celý projekt byl následně aktualizován pro Release Preview verzi Windows 8 (z Customer Preview), pro kompletní seriál (dny 0 - 4, bude popsáno v dalších článcích), je k dispozici zde: http://mywindows.cz/media/urza/UrzaGatherer.zip

Vytvoření projektu

Ze všeho nejdřív je nutné vytvořit prázdný projekt (můžete samozřejmě vytvořit projekt, který už je předem více připravený, například aplikaci Grid Application, ale naším cílem je porozumět tomu, jak je spolu vše provázáno) pomocí položky nabídky File/New Project (Soubor/Nový projekt):

Po vytvoření bude projekt obsahovat pouze nezbytně nutné soubory:

Vytvoření potřebných prostředků

Soubor package.appxmanifest představuje popis vaší aplikace pro systém Windows 8. Obsahuje především popis aplikace a informace o tom, jaká loga jsou s ní spojená:

Rád ke svým aplikacím přidávám loga a barvy, protože je to snadný způsob, jak aplikace zdokonalit.

Například úvodní obrazovka je opravdu důležitá, protože je tím prvním, co uživatel z vaší aplikace uvidí (a jak jistě víte, na prvním dojmu skutečně záleží):

Někdy se jedná o nejtěžší část vývoje, protože vývojáři často nebývají také návrháři.

Vytvoření struktury projektu

Tato část se odvíjí od vašeho způsobu myšlení. Já osobně jsem pro tento projekt zvolil následující strukturu:

  • Složka pro prostředky (/images)
  • Složka pro kód JavaScript, který nesouvisí se stránkami (/js)
  • Složka pro stránky (/pages)
  • Složka pro každou stránku (/pages/xxx), v níž vytvářím soubory CSS, JS a HTML (home.js, home.html, home.css)
  • Kořenová stránka s názvem default.html (spolu s příslušnými soubory CSS a JS)

Připojení k datům

Jakmile budete mít připravené prostředky a strukturu projektu, můžete do složky js přidat soubor data.js, který bude zajišťovat všechny operace související s daty.

Data pro aplikaci UrzaGatherer tvoří:

  • soubor all.json, který popisuje všechny podporované karty
  • seznam obrázků karet
  • seznam log pro jednotlivá rozšíření (karty patří do rozšíření, které zase patří do bloku)
  • seznam log pro jednotlivé bloky

Když začnete od prázdného souboru data.js, je nutné vytvořit automatickou anonymní funkci:

(function () { })();

Uvnitř této funkce se můžete připojit k datům. V aplikaci UrzaGatherer jsou data uložená v souboru JSON, který je příliš velký na to, abychom ho pokaždé znovu stahovali (cca 8 MB). Proto ho načteme pouze jednou a uložíme ho do místní složky:

(function () {

    var blocks = new WinJS.Binding.List();
    var expansions = new WinJS.Binding.List();
    var root = "http://urzagatherer.blob.core.windows.net";

    var processBlocks = function (data) {
        var result = JSON.parse(data);

        for (var blockIndex = 0; blockIndex < result.length; blockIndex++) {
            var block = result[blockIndex];

            block.logo = root + "/blocks/" + block.name.replace(":", "_") + ".png";
            blocks.push(block);

            var sortedExpansions = block.expansions.sort(expansionSorter);

            for (var expansionIndex = 0; expansionIndex < sortedExpansions.length; expansionIndex++) {
                var expansion = sortedExpansions[expansionIndex];
                expansion.block = block;
                expansion.logo = root + "/logos/" + expansion.name.replace(":", "_") + ".png";
                expansions.push(expansion);
            }
        }
    }

    var getBlocksDistant = function (onload) {

        var localFolder = Windows.Storage.ApplicationData.current.localFolder;
        var requestStr = root + "/cards/all.json";

        WinJS.xhr({ url: requestStr }).then(function (request) {
            processBlocks(request.responseText);

            localFolder.createFileAsync("all.json",             Windows.Storage.CreationCollisionOption.replaceExisting).then(function (file) {
                Windows.Storage.FileIO.writeTextAsync(file, request.responseText);
            });

            if (onload)
                onload();
        });
    }

    var getBlocks = function (onload) {
        var localFolder = Windows.Storage.ApplicationData.current.localFolder;

        localFolder.getFileAsync("all.json").done(function (file) {
            return Windows.Storage.FileIO.readTextAsync(file).then(function (data) {
                processBlocks(data);

                if (onload)
                    onload();
            });
        }, function () {
            getBlocksDistant(onload);
        });
    }

    var expansionSorter = function (i0, i1) {
        if (i0.orderInBlock > i1.orderInBlock)
            return 1;
        else if (i0.orderInBlock < i1.orderInBlock)
            return -1;
        return 0;
    };

    WinJS.Namespace.define("UrzaGatherer", {
        Blocks: blocks,
        Expansions: expansions,
        Init: getBlocks
    });
})();

Pomocí funkce WinJS.Namespace.define můžete deklarovat globální objekt (s názvem UrzaGatherer), který bude k dispozici všude ve vašem kódu.

Funkce Init začíná tím, že se pokusí načíst data z místní složky, a pokud se jí to nepodaří, stáhne je s použitím funkce WinJS.xhr (http://msdn.microsoft.com/en-us/library/windows/apps/br229787.aspx). Funkce Init také v parametru přijímá funkci, pomocí níž signalizuje dostupnost dat. Pomocí této funkce skrývám kroužek zobrazený při čekání na dokončení operace (indikátor průběhu v režimu „kroužku“).

Příprava úvodní stránky

Systém navigace

Stránka default.html je úvodní stránka, tedy stránka, která se uživateli zobrazí po spuštění aplikace. Tato stránka zodpovídá za vytvoření systému navigace:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>UrzaGatherer</title>

    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.0.6/css/ui-light.css" rel="stylesheet">
    <script src="//Microsoft.WinJS.0.6/js/base.js"></script>
    <script src="//Microsoft.WinJS.0.6/js/ui.js"></script>

    <!-- UrzaGatherer references -->
    <link href="/default.css" rel="stylesheet">
    <script src="/js/data.js"></script>
    <script src="/js/tools.js"></script>
    <script src="/js/navigator.js"></script>
    <script src="/default.js"></script>
</head>
<body>
    <div id="contenthost" data-win-control="UrzaGatherer.PageControlNavigator" data-win-options="{home: '/pages/home/home.html'}"></div>
</body>
</html>

Tato stránka je poměrně jednoduchá: přidá odkazy na soubory WinJS a poté načte výchozí šablony stylů a kód JavaScript.

Obsahuje pouze jeden element div, který představuje hostitelský element, v rámci něhož se budou načítat podřízené stránky. To je důležitý princip pro pochopení toho, jak funguje navigace při použití kódu HTML5/JavaScript v systému Windows 8. Platí, že stránky se nenačítají jako kořenové stránky, ale jako stránky podřízené stránce default.html.

Pro zajištění této funkčnosti je nutné vytvořit stránku navigator.js (můžete ji zkopírovat z libovolné šablony projektu v sadě Visual Studio 11, například Grid Application):

(function () {
    "use strict";

    var appView = Windows.UI.ViewManagement.ApplicationView;
    var displayProps = Windows.Graphics.Display.DisplayProperties;
    var nav = WinJS.Navigation;
    var ui = WinJS.UI;
    var utils = WinJS.Utilities;

    WinJS.Namespace.define("UrzaGatherer", {
        PageControlNavigator: WinJS.Class.define(
            // Define the constructor function for the PageControlNavigator.
            function PageControlNavigator(element, options) {
                this.element = element || document.createElement("div");
                this.element.appendChild(this._createPageElement());

                this.home = options.home;
                this.lastViewstate = appView.value;

                nav.onnavigated = this._navigated.bind(this);
                window.onresize = this._resized.bind(this);

                document.body.onkeyup = this._keyupHandler.bind(this);
                document.body.onkeypress = this._keypressHandler.bind(this);
                document.body.onmspointerup = this._mspointerupHandler.bind(this);

                UrzaGatherer.navigator = this;
            }, {
                /// <field domElement="true" />
                element: null,
                home: "",
                lastViewstate: 0,

                // This function creates a new container for each page.
                _createPageElement: function () {
                    var element = document.createElement("div");
                    element.style.width = "100%";
                    element.style.height = "100%";
                    return element;
                },

                // This function responds to keypresses to only navigate when
                // the backspace key is not used elsewhere.
                _keypressHandler: function (args) {
                    if (args.key === "Backspace") {
                        setImmediate(function () {
                            nav.back();
                        });
                    }
                },

                // This function responds to keyup to enable keyboard navigation.
                _keyupHandler: function (args) {
                    if ((args.key === "Left" && args.altKey) || (args.key === "BrowserBack")) {
                        setImmediate(function () {
                            nav.back();
                        });
                    } else if ((args.key === "Right" && args.altKey) || (args.key === "BrowserForward")) {
                        setImmediate(function () {
                            nav.forward();
                        });
                    }
                },

                _mspointerupHandler: function (args) {
                    if (args.button === 3) {
                        setImmediate(function () {
                            nav.back();
                        });
                    } else if (args.button === 4) {
                        setImmediate(function () {
                            nav.forward();
                        });
                    }
                },

                // This function responds to navigation by adding new pages
                // to the DOM.
                _navigated: function (args) {
                    var that = this;
                    var newElement = that._createPageElement();
                    var parentedComplete;
                    var parented = new WinJS.Promise(function (c) { parentedComplete = c; });

                    args.detail.setPromise(
                        WinJS.Promise.timeout().then(function () {
                            if (that.pageElement.winControl && that.pageElement.winControl.unload) {
                                that.pageElement.winControl.unload();
                            }
                            return WinJS.UI.Pages.render(args.detail.location, newElement, args.detail.state, parented);
                        }).then(function parentElement(control) {
                            that._previousPage = newElement.winControl;
                            that.element.appendChild(newElement);
                            that.element.removeChild(that.pageElement);

                            parentedComplete();

                            var offset = { top: "0px", left: "50px" };
                            var enterPage = WinJS.UI.Animation.enterPage(newElement, offset);
                            enterPage.then(function () {
                                document.body.focus();
                                that.navigated();

                                setImmediate(function () {
                                    if (that._previousPage.afterPageEnter) {
                                        that._previousPage.afterPageEnter(newElement);
                                    }
                                });
                            });
                        })
                    );
                },

                _resized: function (args) {
                    if (this.pageControl && this.pageControl.updateLayout) {
                        this.pageControl.updateLayout.call(this.pageControl, this.pageElement, appView.value, this.lastViewstate);
                    }
                    this.lastViewstate = appView.value;
                },

                // This function updates application controls once a navigation
                // has completed.
                navigated: function () {
                    // Do application specific on-navigated work here
                    var backButton = this.pageElement.querySelector("header[role=banner] .win-backbutton");
                    if (backButton) {
                        backButton.onclick = function () {
                            setImmediate(function () {
                                nav.back();
                            });
                        };

                        if (nav.canGoBack) {

                            backButton.removeAttribute("disabled");
                        } else {
                            backButton.setAttribute("disabled", "disabled");
                        }
                    }
                },

                // This is the PageControlNavigator object.
                pageControl: {
                    get: function () { return this.pageElement && this.pageElement.winControl; }
                },

                // This is the root element of the current page.
                pageElement: {
                    get: function () { return this.element.firstElementChild; }
                }
            }
        )
    });
})();

Jak vidíte, třída PageControlNavigator představuje ovládací prvek, který po odstranění předchozí stránky načte novou stránku a přidá ji jako podřízený element. Je nutné tento princip dobře pochopit, protože z něj vyplývá, že všechny načtené šablony stylů CSS a skripty zůstávají aktivní a existují v jedinečné globální stránce (http://msdn.microsoft.com/en-us/library/windows/apps/hh452768.aspx).

Použití stylů ve stránce

Šablona default.css je kořenová šablona a jak teď už víte, používá se pro všechny načtené stránky. Tento soubor zodpovídá za nastavení globální struktury, což znamená například:

  • případné pozadí
  • rozložení poskytující prostor pro hlavičku (název a tlačítko Zpět) a obsah

Do tohoto souboru také ukládám globální styly, které používám v celé své aplikaci, například třídu hidden (umožňující skrývat elementy):

html {
    cursor: default;
}

body {
    background-image: url('images/background.jpg');
    background-size: 100% 100%
}

#contenthost {
    height: 100%;
    width: 100%;
}

.fragment {
    /* Define a grid with rows for a banner and a body */
    -ms-grid-columns: 1fr;
    -ms-grid-rows: 133px 1fr 0px;
    display: -ms-grid;
    height: 100%;
    width: 100%;
}

.fragment header[role=banner] {
    /* Define a grid with columns for the back button and page title. */
    -ms-grid-columns: 120px 1fr;
    -ms-grid-rows: 1fr;
    display: -ms-grid;
}

.fragment header[role=banner] .win-backbutton {
    margin-left: 39px;
    margin-top: 59px;
}

.fragment header[role=banner] .titlearea {
    -ms-grid-column: 2;
    margin-top: 37px;
}

.fragment header[role=banner] .titlearea .pagetitle {
    width: calc(100% - 20px);
}

.fragment section[role=main] {
    -ms-grid-row: 2;
    height: 100%;
    width: 100%;
}

.hidden {
    display: none;
}

Můžete si všimnout, že nastavení display:-ms-grid používám všude, kde to je možné, protože systém mřížek ve specifikaci CSS3 (http://msdn.microsoft.com/en-us/library/windows/apps/hh465327.aspx) umožňuje opravdu snadno vytvořit rozložení a zarovnání.

Při vytváření pozadí si samozřejmě můžete vyžádat pomoc od návrháře, nebo s použitím jednoduchého grafického nástroje můžete vytvořit tenký přechod, jak je tento:

Vytvoření výchozí obrazovky

Uživatelé jako první uvidí výchozí obrazovku, na níž chci zobrazit bloky, které jsou k dispozici, spolu s jejich rozšířeními.

Základní verze stránky vypadá takto:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>UrzaGatherer</title>
    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.0.6/css/ui-light.css" rel="stylesheet">
    <script src="//Microsoft.WinJS.0.6/js/base.js"></script>
    <script src="//Microsoft.WinJS.0.6/js/ui.js"></script>
    <!-- UrzaGatherer references -->
    <link href="home.css" rel="stylesheet">
    <script src="home.js"></script>
</head>
<body>
    <!--Content-->
    <div class="home fragment">
        <header aria-label="Header content" role="banner">
            <button class="win-backbutton" aria-label="Back" disabled></button>
            <h1 class="titlearea win-type-ellipsis"><span class="pagetitle">UrzaGatherer</span>
            </h1>
        </header>
        <section aria-label="Main content" role="main">

        </section>
    </div>
</body>
</html>

Všimněte si banneru (záhlaví) a části pro umístění obsahu.

K tomu jsem použil ovládací prvek WinJS.UI.ListView. Pomocí tohoto ovládacího prvku je možné zobrazit seskupený seznam hodnot (v tomto případě zobrazuje seznam rozšíření seskupených podle bloků):

<div class="blocksList" aria-label="List of blocks" data-win-control="WinJS.UI.ListView"
    data-win-options="{itemTemplate:select('.itemTemplate'), groupHeaderTemplate:select('.headerTemplate')
                       , selectionMode:'none', swipeBehavior:'none', tapBehavior:'invoke',
                        layout:{type:WinJS.UI.GridLayout}}">
</div>

Ovládací prvek se odkazuje na dvě šablony (itemTemplate a headerTemplate), které definují, jak mají být vykresleny jednotlivé položky a záhlaví:

<div class="headerTemplate" data-win-control="WinJS.Binding.Template">
    <div class="header-title" data-win-bind="innerText: name">
    </div>
    <img class="item-image" data-win-bind="src: logo" src="#" />
</div>
<div class="itemTemplate" data-win-control="WinJS.Binding.Template">
    <img class="item-image" data-win-bind="src: logo" src="#" />
    <div class="item-overlay">
        <h4 class="item-title" data-win-bind="textContent: name"></h4>
    </div>
</div>

Jak jsem už zmínil, rozložení se vždy snažím vytvořit pomocí mřížky CSS3. Toto jsou například styly pro položky seznamu:

.home .blocksList .win-item {
    -ms-grid-columns: 1fr;
    -ms-grid-rows: 1fr 30px;
    display: -ms-grid;
    height: 130px;
    width: 260px;
    background: white;
    outline: rgba(0, 0, 0, 0.8) solid 2px;
}

.home .blocksList .win-item:hover {
    outline: #5F38FF solid 2px;
}
.home .blocksList .win-item .item-image-container {
	-ms-grid-columns: 1fr;
	-ms-grid-rows: 1fr;
	-ms-grid-row: 1;
	display: -ms-grid;
	padding: 4px;
	-ms-transition: opacity ease-out 0.2s, -ms-transform ease-out 0.2s;
	-ms-transform: scale(1.0, 1.0);
}
.home .blocksList .win-item .item-image-container:hover {
	opacity: 0.9;
-ms-transform: scale(1.1, 1.1);
}
.home .blocksList .win-item .item-image {
	-ms-grid-row: 1;
	-ms-grid-column-align: center;
	-ms-grid-row-align: center;
	max-height: 90px;
}
.home .blocksList .win-item .item-overlay {
	-ms-grid-row: 2;
	padding: 3px 15px 2px;
	background-color: rgba(0, 0, 0, 0.8);
}

Pomocí přechodů definovaných ve specifikaci CSS3 (http://msdn.microsoft.com/en-us/library/windows/apps/Hh781227.aspx) lze také velice snadno vyřešit stav při umístění ukazatele myši nad určitý prvek (hover). Všimněte si použití předpony ".home", která zajišťuje, že tyto styly budou použity pouze pro stránku home.html (kvůli systému navigace založenému na jedné stránce).

Ovládací prvek blocksList je následně naplněn daty s použitím funkce createGrouped třídy WinJS.Binding.List:

var groupDataSource = UrzaGatherer.Expansions.createGrouped(this.groupKeySelector,
                                                            this.groupDataSelector, this.groupCompare);

ui.setOptions(listView, {
    itemDataSource: groupDataSource.dataSource,
    groupDataSource: groupDataSource.groups.dataSource,
    layout: new ui.GridLayout({ groupHeaderPosition: "top" })
});

Důležitým prvkem je tady funkce groupKeySelector. Pomocí této funkce je vytvořen klíč pro každou skupinu. Tento klíč slouží k seskupení položek a bude mít také další využití po přidání ovládacího prvku SemanticZoom:

groupKeySelector: function (item) { return item.block.name + "*" + item.block.index; },

Upozorňuji, že MUSÍTE vrátit řetězec, nikoli číslo!

Funkce groupCompare přijímá klíče, které seřadí:

groupCompare: function (i0, i1) {   
	var index0 = parseInt(i0.split("*")[1]);   
	var index1 = parseInt(i1.split("*")[1]);   
	return index1 - index0; 
}

Přidání vlastního ovládacího prvku pro obrázky

Problém s obrázky spočívá v tom, že jejich stahování trvá dlouhou dobu a po stažení se zobrazují bez animace, což působí jako nepodařený efekt postupného zobrazování. Proto jsem se rozhodl vytvořit vlastní ovládací prvek, který pomocí hezké animace zajistí, aby se obrázky zobrazovaly plynule.

Deklarace vlastního ovládacího prvku je velmi jednoduchá (s použitím funkce WinJS.Class.define):

(function () {

    var delayImageLoader = WinJS.Class.define(
            function (element, options) {
                this._element = element || document.createElement("div");
                this.element.winControl = this;
                WinJS.Utilities.addClass(this.element, "imageLoader");
                WinJS.Utilities.query("img", element).forEach(function (img) {
                    img.addEventListener("load", function () {
                        WinJS.Utilities.addClass(img, "loaded");
                    });
                });
            },
            {
                element: {
                    get: function () { return this._element; }
                },
            });

    WinJS.Namespace.define("UrzaGatherer.Tools", {
        DelayImageLoader: delayImageLoader
    });
})();

Jak vidíte, ovládací prvek vyhledává podřízené obrázky a přidává naslouchací proces pro událostload. Všechnu práci vlastně odvádějí šablony stylů CSS, protože ovládací prvek pouze na začátku přidává třídu imageLoader a na konci třídu loaded.

Tyto dvě třídy jsou definovány v souboru default.css:

.imageLoader img {
	opacity: 0;
	-ms-transform: scale(0.8, 0.8);
}
.imageLoader img.loaded {
	opacity: 1;
	-ms-transition: opacity ease-out 0.2s, -ms-transform ease-out 0.2s;
	-ms-transform: scale(1, 1);
}

S použitím přechodů definovaných ve specifikaci CSS3 se obrázek po stažení plynule zobrazí.

Přidání sémantického přiblížení

Nakonec jsem na domovskou stránku přidal sémantické přiblížení (http://msdn.microsoft.com/en-us/library/windows/apps/hh465492.aspx), které uživatelům umožňuje rychle přejít na určitý blok:

Aby to bylo možné, je nutné původní ovládací prvek ListView doplnit o další (přiblížený) ovládací prvek ListView v rámci ovládacího prvku WinJS.UI.SemanticZoom.

<div class="zoomControl" data-win-control="WinJS.UI.SemanticZoom">
    <div class="blocksList" aria-label="List of blocks" data-win-control="WinJS.UI.ListView"
     data-win-options="{itemTemplate:select('.itemTemplate'), groupHeaderTemplate:select('.headerTemplate'), 
                            selectionMode:'none', swipeBehavior:'none', tapBehavior:'invoke',  
                            layout:{type:WinJS.UI.GridLayout}}">
    </div>
    <div class="zoomedList" aria-label="Zoomed List of blocks" data-win-control="WinJS.UI.ListView"
     data-win-options="{itemTemplate:select('.semanticZoomTemplate'), selectionMode:'none', 
                         swipeBehavior:'none', tapBehavior:'invoke',  layout:{type:WinJS.UI.GridLayout}}">
    </div>
</div>

Pro zajištění synchronizace těchto dvou ovládacích prvků ListView je nutné použít stejný zdroj dat pro skupinu v prvním seznamu a položky v druhém seznamu:

var groupDataSource = UrzaGatherer.Expansions.createGrouped(this.groupKeySelector,
                                                            this.groupDataSelector, this.groupCompare);

ui.setOptions(listView, {
    itemDataSource: groupDataSource.dataSource,
    groupDataSource: groupDataSource.groups.dataSource,
    layout: new ui.GridLayout({ groupHeaderPosition: "top" })
});

ui.setOptions(zoomedListView, {
    itemDataSource: groupDataSource.groups.dataSource
});

Anglický originál tohoto článku od David Catuhe je dostupný na MSDN.


win8 soutěž win8 vývoj

Komentáře

  1. 1 Michael Bujnovský 21.06.12, 13:45:03

    Proč HTML 5, CSS3, javascript a ne standardní WPF?

  2. 2 Radek Hulán 21.06.12, 13:59:31

    [1] WinRT / C#.. Protože HTML / CSS / JS zná mnohem více lidí a je to jedna z vývojářských technologií Windows 8.

  3. 3 Michal Haták 22.06.12, 16:03:37

    Jak je to s metro aplikacemi a win7 ? Viděl jsem už nějaké aplikace (konkrétně třeba: http://windows.github.com/), děkuji za odpověď :)

  4. 4 Adam Kalisz 28.06.12, 21:56:51

    Ten GitHub je pěkný! Jinak Metro pod verzí Windows starší Windows 8 nepojedou. Je to dáno absencí běhového prostředí WinRT.

Nový komentář

Pro přidání komentáře se musíte  registrovat Facebookem. Je to snadné a bezpečné.