pocet = x => x * x;
-Console.WriteLine(pocet(4));
-Action pozdrav = () => Console.WriteLine("ahoj");
-pozdrav();
-```
diff --git a/content/programovani/4-rocnik/08-async.en.md b/content/programovani/4-rocnik/08-async.en.md
deleted file mode 100644
index 98da3e9..0000000
--- a/content/programovani/4-rocnik/08-async.en.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
-nazev: Async and await
-popis: Work that waits without freezing the way you write it.
----
diff --git a/content/programovani/4-rocnik/08-async.md b/content/programovani/4-rocnik/08-async.md
deleted file mode 100644
index 9baebaf..0000000
--- a/content/programovani/4-rocnik/08-async.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-id: cs-4-async
-nazev: Async a await
-popis: Práce, která počká, aniž by zamkla zbytek zápisu.
----
-
-# Výklad
-
-## await
-*počkej*
-
-- await Task.Delay čeká, ale zápis zůstane řádek za řádkem.
-- Metoda s await má v hlavičce async a vrací Task.
-- V krátkém programu smí await stát i na nejvyšší úrovni.
-
-> Tip: Delay(300) čeká 300 milisekund. Výpis hotovo přijde až po něm.
-
-## Task
-*slib*
-
-- Task je slib, že práce doběhne.
-- await slib rozbalí. Bez await byste měli slib, ne výsledek.
-- Na síť a soubory se await hodí. Na 2 + 2 ne.
-
-> Tip: Dejte za první výpis ještě jeden await a druhý řádek.
-
-# Kód
-
-```
-Console.WriteLine("začátek");
-await Task.Delay(300);
-Console.WriteLine("hotovo");
-```
diff --git a/content/programove-vybaveni/1-rocnik/01-software-a-licence.en.md b/content/programove-vybaveni/1-rocnik/01-software-a-licence.en.md
deleted file mode 100644
index 77b2182..0000000
--- a/content/programove-vybaveni/1-rocnik/01-software-a-licence.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Software and licences
-popis: System and application programs, and the terms for using them.
-nazev_cviceni: Quiz: software and licences
-nadpis_kvizu: Name the kind of program and the kind of licence
----
diff --git a/content/programove-vybaveni/1-rocnik/01-software-a-licence.md b/content/programove-vybaveni/1-rocnik/01-software-a-licence.md
deleted file mode 100644
index e40dee4..0000000
--- a/content/programove-vybaveni/1-rocnik/01-software-a-licence.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-1-software-a-licence
-nazev: Software a licence
-popis: Systémové a aplikační programy a to, za jakých podmínek se smí používat.
-nazev_cviceni: Kvíz: software a licence
-nadpis_kvizu: Poznejte druh programu a druh licence
----
-
-# Výklad
-
-## Dva druhy programů
-*Software*
-
-- Software je program. Hardware je zařízení. Samotný počítač bez programu nic neudělá.
-- Operační systém řídí počítač: Windows, Linux, macOS. Ve škole pracujeme ve Windows.
-- Aplikační software řeší jednu práci: Word dokument, Excel tabulku, prohlížeč web.
-- Ovladač je malý program k zařízení. Tiskárna bez ovladače netiskne, i když je zapnutá.
-
-> Tip: Když program spadne a systém běží dál, spadla aplikace. Když nesvítí plocha, spadl systém.
-
-## Licence
-*Podmínky*
-
-- Licence říká, kdo program smí nainstalovat a na kolik počítačů.
-- Komerční program se kupuje, nebo se platí předplatné. Microsoft 365 je předplatné.
-- OEM licence je přivázaná k jednomu počítači. Na nový stroj se nepřetahuje.
-- Stažený program bez licence není zdarma. Je to kopie, kterou škola nesmí používat.
-
-> Tip: Školní účet Microsoft 365 nepatří na domácí počítače navždy. Platí, dokud škola předplatné má.
-
-## Zdarma není jeden pojem
-*Freeware a open source*
-
-- Freeware je zdarma, ale zdrojový kód nedostanete. Smíte ho používat za podmínek autora.
-- Open source má otevřený kód. GPL navíc chce, aby odvozené programy zůstaly otevřené.
-- Zkušební verze (shareware) po čase skončí, nebo omezí funkce. Není to trvalá licence.
-- Na školní počítač patří jen programy, které škola schválila. Vlastní instalace se hlásí učiteli.
-
-> Tip: Cena nula neznamená, že smíte program rozdávat dál. I freeware má licenci.
-
-## Zadání: šest programů
-*Cvičení*
-
-- Do tabulky zapište šest programů ze školního počítače: systém, prohlížeč, Word, Excel a dva další.
-- U každého napište, jestli je systémový, nebo aplikační.
-- U Wordu a Excelu napište, jaká licence ve škole je: předplatné Microsoft 365, ne domácí krabice.
-- Soubor pojmenujte licence.txt a uložte ho do Dokumenty. Zatím nic neinstalujte.
-
-> Tip: Název systému zjistíte příkazem winver. Okno ukáže edici Windows a verzi.
-
-# Kvíz
-
-## Kam patří Microsoft Word?
-- Mezi operační systémy
-- [x] Mezi aplikační software
-- Mezi ovladače tiskárny
-- Mezi hardware
-> Word řeší jednu práci, dokument. Systém pod ním je Windows.
-
-## Co znamená OEM licence Windows?
-- Smí se přenést na každý další počítač
-- [x] Je vázaná na jeden počítač
-- Je to totéž co GPL
-- Platí jen 14 dní
-> OEM se prodává s počítačem a na jiný stroj se nepřesouvá.
-
-## Čím se freeware liší od open source?
-- Freeware má otevřený zdrojový kód
-- [x] Freeware je zdarma, ale kód obvykle nedostanete
-- Open source se nesmí používat ve škole
-- Je to totéž
-> Zdarma a otevřený kód jsou dvě různé věci. GPL je otevřená licence, ne jen nulová cena.
-
-## Který program je operační systém?
-- Excel
-- [x] Windows
-- WordPress
-- Power BI
-> Windows řídí počítač. Ostatní jsou aplikace, které na systému běží.
diff --git a/content/programove-vybaveni/1-rocnik/02-operacni-system.en.md b/content/programove-vybaveni/1-rocnik/02-operacni-system.en.md
deleted file mode 100644
index 572309c..0000000
--- a/content/programove-vybaveni/1-rocnik/02-operacni-system.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: The operating system
-popis: What the system does between the user and the hardware.
-nazev_cviceni: Quiz: the operating system
-nadpis_kvizu: Name the role of the system
----
diff --git a/content/programove-vybaveni/1-rocnik/02-operacni-system.md b/content/programove-vybaveni/1-rocnik/02-operacni-system.md
deleted file mode 100644
index 352a114..0000000
--- a/content/programove-vybaveni/1-rocnik/02-operacni-system.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-1-operacni-system
-nazev: Operační systém
-popis: Co systém dělá mezi uživatelem a hardwarem.
-nazev_cviceni: Kvíz: operační systém
-nadpis_kvizu: Poznejte roli systému
----
-
-# Výklad
-
-## Mezi člověkem a strojem
-*Role*
-
-- Systém spouští programy, hlídá paměť a přiděluje čas procesoru.
-- Souborový systém ukládá data na disk. Ve Windows je to NTFS.
-- Ovladač si systém načte, aby mluvil s tiskárnou, diskem a sítí.
-- Uživatel vidí plochu a okna. Jádro pod tím nevidí a do něj nesahá.
-
-> Tip: Když dva programy chtějí tiskárnu najednou, rozhodne systém. Samy se nedomluví.
-
-## Tři systémy
-*Windows, Linux, macOS*
-
-- Školní učebna je Windows. Příkazy a obrazovky v tomto předmětu jsou z Windows.
-- Linux je častý na serverech. Stejný dokument v Excelu tam sám od sebe není.
-- macOS běží na počítačích Apple. Zkratka Ctrl je tam často Cmd.
-- Soubor .docx je dokument Wordu. Není to program a není to systém.
-
-> Tip: Přípona souboru říká, čím ho otevřít. Systém podle ní vybere aplikaci.
-
-## Start počítače
-*Až po přihlášení*
-
-- Po zapnutí naběhne firmware, pak systém, pak přihlašovací obrazovka.
-- Dokud se nepřihlásíte, pracujete s účtem nikoho. Soubory uživatele jsou zamčené.
-- Každý účet má vlastní plochu a složku C:\Users\jméno.
-- Vypnutí přes nabídku Start uloží práci systému. Dlouhý stisk tlačítka je až poslední možnost.
-
-> Tip: Restart po aktualizaci není trest. Systém si domlouvá soubory, které za běhu nevymění.
-
-## Zadání: jaký systém to je
-*Cvičení*
-
-- Spusťte winver a opište edici a verzi, třeba Windows 11 Pro, verzi z okna.
-- Otevřete C:\Users a napište, které složky uživatelů tam jsou. Do cizích nesaháte.
-- Najděte jeden ovladač v názvu: ve Správci zařízení rozbalte Monitory nebo Tiskárny.
-- Zápis uložte jako system.txt do Dokumenty. Hesla ani sériová čísla nepište.
-
-> Tip: Správce zařízení je ve Windows devmgmt.msc. Žlutý vykřičník znamená chybějící nebo vadný ovladač.
-
-# Kvíz
-
-## Co dělá operační systém s pamětí?
-- Nic, paměť si berou programy samy
-- [x] Přiděluje ji programům
-- Maže ji při každém uložení dokumentu
-- Patří do Excelu
-> Program požádá, systém přidělí. Dva programy nesmí psát do stejného místa.
-
-## Jaký souborový systém používá školní Windows na disku?
-- [x] NTFS
-- Jen názvy souborů bez systému
-- SVG
-- MySQL
-> NTFS drží soubory, oprávnění i větší disky. Názvy složek jsou až uvnitř něj.
-
-## Kdy smíte sáhnout do složky jiného uživatele v C:\Users?
-- Kdykoli, je to jeden počítač
-- [x] Nesmíte, je to jeho účet
-- Jen když je složka na ploše
-- Když má dokument příponu docx
-> Každý účet má vlastní soubory. Cizí složka není společný šuplík.
-
-## Čím zjistíte edici Windows?
-- [x] Příkazem winver
-- Otevřením Excelu
-- Smazáním koše
-- Souborem .bat
-> winver otevře okno O systému Windows s edicí a verzí.
diff --git a/content/programove-vybaveni/1-rocnik/03-soubory-a-slozky.en.md b/content/programove-vybaveni/1-rocnik/03-soubory-a-slozky.en.md
deleted file mode 100644
index be64b98..0000000
--- a/content/programove-vybaveni/1-rocnik/03-soubory-a-slozky.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Files and folders
-popis: A path, an extension and order in Documents.
-nazev_cviceni: Quiz: files
-nadpis_kvizu: Build the path and the name
----
diff --git a/content/programove-vybaveni/1-rocnik/03-soubory-a-slozky.md b/content/programove-vybaveni/1-rocnik/03-soubory-a-slozky.md
deleted file mode 100644
index 8989f5a..0000000
--- a/content/programove-vybaveni/1-rocnik/03-soubory-a-slozky.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-1-soubory
-nazev: Soubory a složky
-popis: Cesta, přípona a pořádek v Dokumentech.
-nazev_cviceni: Kvíz: soubory
-nadpis_kvizu: Sestavte cestu a název
----
-
-# Výklad
-
-## Cesta
-*Kde soubor leží*
-
-- Cesta začíná diskem. C:\Users\novak\Documents\Skola\1-rocnik je složka, ne soubor.
-- Zpětné lomítko odděluje složky. Dopředné lomítko je na webu, ne ve Windows.
-- Dokumenty jsou %USERPROFILE%\Documents. Na každém účtu to vede jinam, příkaz je stejný.
-- Plocha je taky složka. Soubor jen na ploše se ve školním pořádku ztratí.
-
-> Tip: Do řádku adresy v Průzkumníku vložte cestu a Enter. Nemusíte proklikávat pět složek.
-
-## Přípona
-*Čím se soubor otevře*
-
-- prihlaska.docx je Word. znamky.xlsx je Excel. poznamka.txt je prostý text.
-- Windows přípony schovává. Zapněte Zobrazit, Zobrazit, Přípony názvů souborů.
-- Přejmenovat foto.jpg na foto.docx z něj dokument neudělá. Změní se jen štítek.
-- Dvojitá přípona faktura.pdf.exe je program, který se tváří jako dokument. Neotevírejte ho.
-
-> Tip: Když přípony vidíte, podvodný soubor poznáte dřív, než na něj kliknete.
-
-## Názvy
-*Co je zakázané*
-
-- V názvu nesmí být znaky \ / : * ? " < > |.
-- Mezera smí být, ale v dávkových souborech pak název patří do uvozovek.
-- Školní název je věcný: 2026-06-12-zadost.docx. Final-final2.docx není název.
-- Datum pište rok-měsíc-den. Složka se seřadí od nejstaršího sama.
-
-> Tip: Kopírování je Ctrl+C, vložení Ctrl+V, přesun začíná Ctrl+X. Přesun z flashky na disk soubor na flashce smaže.
-
-## Zadání: strom školy
-*Cvičení*
-
-- V Dokumentech založte Skola, v ní 1-rocnik a v ní slozky word, excel a system.
-- Do word uložte prázdný soubor poznamka.txt. Příponu ať je vidět.
-- Cesta ať končí na \Skola\1-rocnik\word\poznamka.txt.
-- Na ploše nic nenechávejte. Koš nevysypávejte, dokud nevíte, co v něm je.
-
-> Tip: Nová složka je Ctrl+Shift+N. Když se jmenuje Nová složka, hned ji přejmenujte.
-
-# Kvíz
-
-## Která cesta vede k souboru v Dokumentech?
-- [x] C:\Users\novak\Documents\Skola\1-rocnik\poznamka.txt
-- https://skola/poznamka.txt
-- poznamka.txt bez složky, to stačí vždy
-- C:/Users/novak s obyčejnými lomítky jako jediná správná forma
-> Ve Windows se složky oddělují zpětným lomítkem a cesta začíná diskem.
-
-## Co se stane, když foto.jpg přejmenujete na foto.docx?
-- Stane se z něj upravitelný dokument Word
-- [x] Změní se přípona, obsah zůstane obrázek
-- Soubor se uloží do koše
-- Vznikne tabulka Excel
-> Přípona je nápověda pro systém. Bajty obrázku se samy nepřepíšou na dokument.
-
-## Který název je na školní odevzdání nejlepší?
-- final2.docx
-- [x] 2026-06-12-zadost.docx
-- Nový dokument.docx
-- ???.docx
-> Datum rok-měsíc-den se seřadí a je z něj poznat verze. Otazník v názvu Windows ani nepustí.
-
-## Proč zapnout zobrazení přípon?
-- Aby šly soubory mazat
-- [x] Aby byl vidět skutečný typ, třeba pdf.exe
-- Protože bez přípony soubor nejde uložit
-- Excel to vyžaduje u každé buňky
-> Skrytá přípona schová, že faktura.pdf.exe je program.
diff --git a/content/programove-vybaveni/1-rocnik/04-instalace-a-aktualizace.en.md b/content/programove-vybaveni/1-rocnik/04-instalace-a-aktualizace.en.md
deleted file mode 100644
index 2408177..0000000
--- a/content/programove-vybaveni/1-rocnik/04-instalace-a-aktualizace.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Installation and updates
-popis: Where a program comes from and why the system is updated.
-nazev_cviceni: Quiz: installation
-nadpis_kvizu: Pick a safe source
----
diff --git a/content/programove-vybaveni/1-rocnik/04-instalace-a-aktualizace.md b/content/programove-vybaveni/1-rocnik/04-instalace-a-aktualizace.md
deleted file mode 100644
index 4b787c9..0000000
--- a/content/programove-vybaveni/1-rocnik/04-instalace-a-aktualizace.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-1-instalace
-nazev: Instalace a aktualizace
-popis: Odkud se program bere a proč se systém aktualizuje.
-nazev_cviceni: Kvíz: instalace
-nadpis_kvizu: Vyberte bezpečný zdroj
----
-
-# Výklad
-
-## Odkud instalovat
-*Zdroj*
-
-- Školní programy berte z Microsoft Store, z balíčku školy, nebo z webu výrobce.
-- Odkaz v mailu, tlačítko Stáhnout zdarma na cizím webu a crack nejsou zdroj.
-- Instalace chce souhlas správce. Okno Řízení uživatelských účtů se neodklepává poslepu.
-- Na plochu nepatří deset instalačních souborů. Po instalaci je smažte, program už je v systému.
-
-> Tip: Název souboru setup.exe nic neříká. Říká to vydavatel v okně účtů a adresa, odkud soubor přišel.
-
-## Aktualizace
-*Windows Update*
-
-- Aktualizace zašívá chyby, přes které se do počítače leze. Odkládat ji týdny je riziko.
-- Windows Update je v Nastavení, Windows Update. Školní počítač nechte doběhnout.
-- Restart po aktualizaci udělejte, když o něj systém požádá. Ne uprostřed testu, ale ten den.
-- Word a Excel se v Microsoft 365 aktualizují se sadou Office, ne jen s Windows.
-
-> Tip: Verze systému z winver se po velké aktualizaci změní. Když se měsíc nemění, aktualizace stojí.
-
-## Odinstalace
-*Aplikace*
-
-- Program se odebírá v Nastavení, Aplikace, Nainstalované aplikace. Ne smazáním zástupce.
-- Zástupce na ploše je jen odkaz. Smazáním ikony program v počítači zůstane.
-- Neznámý program, který jste neinstalovali, nejdřív nahlaste. Nemusí to být hra spolužáka.
-- Odinstalace nesmaže vaše dokumenty. Smaže program. Dokumenty si před velkým úklidem zkopírujte.
-
-> Tip: Když v seznamu aplikací Word není a zástupce ano, zástupce vede do prázdna, nebo na jiný disk.
-
-## Zadání: stav počítače
-*Cvičení*
-
-- Z winver opište verzi. V Windows Update napište, jestli je systém aktuální, nebo něco čeká.
-- V nainstalovaných aplikacích najděte Word nebo Microsoft 365 a opište název, jak ho seznam uvádí.
-- Najděte jeden program, který nepoznáváte, a zapište ho. Neodinstalovávejte ho bez učitele.
-- Uložte stav.txt do Skola\1-rocnik\system. Nic z internetu nestahujte.
-
-> Tip: Čas poslední aktualizace je ve Windows Update dole v historii. Stačí datum, ne seznam všech záplat.
-
-# Kvíz
-
-## Odkud se má školní program instalovat?
-- Z odkazu v neznámém mailu
-- [x] Z webu výrobce, z Store, nebo z balíčku školy
-- Z stránky s nápisem crack
-- Z přílohy faktura.pdf.exe
-> Výrobce, obchod a škola jsou zdroje. Crack a náhodný mail nejsou.
-
-## Co udělá smazání zástupce na ploše?
-- Odinstaluje program
-- [x] Smaže jen odkaz, program zůstane
-- Smaže Dokumenty
-- Vypne aktualizace
-> Zástupce je zkratka. Program se odebírá v Nainstalovaných aplikacích.
-
-## Proč se Windows Update nenechává měsíc stát?
-- Protože jinak nejde otevřít Word
-- [x] Záplaty zavírají známé díry
-- Protože se jinak smaže plocha
-- Aktualizace jen mění tapetu
-> Neaktualizovaný systém má chyby, které už jsou veřejně popsané.
-
-## Kdy odklepnete okno Řízení uživatelských účtů?
-- Vždycky, ať instalace pokračuje
-- [x] Jen když víte, jaký program a odkud ho spouštíte
-- Nikdy, ani u školního balíčku
-- Jen v Excelu
-> Okno říká, že program chce právo správce. Neznámý vydavatel se neodklepává.
diff --git a/content/programove-vybaveni/1-rocnik/05-ucty.en.md b/content/programove-vybaveni/1-rocnik/05-ucty.en.md
deleted file mode 100644
index b525df1..0000000
--- a/content/programove-vybaveni/1-rocnik/05-ucty.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: User accounts
-popis: A standard user, an administrator and a password that is not the account name.
-nazev_cviceni: Quiz: accounts
-nadpis_kvizu: Pick the account type and the password
----
diff --git a/content/programove-vybaveni/1-rocnik/05-ucty.md b/content/programove-vybaveni/1-rocnik/05-ucty.md
deleted file mode 100644
index fc41b33..0000000
--- a/content/programove-vybaveni/1-rocnik/05-ucty.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-1-ucty
-nazev: Uživatelské účty
-popis: Běžný uživatel, správce a heslo, které není jméno účtu.
-nazev_cviceni: Kvíz: účty
-nadpis_kvizu: Vyberte typ účtu a heslo
----
-
-# Výklad
-
-## Dva typy
-*Správce a uživatel*
-
-- Správce smí instalovat programy a měnit systém. Ve škole je to účet učitele nebo správce učebny.
-- Běžný uživatel smí pracovat se svými soubory. Systémové složky mu systém zamkne.
-- Denní práce nepatří na účet správce. Virus by dostal stejná práva jako vy.
-- Nový školní účet pro cvičení se jmenuje zaci a je běžný uživatel, ne správce.
-
-> Tip: Když systém požádá o heslo správce, běžný účet na to nestačí. To je záměr, ne porucha.
-
-## Heslo
-*Osm znaků*
-
-- Heslo má aspoň 8 znaků a není to jméno účtu, ani slovo heslo, ani 12345678.
-- Do příkazu se heslo nepíše. Příkaz net user zaci * /add se na heslo zeptá a na obrazovce ho neukáže.
-- Heslo na papírek na monitor nepatří. Patří do hlavy, nebo do školního sejfu hesel, když ho učitel určí.
-- Cizí heslo se nezkouší. Ani když je účet spolužáka odhlášený a počítač je společný.
-
-> Tip: Hvězdička v net user znamená dotaz na heslo. Heslo napsané za příkazem zůstane v historii příkazů.
-
-## Vlastní soubory
-*Profil*
-
-- Profil je složka C:\Users\zaci. Jsou v ní Dokumenty, Plocha a Stažené soubory.
-- Soubor uložený do Dokumentů tohoto účtu jiný běžný uživatel neotevře.
-- Společná složka C:\Users\Public je pro věci, které mají vidět všichni. Úkoly tam nedávejte.
-- Odhlášení účet zavře. Vypnutí počítače taky. Přepnutí uživatele nechá první účet otevřený v paměti.
-
-> Tip: Na sdíleném počítači se odhlašujte. Přepnutí uživatele nechá otevřené dokumenty v paměti.
-
-## Zadání: účet na papíře
-*Cvičení*
-
-- Napište, jak by se založil běžný účet zaci: příkaz net user zaci * /add spuštěný jako správce.
-- Napište heslo, které pravidlům nevyhovuje, a druhé, které má aspoň 8 znaků a není jméno účtu. Skutečné školní heslo nepište.
-- Účet nesmí být ve skupině Administrators. Patří mezi běžné Users.
-- Uložte ucet.txt do Skola\1-rocnik\system. Účet v systému zakládejte jen když to učitel výslovně řekne.
-
-> Tip: Kdo jsem, zjistí příkaz whoami. Když vypíše správce, na denní práci jste na špatném účtu.
-
-# Kvíz
-
-## Jaký má být cvičný účet zaci?
-- Správce, ať jde všechno
-- [x] Běžný uživatel
-- Účet bez hesla
-- Účet hosta sdílený celou třídou bez hesla
-> Denní práce běží jako běžný uživatel. Správce je na instalaci a správu, ne na Word.
-
-## Jak se heslo předá příkazu net user?
-- Napíše se za příkaz, ať je v sešitě vidět
-- [x] Hvězdičkou, příkaz se zeptá a heslo neukáže
-- Do názvu účtu
-- Do souboru na plochu
-> net user zaci * /add se zeptá. Heslo v příkazu zůstane v historii.
-
-## Proč se denně nepracuje jako správce?
-- Word jinak nejde otevřít
-- [x] Program, který spustíte, dostane právo měnit systém
-- Správce nemá Dokumenty
-- Excel jinak nepočítá
-> Škodlivý program dědí vaše práva. U správce smí instalovat a měnit systém.
-
-## Kam patří osobní úkol?
-- Do C:\Users\Public
-- [x] Do Dokumentů vlastního účtu
-- Do složky Windows
-- Na účet spolužáka
-> Public vidí každý účet na počítači. Úkol patří do vlastního profilu.
diff --git a/content/programove-vybaveni/1-rocnik/06-spravce-uloh.en.md b/content/programove-vybaveni/1-rocnik/06-spravce-uloh.en.md
deleted file mode 100644
index b9e7a54..0000000
--- a/content/programove-vybaveni/1-rocnik/06-spravce-uloh.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Task Manager
-popis: A process, memory and startup programs. The system is not ended.
-nazev_cviceni: Quiz: Task Manager
-nadpis_kvizu: Pick what you may end
----
diff --git a/content/programove-vybaveni/1-rocnik/06-spravce-uloh.md b/content/programove-vybaveni/1-rocnik/06-spravce-uloh.md
deleted file mode 100644
index 45405b1..0000000
--- a/content/programove-vybaveni/1-rocnik/06-spravce-uloh.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-1-spravce-uloh
-nazev: Správce úloh
-popis: Proces, paměť a programy po spuštění. Systém se neukončuje.
-nazev_cviceni: Kvíz: Správce úloh
-nadpis_kvizu: Vyberte, co smíte ukončit
----
-
-# Výklad
-
-## Okno
-*Ctrl+Shift+Esc*
-
-- Správce úloh se otevře Ctrl+Shift+Esc. Nehledá se přes náhodné ikony na ploše.
-- Karta Procesy ukáže aplikace a to, co běží na pozadí.
-- Sloupec Procesor je v procentech. Sloupec Paměť je v megabajtech.
-- Vysoká paměť u prohlížeče s dvaceti kartami je běžná. Vysoká paměť neznámého procesu se hlásí.
-
-> Tip: Když plocha nereaguje, Ctrl+Shift+Esc často projde i tehdy, když nabídka Start ne.
-
-## Ukončit se smí aplikace
-*Ne systém*
-
-- Ukončit úlohu smíte u svého Wordu, který nereaguje. Rozpracovaný dokument si předtím uložte, jinak o něj přijdete.
-- Proces Systém, Registr a aplikace systému Windows se neukončují. Počítač pak nemusí doběhnout do restartu.
-- Neznámý proces nejdřív vyhledejte podle přesného názvu. Ukončení není první krok.
-- Restart počítače je čistší než ukončit deset systémových procesů za sebou.
-
-> Tip: Word, který nereaguje, ukončete a otevřete znovu. Autorecover často dokument vrátí, ale nespoléhejte na něj.
-
-## Po spuštění
-*Co startuje samo*
-
-- Karta Aplikace po spuštění ukáže programy, které naběhnou s Windows.
-- Každý takový program bere čas startu a paměť. Ve škole jich má být málo.
-- Vypnutí Teams nebo prohlížeče po spuštění, když ho učitel nechce, je v pořádku. Vypnutí zabezpečení Windows není.
-- Změna se projeví po příštím přihlášení, ne vždy hned.
-
-> Tip: Povolené a zakázané je v posledním sloupci. Zakázaný program v počítači zůstane, jen se sám nespustí.
-
-## Zadání: jeden pohled
-*Cvičení*
-
-- Otevřete Správce úloh a zapište tři aplikace, které berou nejvíc paměti. Čísla opište.
-- Najděte, jestli s Windows startuje něco, co nepoznáváte. Neznámé nevypínejte, jen ho zapište.
-- Systémové procesy neukončujte. Když Word nereaguje, ukončete jen Word.
-- Uložte procesy.txt do Skola\1-rocnik\system.
-
-> Tip: Řazení podle paměti je kliknutí na záhlaví sloupce Paměť. První řádek je pak ten nejtěžší.
-
-# Kvíz
-
-## Jakou zkratkou se otevře Správce úloh?
-- [x] Ctrl+Shift+Esc
-- Ctrl+S
-- Win+E je Správce úloh
-- Alt+F4 na ploše
-> Ctrl+Shift+Esc otevře Správce přímo. Win+E je Průzkumník.
-
-## Co smíte ukončit, když nereaguje vaše práce?
-- Proces Systém
-- [x] Svůj Word, který nereaguje
-- Registr
-- Zabezpečení Windows
-> Ukončuje se zaseknutá aplikace. Systémové procesy drží počítač.
-
-## Co dělá vypnutí programu na kartě Po spuštění?
-- Odinstaluje ho
-- [x] Příště se sám nespustí s Windows
-- Smaže Dokumenty
-- Zakáže aktualizace
-> Program v počítači zůstane. Nenaběhne jen sám při přihlášení.
-
-## Vysoká paměť u prohlížeče s mnoha kartami znamená co?
-- Že se má hned ukončit Systém
-- [x] Že prohlížeč prostě drží otevřené karty
-- Že je disk NTFS rozbitý
-- Že vypršela licence
-> Každá karta bere paměť. Neznámý proces je jiný případ než prohlížeč, který jste sami otevřeli.
diff --git a/content/programove-vybaveni/1-rocnik/07-zabezpeceni.en.md b/content/programove-vybaveni/1-rocnik/07-zabezpeceni.en.md
deleted file mode 100644
index aaec038..0000000
--- a/content/programove-vybaveni/1-rocnik/07-zabezpeceni.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Securing the computer
-popis: Antivirus, updates and a file that pretends to be a document.
-nazev_cviceni: Quiz: security
-nadpis_kvizu: Pick what stays on and what stays closed
----
diff --git a/content/programove-vybaveni/1-rocnik/07-zabezpeceni.md b/content/programove-vybaveni/1-rocnik/07-zabezpeceni.md
deleted file mode 100644
index 803fd77..0000000
--- a/content/programove-vybaveni/1-rocnik/07-zabezpeceni.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-1-zabezpeceni
-nazev: Zabezpečení počítače
-popis: Antivir, aktualizace a soubor, který se tváří jako dokument.
-nazev_cviceni: Kvíz: zabezpečení
-nadpis_kvizu: Vyberte, co se nezapíná a co se neotvírá
----
-
-# Výklad
-
-## Zabezpečení Windows
-*Defender*
-
-- Školní Windows mají Zabezpečení Windows. Antivir se nevypíná kvůli instalaci hry.
-- Ochrana v reálném čase má být zapnutá. Vypnutá ochrana je nález, ne úleva.
-- Brána firewall má být zapnutá na školní síti. Vypnutí kvůli jedné hře se nedělá.
-- Druhý antivir vedle Defenderu se bez učitele neinstaluje. Dva se perou a oba pak hlídají hůř.
-
-> Tip: Zabezpečení Windows otevřete z nabídky Start. Zelené fajfky u položek jsou stav, který opisujete do úkolu.
-
-## Soubory a maily
-*Co neotvírat*
-
-- Příloha faktura.pdf.exe se neotevírá, ani když text mailu zní věcně.
-- Odkaz na přihlášení, který nečekáte, se neotvírá. Škola po hesle v mailu nepátrá.
-- Flash disk neznámého původu se do školního počítače nestrká. Nejdřív učitel.
-- Makro ve Wordu se povoluje, jen když dokument přišel ze školy a učitel řekl proč.
-
-> Tip: Heslo se neposílá mailem ani do chatu. Kdo ho po vás chce zprávou, nemá ho dostat.
-
-## Oprávnění k souboru
-*NTFS*
-
-- Pravý klik na složku, Vlastnosti, Zabezpečení ukáže, kdo smí číst a kdo zapisovat.
-- Běžný uživatel zapíše do svých Dokumentů. Do C:\Windows nezapíše.
-- Odebrat si vlastní právo číst vlastní Dokumenty je chyba. Složka pak nejde otevřít.
-- Společná složka Public nemá být místo, kam dáváte test s jménem.
-
-> Tip: Když soubor nejde uložit, často nemáte právo zápisu. Uložte ho do Dokumentů, ne do Program Files.
-
-## Zadání: pět kontrol
-*Cvičení*
-
-- Opište, jestli je ochrana v reálném čase zapnutá a jestli firewall hlásí zapnuto.
-- Z Windows Update opište, jestli něco čeká na restart.
-- Najděte ve stažených souborech, jestli tam není soubor s dvojitou příponou. Neotvírejte ho, zapište název.
-- Uložte bezpecnost.txt do Skola\1-rocnik\system. Ochranu nevypínejte.
-
-> Tip: Když je ochrana vypnutá, nezapínejte ji naslepo třemi kliky v neznámém okně. Řekněte to učiteli, může ji vypínat školní správa.
-
-# Kvíz
-
-## Co má být zapnuté v Zabezpečení Windows?
-- Nic, antivir zpomaluje Word
-- [x] Ochrana v reálném čase a firewall
-- Jen spořič obrazovky
-- Jen OneDrive
-> Antivir i firewall jsou součást školního počítače. Nevypínají se kvůli pohodlí.
-
-## Který soubor neotevřete?
-- poznamka.txt z vlastní složky
-- [x] faktura.pdf.exe z mailu
-- zadost.docx, kterou jste sami uložili
-- znamky.xlsx z hodiny
-> Dvojitá přípona exe je program. Mail s fakturou ho nesmí spustit.
-
-## Kam běžný uživatel smí uložit úkol?
-- Do C:\Windows
-- [x] Do svých Dokumentů
-- Do složky ovladačů
-- Přes cizí účet
-> Dokumenty vlastního účtu jsou na zápis. Systémové složky ne.
-
-## Proč se vedle Defenderu bez učitele nedává druhý antivir?
-- Windows umí jen jeden a druhý se vždy sám smaže
-- [x] Dva antiviry se navzájem ruší
-- Defender je freeware bez licence
-- Excel pak neumí SUMA
-> Dva programy, které hlídají stejné soubory, se blokují. Ve škole stačí Zabezpečení Windows.
diff --git a/content/programove-vybaveni/1-rocnik/08-zadani-windows.en.md b/content/programove-vybaveni/1-rocnik/08-zadani-windows.en.md
deleted file mode 100644
index 847262b..0000000
--- a/content/programove-vybaveni/1-rocnik/08-zadani-windows.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Assignment: a school account and a tidy disk
-popis: Folders, a standard account, updates and protection in one note.
-nazev_cviceni: Quiz: the school account
-nadpis_kvizu: Check the whole assignment
----
diff --git a/content/programove-vybaveni/1-rocnik/08-zadani-windows.md b/content/programove-vybaveni/1-rocnik/08-zadani-windows.md
deleted file mode 100644
index 862c127..0000000
--- a/content/programove-vybaveni/1-rocnik/08-zadani-windows.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-1-zadani-windows
-nazev: Zadání: školní účet a pořádek
-popis: Složky, běžný účet, aktualizace a ochrana v jednom zápisu.
-nazev_cviceni: Kvíz: školní účet
-nadpis_kvizu: Zkontrolujte celé zadání
----
-
-# Výklad
-
-## Složky
-*Strom*
-
-- V Dokumentech je Skola\1-rocnik a v ní word, excel a system.
-- Na ploše nezůstane pracovní soubor. Plocha není archiv.
-- Přípony názvů jsou vidět. poznámka se nejmenuje poznámka.txt.txt.
-- Názvy jsou bez znaků \ / : * ? a datum v názvu je rok-měsíc-den.
-
-> Tip: Cestu opište z řádku adresy. Od oka se lomítka pletou.
-
-## Účet
-*zaci*
-
-- Cvičný účet zaci je běžný uživatel. Není ve skupině Administrators.
-- Heslo má aspoň 8 znaků a nezadává se do příkazu, jen přes hvězdičku v net user zaci * /add.
-- Do zápisu nepatří skutečné heslo. Patří tam pravidlo a příkaz bez hesla.
-- whoami na denní práci nemá ukázat účet správce.
-
-> Tip: Účet zakládejte jen pod dohledem. V zápisu stačí postup, když učitel účet na počítači nechce.
-
-## Stav systému
-*Tři řádky*
-
-- winver: edice a verze.
-- Windows Update: aktuální, nebo čeká restart. Datum stačí.
-- Zabezpečení Windows: ochrana v reálném čase zapnutá, firewall zapnutý.
-- Ze Správce úloh tři nejtěžší aplikace podle paměti. Systémové procesy neukončené.
-
-> Tip: Když je něco vypnuté, zapište to jako nález. Neopravujte firewall naslepo.
-
-## Odevzdání
-*Jeden soubor*
-
-- Všechno je v ucto-windows.txt ve složce Skola\1-rocnik\system.
-- Jsou tam cesta ke složce word, popis účtu, verze Windows a stav ochrany.
-- Není tam heslo, není tam cizí soubor a není tam vypnutý Defender.
-- Soubor otevřete znovu a přečtěte ho. Cesta v textu musí po vložení do adresy opravdu vyjít.
-
-> Tip: Když cesta z textu skončí chybou, složka neexistuje, nebo je v cestě obyčejné lomítko.
-
-# Kvíz
-
-## Jaký účet je zaci?
-- Správce
-- [x] Běžný uživatel mimo skupinu Administrators
-- Účet bez hesla v Public
-- Sdílený účet celé třídy
-> Denní účet není správce. Heslo má a do skupiny Administrators nepatří.
-
-## Kam se ukládá pracovní soubor?
-- Na plochu
-- [x] Do Skola\1-rocnik a příslušné složky
-- Do C:\Windows
-- Do stažených souborů bez názvu
-> Plocha se nearchivuje. Strom Skola drží word, excel a system odděleně.
-
-## Co má být zapnuté?
-- Jen spořič
-- [x] Ochrana v reálném čase a firewall
-- Dva antiviry
-- Sdílení celé složky Users
-> Defender a firewall zůstávají zapnuté. Do úkolu se opisuje jejich stav.
-
-## Co v souboru ucto-windows.txt být nesmí?
-- Verze z winver
-- [x] Skutečné heslo
-- Cesta ke složce
-- Stav Windows Update
-> Heslo se do textového úkolu nepíše. Příkaz ho bere přes hvězdičku, ne do sešitu.
diff --git a/content/programove-vybaveni/2-rocnik/01-word-styly.en.md b/content/programove-vybaveni/2-rocnik/01-word-styly.en.md
deleted file mode 100644
index 92c3344..0000000
--- a/content/programove-vybaveni/2-rocnik/01-word-styly.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Word: styles
-popis: Heading 1, body text and spacing that is not empty lines.
-nazev_cviceni: Quiz: styles
-nadpis_kvizu: Pick the style and the shortcut
----
diff --git a/content/programove-vybaveni/2-rocnik/01-word-styly.md b/content/programove-vybaveni/2-rocnik/01-word-styly.md
deleted file mode 100644
index b0a2f8d..0000000
--- a/content/programove-vybaveni/2-rocnik/01-word-styly.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-2-word-styly
-nazev: Word: styly
-popis: Nadpis 1, normální text a mezery, které nejsou prázdné řádky.
-nazev_cviceni: Kvíz: styly
-nadpis_kvizu: Vyberte styl a zkratku
----
-
-# Výklad
-
-## Styl, ne ruční tučné
-*Nadpis*
-
-- Nadpis 1 je styl. Ctrl+Alt+1 ho zapne. Nadpis 2 je Ctrl+Alt+2.
-- Tučné a větší písmo místo stylu vypadá jako nadpis, ale obsah z něj Word nesestaví.
-- Normální text je styl Normální. Ctrl+Shift+N ho vrátí, když jste omylem v nadpisu.
-- V dokumentu jsou nejvýš tři úrovně nadpisů. Čtvrtá je už nepřehledná.
-
-> Tip: Podokno Styly otevřete z karty Domů. U odstavce je vidět, jaký styl opravdu má.
-
-## Odstavec
-*Mezery ze stylu*
-
-- Prázdný Enter mezi odstavci se nedělá. Mezera se nastaví ve stylu, třeba 8 bodů za odstavcem.
-- Řádkování školního textu je 1,15 nebo 1,5. Ne dvojité přes celou stránku bez důvodu.
-- Zarovnání do bloku u delšího textu, vlevo u nadpisu a u krátkého dopisu.
-- Stránka je A4. Okraje 2,5 cm ze všech stran, to je předvolba Normální.
-
-> Tip: Konec stránky je Ctrl+Enter. Řada Enterů na další stránku se při úpravě rozjede.
-
-## Soubor
-*docx a pdf*
-
-- Pracovní soubor je .docx. Jdou v něm styly i další úpravy.
-- Na odevzdání, které se už nemá měnit, je navíc PDF.
-- Jednou vyexportované PDF neupravujte dál jako jediný zdroj. Opravte docx a PDF udělejte znovu.
-- Název je věcný: 2026-06-12-protokol.docx. Dokument1.docx se neodevzdává.
-
-> Tip: Soubor, Export, Vytvořit soubor PDF. Před exportem se podívejte na náhled tisku, jestli nadpis nesedí sám na konci stránky.
-
-## Zadání: dvoustránkový text
-*Cvičení*
-
-- Dokument A4, okraje 2,5 cm, aspoň tři nadpisy stylem Nadpis 1.
-- Pod každým nadpisem aspoň jeden odstavec stylem Normální. Žádné prázdné Entery mezi nimi.
-- Konec první stránky je Ctrl+Enter, ne deset prázdných řádků.
-- Uložte protokol.docx do Skola\1-rocnik\word a k němu protokol.pdf.
-
-> Tip: Když v navigačním podokně nadpisy nejsou, jsou to jen tučná písma. Označte je a dejte Ctrl+Alt+1.
-
-# Kvíz
-
-## Jak se má udělat nadpis, aby z něj šel obsah?
-- Tučně a 16 bodů
-- [x] Stylem Nadpis 1, zkratka Ctrl+Alt+1
-- Pěti prázdnými Entery nad ním
-- Obrázkem textu
-> Obsah čte styly. Ruční tučné písmo v obsahu není.
-
-## Čím se přejde na další stránku?
-- Deseti Entery
-- [x] Ctrl+Enter
-- Mezerou za odstavcem 80 bodů
-- Smazáním okrajů
-> Ctrl+Enter je konec stránky. Prázdné řádky se při doplnění textu rozjedou.
-
-## Jaké jsou školní okraje na A4?
-- 0 cm, ať se vejde víc
-- [x] 2,5 cm, předvolba Normální
-- 5 cm vpravo a 0 vlevo
-- Okraje se ve Wordu nenastavují
-> Normální okraj je 2,5 cm. Text až do kraje papíru tiskárna často ořízne.
-
-## Který soubor se dál upravuje?
-- PDF
-- [x] docx
-- Jen vytištěná stránka
-- Příloha exe
-> docx drží styly. PDF je kopie na odevzdání.
diff --git a/content/programove-vybaveni/2-rocnik/02-word-tabulka-a-obsah.en.md b/content/programove-vybaveni/2-rocnik/02-word-tabulka-a-obsah.en.md
deleted file mode 100644
index 9a94510..0000000
--- a/content/programove-vybaveni/2-rocnik/02-word-tabulka-a-obsah.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Word: a table, a header and a table of contents
-popis: A 4 by 3 table, a page number and a contents list from the headings.
-nazev_cviceni: Quiz: contents
-nadpis_kvizu: Pick where the contents come from
----
diff --git a/content/programove-vybaveni/2-rocnik/02-word-tabulka-a-obsah.md b/content/programove-vybaveni/2-rocnik/02-word-tabulka-a-obsah.md
deleted file mode 100644
index ce559e7..0000000
--- a/content/programove-vybaveni/2-rocnik/02-word-tabulka-a-obsah.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-2-word-obsah
-nazev: Word: tabulka, záhlaví a obsah
-popis: Tabulka 4×3, číslo stránky a obsah z nadpisů.
-nazev_cviceni: Kvíz: obsah
-nadpis_kvizu: Vyberte, odkud se obsah vezme
----
-
-# Výklad
-
-## Tabulka
-*4 řádky a 3 sloupce*
-
-- Tabulka se vkládá z karty Vložení. Kreslení tužkou od ruky se ve školním protokolu nedělá.
-- Školní tabulka má 4 řádky a 3 sloupce: záhlaví a tři záznamy.
-- Záhlaví tabulky je tučné a řádek se neopakuje jako obyčejný text pod tabulkou.
-- Text v buňce se nezalamuje deseti mezerami. Šířka sloupce se táhne myší.
-
-> Tip: Když potřebujete řádek navíc, klepněte vpravo od řádku a dejte vložit. Enter v buňce jen zalomí text.
-
-## Záhlaví a zápatí
-*Číslo stránky*
-
-- Záhlaví je nahoře, zápatí dole. Dvojklik v okraji je otevře.
-- V záhlaví je název školy a název dokumentu. V zápatí je číslo stránky.
-- Číslo stránky se vloží jako pole. Ručně napsaná jednička se na druhé stránce neposune.
-- Záhlaví není nadpis dokumentu. Nadpis zůstává v textu jako Nadpis 1.
-
-> Tip: Pole čísla stránky má po označení šedé pozadí. Když je to obyčejná číslice, na další stránce se nezmění.
-
-## Obsah
-*Z nadpisů*
-
-- Obsah je na kartě Reference, Obsah. Word ho složí z Nadpis 1 a Nadpis 2.
-- Ručně psaný seznam stránek obsah není. Po vložení odstavce se rozejde.
-- Po změně textu se obsah aktualizuje pravým klikem, Aktualizovat pole, a volbou celá tabulka.
-- Obsah stojí na začátku, za titulní řádek, na vlastní stránce nebo hned pod názvem. Ne na konci.
-
-> Tip: Když je obsah prázdný, nadpisy nejsou styly. Ctrl+Alt+1 a obsah aktualizujte znovu.
-
-## Zadání: protokol s tabulkou
-*Cvičení*
-
-- Vezměte protokol.docx. Přidejte tabulku 4×3: sloupec Jméno, Třída, Stav.
-- Tři řádky: Anna Nováková, 1.A, odevzdáno. Druhý a třetí řádek doplňte vlastními jmény.
-- Záhlaví s názvem školy, zápatí s číslem stránky, obsah z nadpisů na začátku.
-- Uložte znovu docx a nové PDF. Staré PDF přepište, ať neexistují dvě verze.
-
-> Tip: Aktualizace obsahu se ptá, jestli jen čísla, nebo celá tabulka. Po nových nadpisech berte celou tabulku.
-
-# Kvíz
-
-## Jak velká je školní tabulka v tomto úkolu?
-- [x] 4 řádky a 3 sloupce
-- Jedna buňka
-- 10 sloupců bez záhlaví
-- Tabulka se ve Wordu nedělá
-> Záhlaví plus tři záznamy jsou čtyři řádky. Sloupce jsou Jméno, Třída a Stav.
-
-## Odkud Word vezme obsah?
-- Z tučných vět
-- [x] Ze stylů Nadpis 1 a Nadpis 2
-- Z zápatí
-- Z Excelu automaticky vždy
-> Bez stylů nadpisů je obsah prázdný. Tučné písmo nestačí.
-
-## Jak se má dostat na stránku číslo?
-- Napsat 1 do zápatí ručně
-- [x] Vložit pole čísla stránky
-- Dát ho do názvu souboru
-- Nepsat ho, Word ho do PDF doplní sám i bez pole
-> Pole se na každé stránce samo zvýší. Ruční jednička zůstane jednička.
-
-## Kdy se obsah aktualizuje celý, ne jen čísla stránek?
-- [x] Když přibyl nadpis
-- Když se jen změnila šířka okraje o milimetr a nadpisy zůstaly
-- Po každém písmenu
-- Obsah se neaktualizuje nikdy
-> Nový nadpis do obsahu přibude jen při aktualizaci celé tabulky.
diff --git a/content/programove-vybaveni/2-rocnik/03-word-dopis.en.md b/content/programove-vybaveni/2-rocnik/03-word-dopis.en.md
deleted file mode 100644
index 6a953ff..0000000
--- a/content/programove-vybaveni/2-rocnik/03-word-dopis.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Word: a formal letter
-popis: Sender, date, subject, greeting, body and signature on one page.
-nazev_cviceni: Quiz: the letter
-nadpis_kvizu: Put the parts in order
----
diff --git a/content/programove-vybaveni/2-rocnik/03-word-dopis.md b/content/programove-vybaveni/2-rocnik/03-word-dopis.md
deleted file mode 100644
index 288e40f..0000000
--- a/content/programove-vybaveni/2-rocnik/03-word-dopis.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-2-word-dopis
-nazev: Word: úřední dopis
-popis: Odesílatel, datum, věc, oslovení, text a podpis na jedné straně.
-nazev_cviceni: Kvíz: dopis
-nadpis_kvizu: Seřaďte náležitosti
----
-
-# Výklad
-
-## Náležitosti
-*Shora dolů*
-
-- Vlevo nahoře odesílatel: škola, jméno, adresa. Vpravo datum 12. 6. 2026.
-- Pod tím adresát. Pak řádek Věc: žádost o uvolnění z výuky.
-- Oslovení, text, rozloučení a jméno s podpisem. Podpis je pod jménem, ne místo něj.
-- Celý dopis je jedna strana A4. Druhá stránka s jedním řádkem je chyba.
-
-> Tip: Datum pište 12. 6. 2026, ne 12/6/26. Český úřední dopis má den, měsíc a rok.
-
-## Vzhled
-*Klidný dopis*
-
-- Písmo jedno, třeba Calibri 11 nebo 12. V dopise není Nadpis 1 přes celou šířku.
-- Řádek Věc je tučný nebo jako krátký nadpis, zbytek je Normální.
-- Odstavce mají mezeru za sebou. Mezi oslovením a textem není deset Enterů.
-- Okraje 2,5 cm. Text do bloku. Bez obrázku, bez tabulky, bez obsahu.
-
-> Tip: Když se poslední řádek podpisu sleze na druhou stránku, zkraťte mezeru za odstavcem, nepřidávejte menší okraje než 2 cm.
-
-## Hromadná pošta
-*Až když je řádků víc*
-
-- Jeden dopis se píše přímo. Hromadná korespondence se vyplatí, když je adresátů aspoň deset.
-- Zdroj dat je Excel s řádkem záhlaví: Jmeno, Prijmeni, Trida.
-- Word na kartě Korespondence vezme tento sešit a pole vloží do oslovení.
-- Sloučený výsledek se ukládá jako nový docx. Zdrojový dopis s poli si nechte zvlášť.
-
-> Tip: Když Excel nemá záhlaví, první žák se stane názvem sloupce a z hromadné pošty vypadne.
-
-## Zadání: žádost
-*Cvičení*
-
-- Napište žádost o uvolnění z výuky na 12. 6. 2026. Adresát je ředitel školy, odesílatel jste vy.
-- V dokumentu je Věc, oslovení, aspoň dvě věty důvodu a podpis.
-- Jedna strana, A4, okraje 2,5 cm. Uložte zadost.docx a zadost.pdf do složky word.
-- Hromadnou poštu v tomto úkolu nedělejte. Stačí jeden dopis.
-
-> Tip: Přečtěte dopis nahlas. Když v něm chybí, o co žádáte a který den, věta důvodu nestačí.
-
-# Kvíz
-
-## Kam patří datum v tomto dopisu?
-- Dospodu místo podpisu
-- [x] Dopravna, 12. 6. 2026
-- Do názvu souboru jako jediné místo
-- Do zápatí místo čísla, datum v textu být nesmí
-> Datum je vpravo nahoře. Podpis dole je jméno, ne datum.
-
-## Jak se označí, o co v dopise jde?
-- [x] Řádkem Věc
-- Stylem Nadpis 3 uprostřed stránky
-- Obrázkem razítka
-- Obsahem na druhé straně
-> Věc je krátký řádek nad oslovením, třeba žádost o uvolnění z výuky.
-
-## Kdy se použije hromadná korespondence?
-- Na jeden dopis řediteli
-- [x] Když je stejný dopis pro deset a víc adresátů a data jsou v Excelu
-- Na každou tabulku
-- Místo PDF
-> Jeden dopis se píše ručně. Hromadná pošta bere řádky z Excelu se záhlavím.
-
-## Jak dlouhý je školní dopis z tohoto zadání?
-- [x] Jedna strana A4
-- Přesně tři strany
-- Půl stránky a zbytek prázdných Enterů
-- Deset stran se styly Nadpis 1
-> Žádost se vejde na jednu stranu. Druhá stránka jen s podpisem je špatně zalomená.
diff --git a/content/programove-vybaveni/2-rocnik/04-excel-bunky.en.md b/content/programove-vybaveni/2-rocnik/04-excel-bunky.en.md
deleted file mode 100644
index 17e029c..0000000
--- a/content/programove-vybaveni/2-rocnik/04-excel-bunky.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Excel: cells and formulas
-popis: The address A1, a formula with an equals sign and a dollar lock.
-nazev_cviceni: Quiz: cells
-nadpis_kvizu: Complete the formula
----
diff --git a/content/programove-vybaveni/2-rocnik/04-excel-bunky.md b/content/programove-vybaveni/2-rocnik/04-excel-bunky.md
deleted file mode 100644
index 9ac18a5..0000000
--- a/content/programove-vybaveni/2-rocnik/04-excel-bunky.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-2-excel-bunky
-nazev: Excel: buňky a vzorce
-popis: Adresa A1, vzorec se rovnítkem a ukotvení dolarama.
-nazev_cviceni: Kvíz: buňky
-nadpis_kvizu: Doplňte vzorec
----
-
-# Výklad
-
-## Buňka
-*Adresa*
-
-- Sloupce jsou písmena, řádky čísla. B2 je druhý sloupec a druhý řádek.
-- Do buňky patří jedna hodnota. Jméno a třída do jedné buňky nepatří.
-- Číslo je číslo, text je text. Jeden apostróf před číslem z něj udělá text a přestane se sčítat.
-- Sešit je soubor xlsx. List je záložka dole. Školní sešit má listy Ceník a Součet.
-
-> Tip: Klik na záhlaví sloupce označí celý sloupec. Na číslo řádku celý řádek.
-
-## Vzorec
-*Rovnítko*
-
-- Vzorec začíná =. Bez rovnítka je B2*C2 jen text a nic nespočítá.
-- V ceníku je v D2 vzorec =B2*C2. B je počet, C je cena, D je částka.
-- Český Excel odděluje argumenty funkcí středníkem. Krát je hvězdička, děleno lomítko.
-- Vzorec se kopíruje za úchyt v pravém dolním rohu buňky. Neopisuje se do pěti buněk ručně.
-
-> Tip: Když buňka ukáže vzorec místo výsledku, je sloupec úzký, nebo je buňka text. Zkontrolujte rovnítko.
-
-## Posun a dolar
-*A1 a $A$1*
-
-- Zkopírování =B2*C2 o řádek níž udělá =B3*C3. Adresy se posunou.
-- =$B$2 se neposune ani ve sloupci, ani v řádku. Dolar kotví.
-- Sazba DPH v buňce F1 se ve vzorcích bere jako $F$1, ať kopírování neodjede na F2.
-- Smazání hodnoty, na kterou vzorec ukazuje, udělá chybu. Vzorec na prázdnou buňku není nula sám od sebe, když čekal číslo a dostal text.
-
-> Tip: Dolar jen u písmene, $B2, kotví sloupec. Dolar jen u čísla, B$2, kotví řádek.
-
-## Zadání: ceník
-*Cvičení*
-
-- List Ceník: sloupce Polozka, Pocet, Cena, Castka. Záhlaví v řádku 1, pět položek v řádcích 2 až 6.
-- Castka v D2 je =B2*C2 a dolů se táhne. Žádná částka není napsaná ručně.
-- Sazba v F1 je 0,21 a v E2 je =D2*$F$1. Po zkopírování dolů pořád ukazuje na F1.
-- Uložte cenik.xlsx do Skola\1-rocnik\excel.
-
-> Tip: Klik na D2 ukáže vzorec v řádku vzorců. Když tam je jen číslo, částka je opsaná a při změně počtu se nehne.
-
-# Kvíz
-
-## Čím začíná vzorec?
-- Závorkou
-- [x] Rovnítkem
-- Středníkem
-- Dolarama bez rovnítka
-> Bez = Excel nic nepočítá. Text B2*C2 zůstane text.
-
-## Co je v D2, když počet je v B2 a cena v C2?
-- [x] =B2*C2
-- B2*C2 bez rovnítka
-- =B2+C2
-- Ručně spočítané číslo
-> Částka je součin. Sčítání by sečetlo počet s cenou, což není částka.
-
-## Proč je sazba ve vzorci $F$1?
-- [x] Aby se při kopírování dolů adresa neposunula
-- Protože dolar je měna tabulky
-- Aby vzorec přestal počítat
-- $ se v Excelu nesmí
-> F1 je jedna buňka pro všechny řádky. Bez dolarů by druhý řádek četl F2.
-
-## Co udělá zkopírování =B2*C2 o řádek níž?
-- Nechá B2 a C2
-- [x] Vznikne =B3*C3
-- Smaže list
-- Změní se na text
-> Relativní adresa se posune o stejný počet řádků jako kopírování.
diff --git a/content/programove-vybaveni/2-rocnik/05-excel-funkce.en.md b/content/programove-vybaveni/2-rocnik/05-excel-funkce.en.md
deleted file mode 100644
index 06d600d..0000000
--- a/content/programove-vybaveni/2-rocnik/05-excel-funkce.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Excel: functions
-popis: SUM, IF and VLOOKUP. The Czech edition separates arguments with a semicolon.
-nazev_cviceni: Quiz: functions
-nadpis_kvizu: Pick the right formula
----
diff --git a/content/programove-vybaveni/2-rocnik/05-excel-funkce.md b/content/programove-vybaveni/2-rocnik/05-excel-funkce.md
deleted file mode 100644
index 67c6907..0000000
--- a/content/programove-vybaveni/2-rocnik/05-excel-funkce.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-2-excel-funkce
-nazev: Excel: funkce
-popis: SUMA, KDYŽ a SVYHLEDAT. V české verzi se argumenty oddělují středníkem.
-nazev_cviceni: Kvíz: funkce
-nadpis_kvizu: Vyberte správný zápis
----
-
-# Výklad
-
-## Souhrny
-*SUMA a spol.*
-
-- SUMA(D2:D6) sečte částky. Průměr je PRŮMĚR, nejmenší MIN, největší MAX.
-- POČET spočítá čísla. Prázdnou buňku nepočítá. Text nepočítá.
-- Rozsah D2:D6 zahrnuje oba konce. D2:D6 jsou řádky 2 až 6, ne jen 2 a 6.
-- Souhrn patří pod tabulku, nebo na list Součet. Nepočítá se na papír a neopisuje se.
-
-> Tip: Anglický Excel má SUM a čárku. Český má SUMA a středník. Ve škole pište česky, jak to má školní Office.
-
-## KDYŽ
-*Známky*
-
-- České známky: 1 je nejlepší, 5 je neprospěl. Prospěch je známka 1 až 4.
-- =KDYŽ(D2<=4;"prospěl";"neprospěl") vrací text podle známky v D2.
-- Středníky oddělují podmínku, větev ano a větev ne. Čárka v české verzi vzorec rozbije.
-- Podmínka, která čeká známku aspoň 4 jako úspěch, je obráceně. Čtyřka ještě prochází, pětka ne.
-
-> Tip: Uvozovky kolem textu ve funkci jsou povinné. Bez nich Excel hledá buňku nebo název.
-
-## SVYHLEDAT
-*Ceník*
-
-- SVYHLEDAT najde hodnotu v levém sloupci oblasti a vrátí jiný sloupec stejného řádku.
-- =SVYHLEDAT(A2;Ceník!$A$2:$C$20;3;NEPRAVDA) hledá A2, oblast je ukotvená, vrátí 3. sloupec, shoda je přesná.
-- NEPRAVDA znamená přesnou shodu. PRAVDA by směla vzít nejbližší nižší a u textu plete.
-- Hledaná hodnota musí být v prvním sloupci oblasti. Jinak funkce nehledá uprostřed tabulky.
-
-> Tip: Když funkce vrátí #N/A, hodnota v prvním sloupci není. Zkontrolujte mezeru za jménem a stejný typ, text proti číslu.
-
-## Zadání: známky a ceník
-*Cvičení*
-
-- Na list Znamky dejte pět žáků, sloupec Znamka a sloupec Stav se vzorcem KDYŽ, prospěl při známce 1 až 4.
-- Pod známky SUMA nedávejte, součet známek nic neříká. Dejte PRŮMĚR.
-- Na listu Ceník doplňte SVYHLEDAT, který podle kódu položky vrátí cenu. Oblast ukotvěte a dejte NEPRAVDA.
-- Uložte znamky.xlsx do složky excel. Oba listy ať jsou v jednom sešitu.
-
-> Tip: Průměr známek 1,0 až 5,0 vychází jako číslo. Když vyjde 0, počítáte prázdné buňky, nebo máte text místo čísel.
-
-# Kvíz
-
-## Jak se v českém Excelu zapíše součet D2 až D6?
-- =SUM(D2:D6)
-- [x] =SUMA(D2:D6)
-- =SUMA(D2,D6)
-- SUMA(D2:D6) bez rovnítka
-> Česká funkce je SUMA a rozsah je D2:D6. SUM je anglická verze.
-
-## Který vzorec označí známku 5 jako neprospěl?
-- [x] =KDYŽ(D2<=4;"prospěl";"neprospěl")
-- =KDYŽ(D2>=4;"prospěl";"neprospěl")
-- =KDYŽ(D2<=4,"prospěl","neprospěl")
-- =D2<=4
-> Prochází 1 až 4, tedy menší nebo rovno 4. V češtině jsou středníky, ne čárky.
-
-## Co znamená NEPRAVDA na konci SVYHLEDAT?
-- Vypne vzorec
-- [x] Chce přesnou shodu
-- Sečte sloupec
-- Vrátí vždy první řádek
-> NEPRAVDA je přesně ta hodnota. PRAVDA bere přibližnou shodu a u kódů položek lže.
-
-## Proč je oblast Ceník!$A$2:$C$20 ukotvená?
-- [x] Aby se při kopírování vzorce dolů neposunula
-- Protože jinak nejde uložit xlsx
-- Dolar sčítá ceny
-- SVYHLEDAT ukotvení zakazuje
-> Každý řádek hledá v téže tabulce. Bez dolarů by se oblast sunula dolů s vzorcem.
diff --git a/content/programove-vybaveni/2-rocnik/06-excel-grafy.en.md b/content/programove-vybaveni/2-rocnik/06-excel-grafy.en.md
deleted file mode 100644
index a159302..0000000
--- a/content/programove-vybaveni/2-rocnik/06-excel-grafy.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Excel: charts
-popis: A column chart to compare, a line chart for time. A pie only for parts of a whole.
-nazev_cviceni: Quiz: charts
-nadpis_kvizu: Pick the chart type
----
diff --git a/content/programove-vybaveni/2-rocnik/06-excel-grafy.md b/content/programove-vybaveni/2-rocnik/06-excel-grafy.md
deleted file mode 100644
index 4eda9dd..0000000
--- a/content/programove-vybaveni/2-rocnik/06-excel-grafy.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-2-excel-grafy
-nazev: Excel: grafy
-popis: Sloupcový graf pro porovnání, spojnicový pro čas. Koláč jen pro podíl celku.
-nazev_cviceni: Kvíz: grafy
-nadpis_kvizu: Vyberte typ grafu
----
-
-# Výklad
-
-## Který graf
-*Typ*
-
-- Porovnání tříd nebo položek je sloupcový graf.
-- Vývoj po měsících je spojnicový. Osa x je čas, zleva doprava.
-- Koláč je jen pro podíly jednoho celku, nejvýš pět výsečí. Dvanáct měsíců v koláči nečtěte.
-- Dva grafy přes sebe, které říkají totéž, se nedělají.
-
-> Tip: Když váháte, sloupec je ve škole skoro vždycky bezpečnější než koláč.
-
-## Co musí být vidět
-*Popisky*
-
-- Graf má název, třeba Tržba po měsících. Název Graf 1 se neodevzdává.
-- Osa hodnot má jednotku, Kč nebo počet. Osa kategorií má názvy, ne 1 až 12 bez legendy měsíce, pokud to z dat není jasné.
-- Legenda se dá pryč, když je řada jen jedna. Zabírá místo a nic neříká.
-- Data grafu jsou v buňkách. Namalovat sloupec obrázkem nejde aktualizovat.
-
-> Tip: Klik na graf, Vybrat data. Když oblast nesedí, graf kreslí i záhlaví nebo prázdné řádky.
-
-## Odkud bere čísla
-*Tabulka*
-
-- Nejdřív čísla, potom graf. Graf bez zdroje je obrázek.
-- Nula v buňce je nula a v grafu je vidět. Prázdná buňka čáru přeruší.
-- Text v číselném sloupci graf vynechá nebo ho položí jako nulu. Čísla nepište s jednotkou Kč v téže buňce.
-- Změna čísla v tabulce změní graf. Když ne, graf je kopie jako obrázek.
-
-> Tip: Kč patří do záhlaví sloupce nebo do názvu osy, ne za číslo do buňky. Jinak je buňka text.
-
-## Zadání: jeden graf
-*Cvičení*
-
-- Na novém listu Mesice dejte leden až červen a k nim tržbu v Kč, šest čísel.
-- Vložte spojnicový graf s názvem Tržba leden až červen. Jednotka ať je v názvu osy nebo v názvu grafu.
-- Koláč nedělejte. Legendu jedné řady vypněte.
-- Uložte do znamky.xlsx jako další list, nebo do trzba.xlsx ve složce excel.
-
-> Tip: Náhled tisku ať graf neroztrhne na dvě stránky. Dejte ho pod tabulku, ne přes ni.
-
-# Kvíz
-
-## Jaký graf je na tržbu leden až červen?
-- Koláč se šesti výsečemi
-- [x] Spojnicový
-- Náhodný 3D kužel
-- Obrázek sloupců vložený z Wordu
-> Čas jde zleva doprava ve spojnicovém grafu. Koláč neukáže pořadí měsíců.
-
-## Kdy je koláč na místě?
-- Na dvanáct měsíců
-- [x] Na podíly jednoho celku, jen pár výsečí
-- Na každou tabulku
-- Když chybí název
-> Koláč je celek rozdělený na pár dílů. Časová řada do něj nepatří.
-
-## Kam nepatří Kč?
-- Do názvu osy
-- [x] Do buňky za číslo, třeba 120 Kč
-- Do názvu grafu
-- Do záhlaví sloupce
-> 120 Kč je text. Graf i SUMA pak číslo nevidí.
-
-## Co se stane po změně čísla v tabulce, když je graf v pořádku?
-- Graf se musí nakreslit znovu ručně
-- [x] Sloupec nebo bod se změní sám
-- Excel graf smaže
-- Změní se licence
-> Graf je navázaný na buňky. Obrázek vložený místo grafu se sám nezmění.
diff --git a/content/programove-vybaveni/2-rocnik/07-excel-kontingence.en.md b/content/programove-vybaveni/2-rocnik/07-excel-kontingence.en.md
deleted file mode 100644
index bb01bed..0000000
--- a/content/programove-vybaveni/2-rocnik/07-excel-kontingence.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Excel: a table, a filter and a pivot
-popis: A list as a table, a filter and a total by class.
-nazev_cviceni: Quiz: the pivot table
-nadpis_kvizu: Say what goes in rows and what goes in values
----
diff --git a/content/programove-vybaveni/2-rocnik/07-excel-kontingence.md b/content/programove-vybaveni/2-rocnik/07-excel-kontingence.md
deleted file mode 100644
index 8f91004..0000000
--- a/content/programove-vybaveni/2-rocnik/07-excel-kontingence.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-2-excel-kontingence
-nazev: Excel: tabulka, filtr a kontingenční tabulka
-popis: Seznam jako tabulka, filtr a součet podle třídy.
-nazev_cviceni: Kvíz: kontingenční tabulka
-nadpis_kvizu: Řekněte, co patří do řádků a co do hodnot
----
-
-# Výklad
-
-## Tabulka
-*Ctrl+T*
-
-- Seznam se záhlavím se označí a Ctrl+T z něj udělá tabulku. Filtr v záhlaví vznikne sám.
-- Záhlaví je jeden řádek: Polozka, Trida, Castka. Dva řádky nadpisů Ctrl+T rozbije.
-- Hned pod tabulku se nepíše další text. Souhrn je v řádku souhrnů, nebo na jiném listu.
-- Řazení je šipka v záhlaví. Seřadit jen půlku sloupce mimo tabulku rozhodí řádky.
-
-> Tip: Tabulka dostane na kartě Návrh název, třeba Prodej. Pak se na ni dá odkazovat jménem.
-
-## Filtr
-*Jen 1.A*
-
-- Filtr Trida = 1.A ostatní řádky schová. Nesmaže je.
-- Zrušení filtru je Vybrat vše, nebo Vymazat filtr na kartě Data.
-- Řádek souhrnů tabulky sečte jen to, co filtr nechá vidět.
-- Obyčejná SUMA přes stejné buňky sečte i řádky, které filtr schoval. Na filtrovaný součet to není.
-
-> Tip: Když po filtru zmizí skoro všechno, podívejte se, jestli nemáte mezeru v názvu třídy. 1.A a 1.A s mezerou navíc jsou dvě hodnoty.
-
-## Kontingenční tabulka
-*Součet podle třídy*
-
-- Kontingenční tabulka je na kartě Vložení. Zdroj je tabulka i se záhlavím.
-- Do řádků přijde Trida. Do hodnot Součet z Castka. Sloupce nechte prázdné, dokud není druhý rozměr.
-- Známky se v kontingenci nesčítají. U známek dejte průměr, u peněz součet.
-- Kontingence patří na nový list. Když ji položíte přes zdroj, zdroj přepíšete.
-
-> Tip: Po změně dat dejte Obnovit. Kontingenční tabulka se sama nehne.
-
-## Zadání: prodej po třídách
-*Cvičení*
-
-- List Prodej: aspoň 12 řádků, sloupce Polozka, Trida, Castka. Třídy jen 1.A a 1.B.
-- Ctrl+T a název tabulky Prodej. Filtr ať umí ukázat jen 1.A a zase všechno.
-- Kontingenční tabulka na novém listu: řádky Trida, hodnoty součet Castka.
-- Uložte prodej.xlsx do složky Skola\1-rocnik\excel. Ruční součty vedle kontingence nepište.
-
-> Tip: Když kontingence ukáže Počet místo Součet, v hodnotách počítá buňky. Přepněte souhrn na Součet.
-
-# Kvíz
-
-## Co udělá Ctrl+T na seznamu se záhlavím?
-- Smaže ho
-- [x] Udělá z něj tabulku s filtrem
-- Převede ho na PDF
-- Uzamkne list heslem
-> Tabulka má filtr v záhlaví a jde pojmenovat. Obyčejná oblast to nemá.
-
-## Co sečte jen řádky, které filtr nechá vidět?
-- Obyčejná SUMA přes celou oblast
-- [x] Řádek souhrnů tabulky
-- Nadpis grafu
-- Ručně opsaný součet
-> SUMA schované řádky filtru pořád počítá. Řádek souhrnů tabulky bere jen viditelné.
-
-## Kam v kontingenční tabulce patří Trida a kam Castka?
-- Trida do hodnot jako součet, Castka do řádků
-- [x] Trida do řádků, součet Castka do hodnot
-- Obojí do filtrů a nikam jinam
-- Trida do názvu souboru
-> Řádky jsou třídy. Hodnoty jsou součet peněz. Součet názvů tříd nedává smysl.
-
-## Kdy se u známek v kontingenci použije průměr?
-- Vždy se známky sčítají
-- [x] Když má vyjít průměrná známka, ne součet známek
-- Průměr Excel neumí
-- Jen u textu
-> Součet známek nic neříká. Peníze se sčítají, známky se průměrují.
diff --git a/content/programove-vybaveni/2-rocnik/08-zadani-office.en.md b/content/programove-vybaveni/2-rocnik/08-zadani-office.en.md
deleted file mode 100644
index 04488f3..0000000
--- a/content/programove-vybaveni/2-rocnik/08-zadani-office.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Assignment: a report and a sheet
-popis: A Word letter and an Excel grade sheet in one folder.
-nazev_cviceni: Quiz: the Office hand-in
-nadpis_kvizu: Check both files
----
diff --git a/content/programove-vybaveni/2-rocnik/08-zadani-office.md b/content/programove-vybaveni/2-rocnik/08-zadani-office.md
deleted file mode 100644
index 4c2a906..0000000
--- a/content/programove-vybaveni/2-rocnik/08-zadani-office.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-2-zadani-office
-nazev: Zadání: protokol a tabulka
-popis: Dopis ve Wordu a známky v Excelu v jedné složce.
-nazev_cviceni: Kvíz: odevzdání Office
-nadpis_kvizu: Zkontrolujte oba soubory
----
-
-# Výklad
-
-## Word
-*Žádost*
-
-- zadost.docx je jedna strana A4, okraje 2,5 cm, datum 12. 6. 2026 vpravo nahoře.
-- Je v ní řádek Věc, oslovení, aspoň dvě věty a podpis. Není v ní obsah ani tabulka.
-- K protokolu z dřívějška, pokud ho máte, patří styly Nadpis 1 a PDF.
-- PDF se jmenuje stejně jako docx. Dvě verze final a final2 se neodevzdávají.
-
-> Tip: Náhled tisku má ukázat jednu stránku. Podpis na druhé stránce znamená moc velké mezery.
-
-## Excel
-*Známky*
-
-- znamky.xlsx má list Znamky: jméno, známka, stav.
-- Stav je vzorec =KDYŽ(D2<=4;"prospěl";"neprospěl") nebo se sloupcem známky, který opravdu máte. Známka 5 je neprospěl.
-- Pod tabulkou je PRŮMĚR známek, ne jejich součet.
-- Čísla jsou čísla. Text 1 nebo 120 Kč ve stejné buňce se nepočítá.
-
-> Tip: Klik na buňku stavu musí v řádku vzorců ukázat KDYŽ. Opsané slovo prospěl se při změně známky nehne.
-
-## Kontrola vzorců
-*Ne opis*
-
-- V ceníku, pokud ho přikládáte, je částka =B2*C2 a sazba ukotvená jako $F$1.
-- SVYHLEDAT, pokud ho máte, končí na NEPRAVDA a oblast je ukotvená.
-- Graf tržby je spojnicový, má název a není to koláč dvanácti měsíců.
-- Kontingenční tabulka, pokud je v sešitu, stojí na vlastním listu a sčítá částky po třídách.
-
-> Tip: Stačí žádost a známky. Ceník, graf a kontingence jsou navíc, ale když je přiložíte, musí sedět.
-
-## Složka
-*Dva hlavní soubory*
-
-- Ve složce Skola\1-rocnik\word je zadost.docx a zadost.pdf.
-- Ve složce Skola\1-rocnik\excel je znamky.xlsx.
-- Na ploše tyto soubory nejsou. Přípony jsou vidět.
-- Otevřete oba znovu. Datum, věc a aspoň jedna pětka se stavem neprospěl mají být vidět.
-
-> Tip: Když PDF nejde otevřít, export nedoběhl. Udělejte ho z docx znovu.
-
-# Kvíz
-
-## Jaký je školní dopis z tohoto zadání?
-- Tři strany se styly a obsahem
-- [x] Jedna strana A4 s řádkem Věc a datem 12. 6. 2026
-- Jen tabulka 4×3
-- Prezentace
-> Žádost je jeden list. Obsah a Nadpis 1 patří protokolu, ne tomuto dopisu.
-
-## Který vzorec označí pětku jako neprospěl?
-- [x] =KDYŽ(D2<=4;"prospěl";"neprospěl")
-- =KDYŽ(D2>=4;"prospěl";"neprospěl")
-- napsané slovo prospěl
-- =SUMA(D2)
-> Prochází známka 1 až 4. Pětka padne do druhé větve. V češtině jsou středníky.
-
-## Co patří pod sloupec známek?
-- Součet známek
-- [x] Průměr
-- Kontingenční součet peněz
-- Nic, známky se mažou
-> Součet známek nemá význam. PRŮMĚR ano.
-
-## Které soubory se odevzdávají jako základ?
-- Jen PDF bez docx
-- [x] zadost.docx, zadost.pdf a znamky.xlsx
-- Jen xlsx na ploše
-- prezentace a video
-> Word se odevzdává i jako zdroj, i jako PDF. Excel je xlsx, protože v PDF by nebyly vzorce.
diff --git a/content/programove-vybaveni/3-rocnik/01-databaze.en.md b/content/programove-vybaveni/3-rocnik/01-databaze.en.md
deleted file mode 100644
index e466ba7..0000000
--- a/content/programove-vybaveni/3-rocnik/01-databaze.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: A database
-popis: A table, a record, a key. When a sheet is no longer enough.
-nazev_cviceni: Quiz: databases
-nadpis_kvizu: Name the key and the reason for a database
----
diff --git a/content/programove-vybaveni/3-rocnik/01-databaze.md b/content/programove-vybaveni/3-rocnik/01-databaze.md
deleted file mode 100644
index bcda465..0000000
--- a/content/programove-vybaveni/3-rocnik/01-databaze.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-3-databaze
-nazev: Databáze
-popis: Tabulka, záznam, klíč. Kdy už nestačí Excel.
-nazev_cviceni: Kvíz: databáze
-nadpis_kvizu: Poznejte klíč a důvod databáze
----
-
-# Výklad
-
-## Excel nestačí
-*Vztahy*
-
-- Excel je výborný na jeden seznam. Jakmile se třída opakuje na každém řádku žáka, začne se název třídy rozcházet.
-- Databáze drží věci v samostatných tabulkách a spojuje je klíčem.
-- Tabulka tridy má název třídy jednou. Tabulka zaci se na ni jen odkáže.
-- Access i MySQL umí totéž v principu. Access je soubor accdb, MySQL je server a příkazy SQL.
-
-> Tip: Když opravujete 1.A na 1. A v padesáti řádcích Excelu, má to být tabulka tříd, ne opis.
-
-## Řádek a sloupec
-*Záznam a pole*
-
-- Řádek je záznam, třeba jeden žák. Sloupec je pole, třeba prijmeni.
-- Každá hodnota je atomická. Jméno a příjmení nepatří do jedné buňky.
-- Primární klíč je hodnota, která se v tabulce neopakuje. U žáka je to id, ne příjmení.
-- Cizí klíč je pole, které ukazuje na primární klíč jiné tabulky. trida_id v tabulce zaci ukazuje na id v tridy.
-
-> Tip: Dva Nováci se smějí jmenovat stejně. Jejich id je různé. Proto příjmení klíč není.
-
-## Typy
-*Číslo, text, datum*
-
-- id je číslo. Jméno je krátký text. Datum narození je datum, ne text 12.6.
-- Ano/ne je samostatný typ, ne text ano.
-- Do číselného pole nepatří jednotka. Počet žáků je 28, ne 28 žáků.
-- Prázdné pole a nula nejsou totéž. Nula je známá hodnota, prázdno je nevíme.
-
-> Tip: Text v číselném poli se nepodaří uložit, nebo se sečte špatně. Typ zvolte při založení sloupce.
-
-## Zadání: návrh na papír
-*Cvičení*
-
-- Navrhněte dvě tabulky: tridy (id, nazev) a zaci (id, jmeno, prijmeni, trida_id).
-- U každého pole napište typ. id v obou tabulkách je primární klíč.
-- Napište dva řádky tříd, 1.A a 1.B, a tři žáky, z toho Annu Novákovou ve třídě 1.A.
-- Soubor navrh.txt uložte k databázím. Access ani MySQL ještě neotevírejte, dokud návrh nesedí.
-
-> Tip: Šipka vede od trida_id k id tabulky tridy. Opačně by žák byl klíčem třídy, což není.
-
-# Kvíz
-
-## Co je primární klíč žáka?
-- Příjmení
-- [x] id, které se neopakuje
-- Název třídy
-- První sloupec, ať je v něm cokoli
-> Příjmení se opakuje. id jednoho žáka ne.
-
-## Co je trida_id v tabulce zaci?
-- Primární klíč třídy v téže tabulce
-- [x] Cizí klíč na tabulku tridy
-- Text 1.A opsaný do každého řádku
-- Heslo
-> Cizí klíč drží jen číslo třídy. Název třídy žije v tabulce tridy jednou.
-
-## Kdy už je Excel na seznam žáků těsný?
-- Jakmile má víc než jeden sloupec
-- [x] Když se údaj, třeba název třídy, opakuje a musí zůstat stejný
-- Excel nestačí nikdy
-- Jakmile je soubor xlsx
-> Opakovaný název se rozjede. Databáze ho má v jedné tabulce a ostatní se na ni odkážou.
-
-## Čím se liší nula a prázdné pole?
-- Ničím
-- [x] Nula je hodnota, prázdné pole znamená, že hodnota chybí
-- Prázdné pole je vždy nula
-- Nula je text
-> Průměr nulu počítá. Prázdnou buňku často ne. Typ i význam se liší.
diff --git a/content/programove-vybaveni/3-rocnik/02-access-tabulky.en.md b/content/programove-vybaveni/3-rocnik/02-access-tabulky.en.md
deleted file mode 100644
index 3ee054f..0000000
--- a/content/programove-vybaveni/3-rocnik/02-access-tabulky.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Access: tables and a relationship
-popis: An AutoNumber, a foreign key and a one-to-many link without a cascade.
-nazev_cviceni: Quiz: Access
-nadpis_kvizu: Pick the key type and the cascade
----
diff --git a/content/programove-vybaveni/3-rocnik/02-access-tabulky.md b/content/programove-vybaveni/3-rocnik/02-access-tabulky.md
deleted file mode 100644
index c92654e..0000000
--- a/content/programove-vybaveni/3-rocnik/02-access-tabulky.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-3-access-tabulky
-nazev: Access: tabulky a vztah
-popis: Automatické číslo, cizí klíč a vztah 1:N bez kaskády.
-nazev_cviceni: Kvíz: Access
-nadpis_kvizu: Vyberte typ klíče a kaskádu
----
-
-# Výklad
-
-## Soubor
-*accdb*
-
-- Databáze Accessu je jeden soubor accdb. Jmenuje se evidence.accdb.
-- Tabulku založíte v návrhovém zobrazení, ne jen v datovém listu. V návrhu je vidět typ.
-- Tabulka tridy má pole id a nazev. Tabulka zaci má id, jmeno, prijmeni a trida_id.
-- Název pole je bez mezery a bez diakritiky: trida_id. Popisek pro lidi se dá nastavit zvlášť.
-
-> Tip: Datový list je na zapisování řádků. Typ pole se mění v návrhu. Když už v tabulce data jsou, změna typu může selhat.
-
-## Klíče
-*Automatické číslo*
-
-- id je typ Automatické číslo a je to primární klíč. Access ho doplňuje sám: 1, 2, 3.
-- trida_id je typ Číslo, dlouhé celé číslo. Není to automatické číslo. Píšete ho vy, nebo ho vybere vztah.
-- nazev, jmeno a prijmeni jsou Krátký text. Délka 50 stačí.
-- Dva primární klíče v jedné tabulce školního cvičení nejsou. Stačí id.
-
-> Tip: Když trida_id necháte jako text, vztah na číselné id třídy Access odmítne. Typy obou konců musí sedět.
-
-## Vztah 1:N
-*Bez kaskády*
-
-- Jedna třída má víc žáků. Žák má jednu třídu. To je vztah 1:N.
-- Ve vztazích přetáhnete tridy.id na zaci.trida_id.
-- Zaškrtněte vynucení referenční integrity. Žák se nesmí odkázat na třídu, která neexistuje.
-- Kaskádové odstranění nechte vypnuté. Smazání třídy by jinak smazalo i žáky.
-
-> Tip: Na straně tridy je jednička, na straně zaci je znak nekonečna. Když je to obráceně, táhli jste vztah špatným směrem.
-
-## Zadání: evidence.accdb
-*Cvičení*
-
-- Založte evidence.accdb s tabulkami tridy a zaci podle návrhu.
-- Vložte třídy 1.A a 1.B. Vložte Annu Novákovou s trida_id třídy 1.A a ještě dva žáky.
-- Vztah 1:N s referenční integritou, bez kaskádového odstranění.
-- Zkuste uložit žáka s trida_id 99. Access ho má odmítnout. Pak to číslo vraťte na platnou třídu.
-
-> Tip: Když jde uložit trida_id 99, integrita není zapnutá. Otevřete vztahy a vztah vytvořte znovu.
-
-# Kvíz
-
-## Jaký typ má id žáka?
-- Krátký text, který píšete ručně
-- [x] Automatické číslo a primární klíč
-- Ano/ne
-- Dlouhý text s celým jménem třídy
-> Automatické číslo žák nedoplňuje. Access ho dá samo a v tabulce se neopakuje.
-
-## Jaký typ má trida_id?
-- Automatické číslo
-- [x] Číslo, dlouhé celé číslo
-- Datum
-- Ano/ne
-> Je to cizí klíč na id třídy, které je taky číslo. Automatické číslo by si vymýšlelo vlastní hodnoty.
-
-## Co dělá referenční integrita?
-- Smaže žáky při smazání třídy
-- [x] Nedovolí žáka s třídou, která neexistuje
-- Schová tabulku
-- Převede accdb na xlsx
-> Odkaz musí vést na skutečný řádek. Třída 99 bez řádku v tridy projít nemá.
-
-## Proč nechat kaskádové odstranění vypnuté?
-- Access ho neumí
-- [x] Smazání třídy by smazalo i její žáky
-- Jinak nejde uložit accdb
-- Kvůli Excelu
-> Třída a žáci jsou různá data. Smazat třídu omylem a přijít o žáky je špatná pojistka pro školní cvičení.
diff --git a/content/programove-vybaveni/3-rocnik/03-access-dotazy.en.md b/content/programove-vybaveni/3-rocnik/03-access-dotazy.en.md
deleted file mode 100644
index 721c282..0000000
--- a/content/programove-vybaveni/3-rocnik/03-access-dotazy.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Access: queries
-popis: A query that shows only class 1.A, and a criterion in quotes.
-nazev_cviceni: Quiz: queries
-nadpis_kvizu: Fill in the criterion
----
diff --git a/content/programove-vybaveni/3-rocnik/03-access-dotazy.md b/content/programove-vybaveni/3-rocnik/03-access-dotazy.md
deleted file mode 100644
index 679b605..0000000
--- a/content/programove-vybaveni/3-rocnik/03-access-dotazy.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-3-access-dotazy
-nazev: Access: dotazy
-popis: Dotaz, který ukáže jen třídu 1.A, a kritérium v uvozovkách.
-nazev_cviceni: Kvíz: dotazy
-nadpis_kvizu: Doplňte kritérium
----
-
-# Výklad
-
-## Dotaz není kopie
-*Pohled*
-
-- Dotaz data nekopíruje do nové tabulky. Přečte je a ukáže výsledek.
-- Když ve třídě přibude žák, stejný dotaz ho příště ukáže taky.
-- Dotaz se dělá v návrhovém zobrazení. Tabulky se přidají a pole se přetáhnou dolů.
-- Uložený dotaz se jmenuje zaci_1A. Dotaz1 se neodevzdává.
-
-> Tip: Když dotaz otevřete a data změníte, měníte zdrojovou tabulku. Na prohlížení to vadit nemusí, na omyl ano.
-
-## Kritérium
-*1.A*
-
-- Pod pole nazev z tabulky tridy patří kritérium "1.A" včetně uvozovek.
-- Bez uvozovek Access text nepozná a dotaz selže, nebo se zeptá na parametr.
-- Číselné kritérium uvozovky nemá. id 1 je prostě 1.
-- Chcete-li žáky obou tříd, kritérium 1.A tam nedávejte. Filtr, který tam zbyl z minula, výsledek ořeže.
-
-> Tip: Řádek Kritéria je filtr. Řádek Zobrazit schová sloupec, který v dotazu být musí, ale ve výsledku ho nechcete.
-
-## Spojení v dotazu
-*Obě tabulky*
-
-- Do dotazu přidejte tridy i zaci. Access použije vztah, který už máte.
-- Ven dejte prijmeni, jmeno a nazev třídy. id ven dávat nemusíte.
-- Řazení podle prijmeni je vzestupně. V návrhové mřížce je na to řádek Řadit.
-- Žák bez třídy se při vnitřním spojení neukáže. Díky integritě takový řádek stejně nemá vzniknout.
-
-> Tip: Když se každý žák ukáže dvakrát, tabulky nejsou spojené a Access udělal všechny kombinace. Vraťte vztah.
-
-## Zadání: jen 1.A
-*Cvičení*
-
-- Dotaz zaci_1A ukáže jméno, příjmení a název třídy, jen pro 1.A, seřazené podle příjmení.
-- Anna Nováková v něm je. Žák z 1.B v něm není.
-- Uložte dotaz do evidence.accdb. Výsledek nekopírujte do Excelu jako jediný výstup.
-- Změňte v tabulce příjmení Anny na Novák a dotaz otevřete znovu. Má ukázat Novák.
-
-> Tip: Když se zeptá na parametr 1.A, kritérium nemá uvozovky a Access ho bere jako otázku.
-
-# Kvíz
-
-## Kam patří podmínka "1.A"?
-- Do názvu souboru
-- [x] Do řádku Kritéria pod polem nazev
-- Do primárního klíče místo id
-- Do kaskádového odstranění
-> Kritéria filtrují řádky. Pole nazev drží text 1.A.
-
-## Proč jsou u 1.A uvozovky?
-- [x] Access jinak text pochopí jako parametr nebo chybu
-- Protože 1.A je číslo
-- Uvozovky se v Accessu nepoužívají
-- Aby se zapnula kaskáda
-> Textové kritérium se píše v uvozovkách. Číslo ne.
-
-## Co se stane s dotazem, když do třídy 1.A přidáte žáka?
-- Dotaz je stará kopie a žáka neukáže
-- [x] Po novém otevření ho ukáže
-- Access databázi zavře
-- Musíte ho opsat do Excelu
-> Dotaz čte tabulky. Není to zmrazený export.
-
-## Proč se žák v dotazu objeví dvakrát u každé třídy?
-- Má dva primární klíče
-- [x] Tabulky v dotazu nejsou spojené vztahem
-- Je seřazený podle příjmení
-- Filtr je vypnutý
-> Bez spojení Access zkombinuje každý řádek s každým. Vztah tomu zabrání.
diff --git a/content/programove-vybaveni/3-rocnik/04-access-formulare.en.md b/content/programove-vybaveni/3-rocnik/04-access-formulare.en.md
deleted file mode 100644
index b3cd488..0000000
--- a/content/programove-vybaveni/3-rocnik/04-access-formulare.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Access: a form and a report
-popis: Entry through a form and printing through a report. The datasheet is not for users.
-nazev_cviceni: Quiz: forms
-nadpis_kvizu: Pick what is for entry and what is for print
----
diff --git a/content/programove-vybaveni/3-rocnik/04-access-formulare.md b/content/programove-vybaveni/3-rocnik/04-access-formulare.md
deleted file mode 100644
index 37f9b48..0000000
--- a/content/programove-vybaveni/3-rocnik/04-access-formulare.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-3-access-formulare
-nazev: Access: formulář a sestava
-popis: Zápis přes formulář a tisk přes sestavu. Datový list není pro uživatele.
-nazev_cviceni: Kvíz: formulář
-nadpis_kvizu: Vyberte, čím se data zadávají a čím tisknou
----
-
-# Výklad
-
-## Formulář
-*Jeden žák*
-
-- Formulář ukáže jeden záznam jako políčka. Datový list ukáže mřížku a tam se běžný uživatel snadno trefí do špatného řádku.
-- Formulář Zaci se založí z tabulky zaci. Pole id ať je vidět, ale zamčené. Automatické číslo se nepíše.
-- trida_id je lépe rozbalovací seznam, který ukazuje název třídy a ukládá id.
-- Formulář se jmenuje frmZaci. Výchozí Formulář1 se neodevzdává.
-
-> Tip: Rozbalovací seznam bere řádky z tabulky tridy. Viditelný sloupec je nazev, vázaný sloupec je id.
-
-## Sestava
-*Tisk*
-
-- Sestava je na tisk a na PDF. Formulář je na zápis. Neslouží k tomu totéž.
-- Sestava žáků je seskupená podle třídy a v rámci třídy seřazená podle příjmení.
-- Záhlaví skupiny ukáže 1.A jednou, ne na každém řádku jako jediný nadpis celé sestavy.
-- Do sestavy nepatří tlačítka formuláře. Patří do ní nadpis Evidence žáků a číslo stránky.
-
-> Tip: Náhled sestavy je před tiskem povinný. Prázdná druhá stránka znamená velkou zápatí nebo prázdný oddíl.
-
-## Kdo kam smí
-*Ne do návrhu*
-
-- Uživatel, který jen zapisuje žáky, dostane formulář. Návrhové zobrazení ne.
-- Mazání záznamu na formuláři je vypnuté, dokud učitel neřekne jinak.
-- Sestava se needituje. Když v ní přepíšete jméno, často přepisujete data, a to z sestavy dělat nechcete.
-- Záloha je kopie accdb před mazáním. Jediný soubor na ploše záloha není.
-
-> Tip: Soubor, Uložit jako, a datum v názvu: evidence-2026-06-12.accdb. Pak teprve mažete zkušební řádky.
-
-## Zadání: formulář a sestava
-*Cvičení*
-
-- frmZaci zapíše jméno, příjmení a třídu. id nejde přepsat.
-- Sestava seskupí žáky podle třídy. Anna Nováková je pod 1.A.
-- Z formuláře přidejte jednoho žáka do 1.B a v sestavě ho najděte.
-- Uložte vše do evidence.accdb. PDF sestavy je evidence-zaci.pdf vedle souboru.
-
-> Tip: Když v rozbalovacím seznamu vidíte čísla místo 1.A, zobrazujete id. Přepněte šířku sloupců, ať je vidět nazev.
-
-# Kvíz
-
-## Čím se ve škole zadává nový žák?
-- Přímo v návrhovém zobrazení tabulky
-- [x] Formulářem frmZaci
-- Přepsáním sestavy
-- Excelem místo Accessu bez uložení accdb
-> Formulář ukáže jeden záznam a umí seznam tříd. Návrh tabulky je pro autora databáze.
-
-## K čemu je sestava?
-- K mazání sloupců
-- [x] K tisku a PDF, seskupenému podle třídy
-- K nastavení automatického čísla
-- K instalaci Accessu
-> Sestava formátuje výstup. Zápis zůstává formuláři.
-
-## Proč je id na formuláři zamčené?
-- [x] Automatické číslo se nepíše ručně
-- Access id neumí ukázat
-- Jinak nejde sestava
-- Protože je to text
-> Číslo přidělí Access. Ruční přepsání by rozbilo klíč.
-
-## Co uděláte před mazáním zkušebních řádků?
-- Nic, accdb se sám verzuje
-- [x] Kopie souboru s datem v názvu
-- Smažete vztah
-- Export jen do JPG
-> Jedna kopie accdb je záloha. Mazání bez ní nejde vzít zpět tak snadno jako Ctrl+Z ve Wordu.
diff --git a/content/programove-vybaveni/3-rocnik/05-mysql-select.en.md b/content/programove-vybaveni/3-rocnik/05-mysql-select.en.md
deleted file mode 100644
index 4e04663..0000000
--- a/content/programove-vybaveni/3-rocnik/05-mysql-select.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: MySQL: SELECT
-popis: Choosing columns, a WHERE condition and a sort. Text sits in single quotes.
-nazev_cviceni: Quiz: SELECT
-nadpis_kvizu: Pick the right statement
----
diff --git a/content/programove-vybaveni/3-rocnik/05-mysql-select.md b/content/programove-vybaveni/3-rocnik/05-mysql-select.md
deleted file mode 100644
index 2595d1b..0000000
--- a/content/programove-vybaveni/3-rocnik/05-mysql-select.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-3-mysql-select
-nazev: MySQL: SELECT
-popis: Výběr sloupců, podmínka WHERE a řazení. Text je v jednoduchých uvozovkách.
-nazev_cviceni: Kvíz: SELECT
-nadpis_kvizu: Vyberte správný příkaz
----
-
-# Výklad
-
-## Databáze skola
-*Dvě tabulky*
-
-- Na serveru je databáze skola. Tabulka tridy má id a nazev. Tabulka zaci má id, jmeno, prijmeni a trida_id.
-- Příkazy se píšou v MySQL, ne do Excelu. Středník příkaz ukončí.
-- Nejdřív USE skola; ať další příkazy nemíří do jiné databáze.
-- Heslo k serveru do sešitu nepatří. Patří učiteli a do klienta, který ho neukazuje v příkazu.
-
-> Tip: Když klient hlásí, že tabulka neexistuje, jste v jiné databázi, nebo je v názvu překlep. SHOW TABLES; ukáže, co tam je.
-
-## SELECT
-*Jen to, co potřebujete*
-
-- SELECT prijmeni, jmeno FROM zaci; vrátí dva sloupce všech žáků.
-- Hvězdička SELECT * FROM zaci; je na průzkum. Do odevzdaného dotazu vyberte sloupce jménem.
-- WHERE trida_id = 1 nechá jen jednu třídu. Číslo je bez uvozovek.
-- Text se píše v jednoduchých uvozovkách: WHERE nazev = '1.A'.
-
-> Tip: Dvojité uvozovky kolem 1.A záleží na režimu serveru. Ve škole pište jednoduché, ty platí spolehlivě.
-
-## Pořadí
-*ORDER BY*
-
-- ORDER BY prijmeni; seřadí vzestupně, tedy A až Ž.
-- ORDER BY prijmeni DESC; je sestupně.
-- WHERE patří před ORDER BY. Obrácené pořadí příkazů server odmítne.
-- LIMIT 5 vrátí nejvýš pět řádků. Na kontrolu ano, na úplný seznam třídy ne.
-
-> Tip: Nejdřív FROM, pak WHERE, pak ORDER BY. SELECT je na začátku, i když se sloupce vybírají jako první v hlavě.
-
-## Zadání: tři dotazy
-*Cvičení*
-
-- Do souboru dotazy.sql napište USE skola; a tři SELECT.
-- První: příjmení a jméno všech žáků podle příjmení.
-- Druhý: jen žáci s trida_id = 1.
-- Třetí: název třídy 1.A z tabulky tridy. Text '1.A' je v jednoduchých uvozovkách.
-
-> Tip: Každý příkaz ukončete středníkem. Bez něj klient čeká na pokračování.
-
-# Kvíz
-
-## Který příkaz vrátí příjmení žáků třídy s id 1?
-- [x] SELECT prijmeni FROM zaci WHERE trida_id = 1;
-- SELECT prijmeni WHERE trida_id = 1;
-- SELECT * FROM tridy WHERE nazev = 1.A;
-- WHERE trida_id = 1;
-> SELECT říká sloupce, FROM tabulku, WHERE podmínku. Číslo je bez uvozovek.
-
-## Jak se v podmínce zapíše název třídy?
-- nazev = 1.A
-- [x] nazev = '1.A'
-- nazev = "bez uvozovek a bez čísla"
-- nazev := 1.A
-> Text patří do jednoduchých uvozovek. Bez nich to není řetězec.
-
-## Kam patří ORDER BY?
-- Před SELECT
-- [x] Za WHERE
-- Místo FROM
-- Do názvu tabulky
-> Pořadí je WHERE, potom ORDER BY. SELECT zůstává první.
-
-## K čemu je ve škole SELECT * ?
-- Na odevzdaný tisk všech osobních údajů
-- [x] Na rychlý pohled, co v tabulce je
-- Místo primárního klíče
-- Na smazání řádků
-> Hvězdička je průzkum. Odevzdaný dotaz jmenuje sloupce, které mají být vidět.
diff --git a/content/programove-vybaveni/3-rocnik/06-mysql-zmeny.en.md b/content/programove-vybaveni/3-rocnik/06-mysql-zmeny.en.md
deleted file mode 100644
index a00bb9c..0000000
--- a/content/programove-vybaveni/3-rocnik/06-mysql-zmeny.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: MySQL: INSERT, UPDATE and DELETE
-popis: A new row, a change and a delete. An UPDATE or DELETE without WHERE is not written in class.
-nazev_cviceni: Quiz: changing data
-nadpis_kvizu: Recognise the safe statement
----
diff --git a/content/programove-vybaveni/3-rocnik/06-mysql-zmeny.md b/content/programove-vybaveni/3-rocnik/06-mysql-zmeny.md
deleted file mode 100644
index 63fc270..0000000
--- a/content/programove-vybaveni/3-rocnik/06-mysql-zmeny.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-3-mysql-zmeny
-nazev: MySQL: INSERT, UPDATE a DELETE
-popis: Nový řádek, oprava a smazání. UPDATE a DELETE bez WHERE se ve škole nepíšou.
-nazev_cviceni: Kvíz: změny dat
-nadpis_kvizu: Poznejte bezpečný příkaz
----
-
-# Výklad
-
-## INSERT
-*Nový žák*
-
-- INSERT INTO zaci (jmeno, prijmeni, trida_id) VALUES ('Anna', 'Nováková', 1); přidá jeden řádek.
-- Sloupce v závorce a hodnoty za VALUES musí být ve stejném pořadí a stejném počtu.
-- id s automatickým číslem do výčtu nedávejte, server ho doplní.
-- Jeden INSERT, jeden žák. Balit pět žáků do nepřehledného příkazu ve cvičení nemusíte.
-
-> Tip: Když server hlásí neznámý sloupec, název nesedí s tabulkou. SHOW COLUMNS FROM zaci; ukáže pravdu.
-
-## UPDATE
-*Jen jeden řádek*
-
-- UPDATE zaci SET trida_id = 2 WHERE id = 15; přepíše třídu jen žáku 15.
-- Bez WHERE by stejný příkaz přepsat třídu všem. Ve škole se neodevzdává.
-- Nejdřív SELECT s stejnou podmínkou WHERE. Až když vidíte jeden správný řádek, pusťte UPDATE.
-- SET mění hodnoty. Není to SELECT a nic nevrací kromě počtu ovlivněných řádků.
-
-> Tip: Klient ukáže, kolik řádků se změnilo. Když vidíte desítky a čekali jste jeden, zastavte se a volejte učitele. Záloha se hodí předtím, ne potom.
-
-## DELETE
-*Také s WHERE*
-
-- DELETE FROM zaci WHERE id = 15; smaže jeden řádek.
-- DELETE FROM zaci; bez WHERE smaže všechny žáky. To není čištění tabulky, to je ztráta dat.
-- Smazaný řádek Ctrl+Z v MySQL nevrátí. Vrátí ho záloha, nebo ho vložíte znovu, když znáte hodnoty.
-- Mazat třídu, na kterou ještě ukazují žáci, referenční pravidlo nemá dovolit. Nejdřív žáci, nebo třídu nemazat.
-
-> Tip: Před DELETE udělejte SELECT se stejnou podmínkou. Počet řádků musí být ten, který čekáte, obvykle jeden.
-
-## Zadání: tři změny na kopii
-*Cvičení*
-
-- Na cvičných datech vložte žáka Petr Svoboda do třídy 1, tedy trida_id 1.
-- UPDATE mu změňte třídu na 2, ale jen přes jeho id, které jste předtím přečetli SELECTem.
-- Téhož žáka DELETE smažte zase jen podle id. Ostatní žáci v tabulce zůstanou.
-- Tři příkazy připojte do dotazy.sql pod komentář, který říká, že UPDATE a DELETE mají WHERE.
-
-> Tip: Komentář v SQL začíná dvěma pomlčkami. Do příkazu se nepočítá, příštímu čtenáři řekne, proč tam WHERE je.
-
-# Kvíz
-
-## Který příkaz přidá Annu do třídy 1?
-- [x] INSERT INTO zaci (jmeno, prijmeni, trida_id) VALUES ('Anna', 'Nováková', 1);
-- SELECT Anna INTO zaci;
-- UPDATE zaci SET Anna;
-- DELETE FROM zaci WHERE jmeno = 'Anna';
-> INSERT vkládá řádek. Sloupce a hodnoty drží pořadí.
-
-## Jak se změní třída jen žáku s id 15?
-- UPDATE zaci SET trida_id = 2;
-- [x] UPDATE zaci SET trida_id = 2 WHERE id = 15;
-- SELECT trida_id = 2;
-- INSERT INTO zaci VALUES (15);
-> Bez WHERE by se třída přepsala všem. WHERE id = 15 drží jeden řádek.
-
-## Co udělá DELETE FROM zaci; bez WHERE?
-- Nesmaže nic, WHERE je volitelné a server se zeptá
-- [x] Smaže všechny řádky v zaci
-- Smaže jen poslední řádek
-- Jen vypíše žáky
-> Bez podmínky je cílem celá tabulka. Ve školním cvičení takový příkaz nepište.
-
-## Co uděláte těsně před UPDATE?
-- DELETE bez WHERE
-- [x] SELECT se stejnou podmínkou, ať vidíte ty řádky
-- DROP tabulky
-- Vypnete integritu
-> SELECT ukáže, na koho UPDATE dopadne. Až pak má smysl stejné WHERE použít ke změně.
diff --git a/content/programove-vybaveni/3-rocnik/07-mysql-join.en.md b/content/programove-vybaveni/3-rocnik/07-mysql-join.en.md
deleted file mode 100644
index b97432f..0000000
--- a/content/programove-vybaveni/3-rocnik/07-mysql-join.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: MySQL: joining tables
-popis: An INNER JOIN on trida_id, and why the class name is not repeated on every row.
-nazev_cviceni: Quiz: JOIN
-nadpis_kvizu: Complete the join
----
diff --git a/content/programove-vybaveni/3-rocnik/07-mysql-join.md b/content/programove-vybaveni/3-rocnik/07-mysql-join.md
deleted file mode 100644
index 62e5a1b..0000000
--- a/content/programove-vybaveni/3-rocnik/07-mysql-join.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-3-mysql-join
-nazev: MySQL: spojení tabulek
-popis: INNER JOIN přes trida_id a proč se název třídy neopakuje v každém řádku.
-nazev_cviceni: Kvíz: JOIN
-nadpis_kvizu: Doplňte spojení
----
-
-# Výklad
-
-## Jedna pravda
-*Název jen jednou*
-
-- Název 1.A je v tabulce tridy jednou. V zaci je jen trida_id.
-- Když by název ležel i u žáka, oprava překlepu znamená projít všechny žáky.
-- To je důvod dvou tabulek. Excel to umí jen ručně, databáze vztahem.
-- JOIN tabulky spojí ve výsledku dotazu. Nesloučí je do jedné natrvalo.
-
-> Tip: Po JOIN pořád existují dvě tabulky. Výsledek dotazu je jen pohled pro tento příkaz.
-
-## INNER JOIN
-*Shoda klíčů*
-
-- INNER JOIN vrátí řádky, kde se klíče sejdou. Žák a jeho třída.
-- ON z.trida_id = t.id je podmínka spojení. Není to totéž co WHERE na název.
-- Bez ON server buď příkaz odmítne, nebo spojí každý řádek s každým.
-- Přezdívky z a t jsou v dotazu proto, aby bylo jasné, která tabulka je která.
-
-> Tip: WHERE t.nazev = '1.A' přijde až po spojení. Nejdřív se tabulky potkají, pak se filtruje.
-
-## Celý příkaz
-*Jak má vypadat*
-
-- SELECT z.prijmeni, z.jmeno, t.nazev FROM zaci AS z INNER JOIN tridy AS t ON z.trida_id = t.id;
-- WHERE t.nazev = '1.A' z toho udělá jen jednu třídu.
-- ORDER BY z.prijmeni výsledek seřadí.
-- Sloupce pište s přezdívkou tabulky. Samotné id by bylo nejasné, mají ho obě tabulky.
-
-> Tip: Když příkaz hlásí nejednoznačný sloupec id, napište z.id nebo t.id. Server nehádá.
-
-## Zadání: jeden JOIN
-*Cvičení*
-
-- Do dotazy.sql přidejte dotaz, který vypíše příjmení, jméno a název třídy.
-- Jen třída 1.A, seřazené podle příjmení.
-- Anna Nováková v něm je včetně textu 1.A, ne jen čísla 1.
-- Příkaz spusťte dvakrát. Výsledek má být stejný, JOIN data nemění.
-
-> Tip: Když vidíte číslo místo 1.A, vybíráte trida_id a ne t.nazev. Doplňte sloupec z tabulky tridy.
-
-# Kvíz
-
-## Které spojení ukáže žáka i název jeho třídy?
-- SELECT * FROM zaci;
-- [x] SELECT z.prijmeni, t.nazev FROM zaci AS z INNER JOIN tridy AS t ON z.trida_id = t.id;
-- DELETE FROM tridy;
-- INSERT INTO tridy VALUES ('1.A');
-> JOIN vezme příjmení z žáků a název z tříd tam, kde se id shoduje.
-
-## Co patří do ON?
-- [x] z.trida_id = t.id
-- t.nazev = '1.A'
-- ORDER BY z.prijmeni
-- USE skola
-> ON je shoda klíčů. Filtr na název 1.A je WHERE, ne spojení.
-
-## Proč není název třídy uložený u každého žáka?
-- MySQL to neumí
-- [x] Aby se název měnil na jednom místě
-- Protože text v MySQL nejde uložit
-- Aby nešel JOIN
-> Jedna pravda v tabulce tridy. Žáci drží jen číslo.
-
-## Co JOIN udělá s uloženými tabulkami?
-- Sloučí je natrvalo do jedné
-- [x] Jen složí výsledek dotazu, tabulky zůstanou dvě
-- Smaže cizí klíče
-- Převede je na Excel
-> Výsledek je odpověď na příkaz. tridy a zaci na serveru zůstávají.
diff --git a/content/programove-vybaveni/3-rocnik/08-zadani-databaze.en.md b/content/programove-vybaveni/3-rocnik/08-zadani-databaze.en.md
deleted file mode 100644
index b69e293..0000000
--- a/content/programove-vybaveni/3-rocnik/08-zadani-databaze.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Assignment: a student register
-popis: Classes, students, a relationship and one query that shows 1.A by name.
-nazev_cviceni: Quiz: the register
-nadpis_kvizu: Walk the keys and the query
----
diff --git a/content/programove-vybaveni/3-rocnik/08-zadani-databaze.md b/content/programove-vybaveni/3-rocnik/08-zadani-databaze.md
deleted file mode 100644
index d3a812b..0000000
--- a/content/programove-vybaveni/3-rocnik/08-zadani-databaze.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-3-zadani-databaze
-nazev: Zadání: evidence žáků
-popis: Třídy, žáci, vztah a jeden dotaz, který ukáže 1.A jménem.
-nazev_cviceni: Kvíz: evidence
-nadpis_kvizu: Projděte klíče i dotaz
----
-
-# Výklad
-
-## Struktura
-*Dvě tabulky*
-
-- tridy: id jako klíč, nazev. Řádky 1.A a 1.B.
-- zaci: id jako klíč, jmeno, prijmeni, trida_id jako cizí klíč.
-- Anna Nováková je ve třídě 1.A. Ve třídě 1.B je aspoň jeden jiný žák.
-- Název 1.A není opsaný v tabulce žáků. Je jen v tridy.
-
-> Tip: Ať děláte Access, nebo MySQL, tahle dvě pole a vztah jsou povinné. Nástroj je obal.
-
-## Access
-*Když odevzdáváte accdb*
-
-- evidence.accdb má vztah 1:N, referenční integritu a vypnutou kaskádu.
-- Dotaz zaci_1A a formulář frmZaci. Sestava je seskupená podle třídy.
-- Žák s neexistující třídou nejde uložit. Vyzkoušeno na trida_id 99.
-- PDF sestavy leží vedle accdb.
-
-> Tip: Když učitel otevře jen accdb, formulář a dotaz v něm musí být. Samotné PDF bez souboru nestačí.
-
-## MySQL
-*Když odevzdáváte SQL*
-
-- dotazy.sql obsahuje SELECT se WHERE, INSERT, UPDATE i DELETE, oba měnící příkazy s WHERE.
-- JOIN vypíše příjmení a název 1.A, seřazené podle příjmení.
-- V souboru není heslo k serveru a není DELETE bez WHERE.
-- Příkazy jdou spustit znovu jen tam, kde to dává smysl. DELETE testovacího žáka v souboru má konkrétní id a komentář.
-
-> Tip: Odevzdává se text příkazů, ne snímek obrazovky klienta. Snímek smí být navíc.
-
-## Jedno odevzdání
-*Ne obě poloviční*
-
-- Stačí jeden nástroj, Access nebo MySQL, ale celý: struktura, data, dotaz na 1.A.
-- V dotazu je Anna vidět jako 1.A, ne jako holé číslo, pokud je součástí této části zadání JOIN nebo dotaz s názvem.
-- Záloha souboru má datum v názvu, třeba evidence-2026-06-12.accdb.
-- Do textu úkolu nepište heslo a nepište, že jste kaskádu zapnuli, pokud jste ji zapnout neměli.
-
-> Tip: Když umíte obojí, odevzdejte jedno pořádně. Dvě rozdělané databáze jsou horší než jedna hotová.
-
-# Kvíz
-
-## Kde leží text 1.A?
-- V každém řádku žáka jako jediná pravda
-- [x] V tabulce tridy, v poli nazev
-- V názvu souboru jako jediném místě
-- V hesle
-> Žák má jen trida_id. Název je v tabulce tříd jednou.
-
-## Jaký vztah mají třídy a žáci?
-- [x] 1:N, jedna třída a víc žáků
-- N:N bez třetí tabulky uprostřed školního cvičení
-- Žádný, stačí Excel bez klíče
-- 1:1, každý žák vlastní tabulku třídy
-> Jedna třída, mnoho žáků. Na straně žáků je cizí klíč.
-
-## Co musí mít UPDATE a DELETE v odevzdaném SQL?
-- Žádnou podmínku, ať je příkaz krátký
-- [x] WHERE, které trefí jen zamýšlené řádky
-- Hvězdičku místo sloupců
-- Kaskádu v příkazu SELECT
-> Bez WHERE se mění nebo maže celá tabulka. Ve cvičení se to neodevzdává.
-
-## Co Access odmítne, když je integrita zapnutá?
-- Žáka se třídou 1.A
-- [x] Žáka s trida_id 99, které v tridy není
-- Formulář
-- Sestavu podle příjmení
-> Cizí klíč musí ukazovat na existující třídu. 99 v tabulce tříd není.
diff --git a/content/programove-vybaveni/4-rocnik/01-wordpress.en.md b/content/programove-vybaveni/4-rocnik/01-wordpress.en.md
deleted file mode 100644
index d8adbc0..0000000
--- a/content/programove-vybaveni/4-rocnik/01-wordpress.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: WordPress: pages and posts
-popis: A school site, a static page and a dated news item. The system is not hand-written HTML for every sentence.
-nazev_cviceni: Quiz: WordPress
-nadpis_kvizu: Tell a page from a post
----
diff --git a/content/programove-vybaveni/4-rocnik/01-wordpress.md b/content/programove-vybaveni/4-rocnik/01-wordpress.md
deleted file mode 100644
index de527c6..0000000
--- a/content/programove-vybaveni/4-rocnik/01-wordpress.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-4-wordpress
-nazev: WordPress: stránky a příspěvky
-popis: Web školy, statická stránka a datovaná zpráva. Systém není ručně psané HTML každé věty.
-nazev_cviceni: Kvíz: WordPress
-nadpis_kvizu: Rozlište stránku a příspěvek
----
-
-# Výklad
-
-## Co to je
-*Správa obsahu*
-
-- WordPress je systém pro web. Text se píše v administraci, ne do každé stránky jako do samostatného souboru ve Wordu.
-- Školní web se jmenuje Školní den. Název se nastaví v Nastavení, Obecné.
-- Administrace je cesta /wp-admin/ na adrese webu. Bez účtu tam nejste.
-- Veřejná stránka a administrace jsou dva pohledy. Návštěvník administraci nevidí.
-
-> Tip: Když se po uložení na webu nic nezměnilo, často jste jen uložili koncept. Zveřejnit je samostatný krok.
-
-## Stránka a příspěvek
-*Dva druhy*
-
-- Stránka je trvalá: O škole, Kontakt. Nemá datum jako hlavní údaj.
-- Příspěvek je zpráva: co se stalo 12. 6. 2026. Řadí se od nejnovějšího.
-- Školní den má aspoň jednu stránku O škole a jeden příspěvek o dni otevřených dveří.
-- Příspěvek není stránka s jiným názvem. V menu se s nimi pracuje jinak.
-
-> Tip: V administraci jsou Příspěvky a Stránky dvě samostatné položky. Když novinku přidáte jako stránku, na výpisu zpráv nebude.
-
-## Adresa
-*Trvalý odkaz*
-
-- Trvalý odkaz v Nastavení, Trvalé odkazy, ať je název příspěvku, ne ?p=123.
-- Adresa pak nese slova, třeba /den-otevrenych-dveri/.
-- Jednou zveřejněnou adresu neměňte jen kvůli stylu. Odkaz, který už někdo má, by se rozbil.
-- Název webu a adresa příspěvku jsou dvě věci. Název je Školní den, adresa je konkrétní článek.
-
-> Tip: Když škola už odkazy rozeslala, změnu trvalých odkazů nechte na učiteli. Cvičný web si klidně nastavte hned na začátku.
-
-## Zadání: dvě věci na webu
-*Cvičení*
-
-- Na cvičném webu nastavte název Školní den.
-- Zveřejněte stránku O škole, aspoň dvě věty, bez data jako hlavní osy.
-- Zveřejněte příspěvek Den otevřených dveří s datem 12. 6. 2026.
-- Do souboru web.txt napište adresu stránky a adresu příspěvku. Heslo do souboru nepište.
-
-> Tip: Koncept se na veřejné stránce neukáže. U obou má být stav Publikováno.
-
-# Kvíz
-
-## Čím se liší stránka od příspěvku?
-- Ničím, je to stejné tlačítko
-- [x] Stránka je trvalá, příspěvek je datovaná zpráva
-- Příspěvek nejde zveřejnit
-- Stránka musí být v Excelu
-> O škole je stránka. Novinka s datem je příspěvek.
-
-## Kam se nastaví název webu Školní den?
-- Do názvu počítače
-- [x] Do Nastavení, Obecné
-- Do buňky A1
-- Do příkazu DELETE
-> Název webu je nastavení WordPressu, ne název souboru Windows.
-
-## Jaký trvalý odkaz ve cvičení chcete?
-- ?p=123 natrvalo
-- [x] Název příspěvku ve adrese
-- Jen číslo id žáka
-- Adresu z Accessu
-> Adresa se slovy se čte a pamatuje. Číselný parametr ne.
-
-## Co napsat do web.txt?
-- Heslo správce
-- [x] Adresy zveřejněné stránky a příspěvku
-- Celou databázi MySQL
-- Soubor accdb
-> Úkol dokládá, že stránky jsou venku. Heslo do textového souboru nepatří.
diff --git a/content/programove-vybaveni/4-rocnik/02-wordpress-role.en.md b/content/programove-vybaveni/4-rocnik/02-wordpress-role.en.md
deleted file mode 100644
index bee86a9..0000000
--- a/content/programove-vybaveni/4-rocnik/02-wordpress-role.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: WordPress: users and appearance
-popis: An author writes their own news, an editor also edits others. There is one administrator.
-nazev_cviceni: Quiz: roles
-nadpis_kvizu: Match the role
----
diff --git a/content/programove-vybaveni/4-rocnik/02-wordpress-role.md b/content/programove-vybaveni/4-rocnik/02-wordpress-role.md
deleted file mode 100644
index a4d5f49..0000000
--- a/content/programove-vybaveni/4-rocnik/02-wordpress-role.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-4-wordpress-role
-nazev: WordPress: uživatelé a vzhled
-popis: Autor píše své zprávy, šéfredaktor i cizí. Správce je jeden.
-nazev_cviceni: Kvíz: role
-nadpis_kvizu: Přiřaďte roli
----
-
-# Výklad
-
-## Role
-*Nejmíň práv*
-
-- Správce instaluje doplňky, mění šablonu a zakládá uživatele. Ve třídě je jeden, účet učitele.
-- Šéfredaktor zveřejňuje a upravuje i cizí příspěvky a stránky. Správce to být nemusí.
-- Autor zveřejňuje vlastní příspěvky. Cizí ne. Stránky O škole nezakládá.
-- Spolužák, který jen přidává novinky, dostane roli Autor. Ne správce.
-
-> Tip: Přihlášení každého člověka je vlastní účet. Heslo se nepůjčuje, ani když jde o školní cvičení.
-
-## Šablona a doplňky
-*Málo*
-
-- Šablona je vzhled. Ve cvičení stačí jedna, třeba výchozí, kterou učitel určí.
-- Doplněk přidává funkci. Každý doplněk je další věc, která se musí aktualizovat.
-- Doplněk z neznámého webu se neinstaluje. Stačí jeden překlep a web běží s cizím kódem.
-- Aktualizace WordPressu, šablony a doplňků se dělají v klidný den a po záloze, ne minutu před dnem otevřených dveří.
-
-> Tip: Neaktivní doplněk, který nepoužíváte, smažte. Vypnutý pořád leží na disku a pořád se může hodit útočníkovi, když v něm je chyba.
-
-## Menu
-*Co návštěvník vidí*
-
-- Menu má položku O škole na stránku, ne na koncept.
-- Příspěvky se často vypisují samy na úvodu. Do menu se nemusí přidávat každý.
-- Zkouškový příspěvek s textem test test do veřejného menu nepatří. Smažte ho, nebo ho nechte jako koncept.
-- Název v menu a nadpis stránky ať říkají totéž. O škole a O nas jsou dvě stránky, ne jedna.
-
-> Tip: Náhled v anonymním okně ukáže web bez vašeho přihlášení. To je pohled návštěvníka.
-
-## Zadání: účet autora
-*Cvičení*
-
-- Založte uživatele redakce s rolí Autor. Heslo mu nastavte mimo sešit, do web.txt ho nepište.
-- Ověřte, že tento účet neumí instalovat doplňky. Když umí, je to správce a roli změňte.
-- Šablonu neměňte na náhodnou z internetu. Nechte tu, kterou web už má, nebo tu od učitele.
-- Do web.txt dopište přihlašovací jméno autora a že role je Autor. Ne heslo.
-
-> Tip: Uživatelé, Všichni uživatelé, sloupec Role. Autor tam musí být napsaný, nestačí, že to tak bylo v úmyslu.
-
-# Kvíz
-
-## Jakou roli dostane spolužák, který jen píše novinky?
-- Správce
-- [x] Autor
-- Jediný účet s vaším heslem
-- Roli MySQL
-> Autor zveřejní vlastní příspěvek a nesahá na doplňky ani na cizí texty.
-
-## Kdo smí instalovat doplňky?
-- Každý návštěvník
-- [x] Správce
-- Autor
-- Kdokoli s odkazem na článek
-> Doplňky mění celý web. To je práce správce, ne autora zprávy.
-
-## Proč se doplněk nestahuje z náhodného webu?
-- WordPress doplňky neumí
-- [x] Je to cizí kód, který běží na školním webu
-- Protože pak nejde napsat stránku
-- Doplňky jsou vždycky v Excelu
-> Doplněk je program. Neznámý zdroj do školního webu nepatří.
-
-## Co do web.txt o účtu redakce nepatří?
-- Přihlašovací jméno
-- Role Autor
-- [x] Heslo
-- To, že účet nezvládne instalovat doplňky
-> Heslo do textového úkolu nepište. Jméno a role ano.
diff --git a/content/programove-vybaveni/4-rocnik/03-power-bi.en.md b/content/programove-vybaveni/4-rocnik/03-power-bi.en.md
deleted file mode 100644
index fb41b75..0000000
--- a/content/programove-vybaveni/4-rocnik/03-power-bi.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Power BI: data from Excel
-popis: One workbook, one table, a chart by class. The same data is not loaded twice.
-nazev_cviceni: Quiz: Power BI
-nadpis_kvizu: Pick the source and the visual
----
diff --git a/content/programove-vybaveni/4-rocnik/03-power-bi.md b/content/programove-vybaveni/4-rocnik/03-power-bi.md
deleted file mode 100644
index e3c9d21..0000000
--- a/content/programove-vybaveni/4-rocnik/03-power-bi.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-4-power-bi
-nazev: Power BI: data z Excelu
-popis: Jeden sešit, jedna tabulka, graf podle třídy. Stejná data se nenačítají dvakrát.
-nazev_cviceni: Kvíz: Power BI
-nadpis_kvizu: Vyberte zdroj a vizuál
----
-
-# Výklad
-
-## Soubor
-*pbix*
-
-- Power BI Desktop ukládá práci do pbix. Není to xlsx a není to obrázek grafu.
-- Na odevzdání vedle pbix patří PDF stránky sestavy, ať ji otevře i člověk bez Power BI.
-- Zdroj je prodej.xlsx, tabulka Prodej se sloupci Polozka, Trida, Castka.
-- Načtení je Načíst data, Excel, a vybere se tabulka Prodej, ne celý sešit naslepo dvakrát.
-
-> Tip: Když v modelu vidíte Prodej a Prodej (2), tabulka se načetla dvakrát. Jednu odeberte, než postavíte graf.
-
-## Model
-*Jedna řádka, jedna skutečnost*
-
-- Každý řádek je jeden prodej: položka, třída, částka. Součet v tomtéž sloupci navíc řádek souhrnu být nemá.
-- Řádek souhrnů z Excelu do Power BI nepatří. Součet si spočítá vizuál.
-- Trida je text 1.A a 1.B. Castka je číslo bez Kč v buňce.
-- Když Excel opravíte, v Power BI dejte Obnovit. Sám se o uložení xlsx nedozví.
-
-> Tip: Sloupec, který Power BI bere jako text, se nesčítá. Často je v něm mezera nebo Kč. V Excelu ho opravte a obnovte.
-
-## Vizuál
-*Sloupec a karta*
-
-- Sloupcový graf: osa Trida, hodnota součet Castka. Porovnání dvou tříd.
-- Karta ukáže jedno číslo, celkový součet Castka.
-- Koláč pro dvě třídy je zbytečný. Pro dvanáct měsíců byl špatně už v Excelu.
-- Průřez, anglicky slicer, na pole Trida nechte na příští lekci. Teď ať graf ukáže obě třídy.
-
-> Tip: Když graf ukáže počet řádků místo součtu korun, v hodnotách je Počet. Přepněte souhrn na Součet.
-
-## Zadání: první pbix
-*Cvičení*
-
-- prodej.pbix načte tabulku Prodej jednou.
-- Na stránce jsou sloupcový graf podle třídy a karta s celkovou částkou.
-- Obnovte data potom, co v Excelu změníte jednu částku. Karta se musí pohnout.
-- Uložte pbix do složky Skola\1-rocnik\excel a vedle něj prodej.pdf.
-
-> Tip: PDF je snímek sestavy. pbix je práce, ve které jde vizuál opravit. Odevzdávají se oba.
-
-# Kvíz
-
-## Jaký soubor je pracovní v Power BI?
-- xlsx jako jediný
-- [x] pbix
-- pdf jako jediný
-- accdb
-> pbix drží model a vizuály. Excel je zdroj. PDF je náhled na odevzdání.
-
-## Jak se mají načíst data prodeje?
-- [x] Tabulka Prodej jednou
-- Tentýž list dvakrát
-- Jako obrázek grafu z Excelu
-- Přepisem do Wordu
-> Jedna tabulka v modelu. Druhá kopie téhož zdroje mate součty.
-
-## Jaký vizuál porovná částku tříd 1.A a 1.B?
-- Koláč s dvanácti díly
-- [x] Sloupcový graf, osa Trida, součet Castka
-- Karta bez popisku a bez čísla
-- Průřez místo grafu jako jediný prvek
-> Dvě kategorie a jejich součet jsou sloupce. Karta je jen jedno číslo celkem.
-
-## Co uděláte v Power BI potom, co změníte číslo v Excelu?
-- Nic, pbix se mění sám v tu vteřinu
-- [x] Obnovit
-- Smazat pbix a začít ve Wordu
-- Převést Castka na text
-> Power BI si data při načtení vezme. Další změna Excelu se projeví až po obnovení.
diff --git a/content/programove-vybaveni/4-rocnik/04-power-bi-prurez.en.md b/content/programove-vybaveni/4-rocnik/04-power-bi-prurez.en.md
deleted file mode 100644
index 29027c6..0000000
--- a/content/programove-vybaveni/4-rocnik/04-power-bi-prurez.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Power BI: a slicer and a report
-popis: A class slicer filters the whole page. A filter on one visual leaves the others.
-nazev_cviceni: Quiz: the slicer
-nadpis_kvizu: Say what the slicer affects
----
diff --git a/content/programove-vybaveni/4-rocnik/04-power-bi-prurez.md b/content/programove-vybaveni/4-rocnik/04-power-bi-prurez.md
deleted file mode 100644
index c2e7f08..0000000
--- a/content/programove-vybaveni/4-rocnik/04-power-bi-prurez.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-4-power-bi-prurez
-nazev: Power BI: průřez a sestava
-popis: Průřez třídy přefiltruje celou stránku. Filtr jednoho vizuálu ostatní nechá.
-nazev_cviceni: Kvíz: průřez
-nadpis_kvizu: Řekněte, co průřez ovlivní
----
-
-# Výklad
-
-## Průřez
-*Celá stránka*
-
-- Průřez na pole Trida je ovládací prvek na plátně. Čtenář klikne na 1.A.
-- Sloupcový graf i karta se mají změnit. Celková částka pak není celá škola, ale jen 1.A.
-- Když se karta nehne, průřez na ni není napojený, nebo karta bere jinou tabulku.
-- Výběr se zruší gumou na průřezu, nebo volbou všech tříd. Data se tím nesmažou.
-
-> Tip: Průřez filtruje. Není to nový součet uložený do Excelu.
-
-## Filtr vizuálu
-*Jen jeden graf*
-
-- Filtr v podokně konkrétního vizuálu ovlivní jen ten vizuál.
-- Školní sestava pro porovnání tříd filtr vizuálu nepotřebuje. Má ukázat obě, dokud čtenář nezvolí průřez.
-- Filtr stránky schová třídu všem vizuálům na stránce, i bez klikání. Na odevzdání ho nenechávejte zapnutý omylem.
-- Než sestavu vyexportujete, zrušte výběr průřezu, pokud má PDF ukázat celou školu.
-
-> Tip: PDF zachytí průřez tak, jak je zrovna vybraný. Vybraná 1.A v PDF už druhou třídu neukáže.
-
-## Stránka
-*Jedna otázka*
-
-- Stránka se jmenuje Prodej. Název Stránka 1 se neodevzdává.
-- Nadpis na plátně říká Prodej po třídách. Graf má popisky tříd.
-- Na jedné stránce jsou nejvýš průřez, jeden graf a jedna karta. Druhý graf se stejným obsahem je navíc.
-- Barvy nechte klidné. Červená a zelená těsně vedle sebe se špatně čte.
-
-> Tip: Zobrazte sestavu na projektoru, nebo aspoň v celém okně. Drobný popisek, který na vašem monitoru ujde, vzadu ve třídě zanikne.
-
-## Zadání: stránka s průřezem
-*Cvičení*
-
-- Na stránku Prodej přidejte průřez Trida.
-- Klik na 1.A změní graf i kartu. Po zrušení výběru je zase vidět celý součet.
-- PDF vyexportujte bez vybrané třídy, ať ukazuje 1.A i 1.B.
-- pbix uložte znovu. V modelu je tabulka Prodej pořád jen jednou.
-
-> Tip: Když průřez nabízí prázdný řádek, v Excelu je prázdný řádek tabulky. Smažte ho ve zdroji a obnovte.
-
-# Kvíz
-
-## Co má průřez na Trida změnit?
-- Jen barvu pozadí
-- [x] Graf i kartu na stránce
-- Soubor xlsx na disku
-- Heslo k WordPressu
-> Průřez filtruje vizuály na stránce. Karta s celkovou částkou se musí změnit taky.
-
-## Čím se liší filtr jednoho vizuálu od průřezu?
-- Ničím
-- [x] Filtr vizuálu platí jen pro něj, průřez je ovládání pro stránku
-- Filtr vizuálu smaže Excel
-- Průřez nejde zrušit
-> Průřez je na plátně pro čtenáře. Filtr v podokně vizuálu ostatní grafy nechá.
-
-## Jaké PDF se odevzdává, když mají být vidět obě třídy?
-- S vybranou jen 1.A
-- [x] Bez výběru v průřezu
-- Prázdná stránka
-- Jen karta bez grafu a bez tříd
-> PDF drží aktuální výběr. Než ho uděláte, výběr zrušte.
-
-## Jak se má stránka jmenovat?
-- Stránka 1
-- [x] Prodej
-- pbix
-- DELETE
-> Název říká, co na stránce je. Výchozí Stránka 1 se neodevzdává.
diff --git a/content/programove-vybaveni/4-rocnik/05-batch.en.md b/content/programove-vybaveni/4-rocnik/05-batch.en.md
deleted file mode 100644
index bbfeb4d..0000000
--- a/content/programove-vybaveni/4-rocnik/05-batch.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Batch files
-popis: A bat file that creates the school folders and deletes nothing.
-nazev_cviceni: Quiz: batch
-nadpis_kvizu: Pick the safe command
----
diff --git a/content/programove-vybaveni/4-rocnik/05-batch.md b/content/programove-vybaveni/4-rocnik/05-batch.md
deleted file mode 100644
index b110c71..0000000
--- a/content/programove-vybaveni/4-rocnik/05-batch.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-4-batch
-nazev: Dávkové soubory
-popis: Soubor bat, který založí složky školy a nic nemaže.
-nazev_cviceni: Kvíz: batch
-nadpis_kvizu: Vyberte bezpečný příkaz
----
-
-# Výklad
-
-## Soubor bat
-*Příkazy za sebou*
-
-- Dávkový soubor je text s příponou bat. Příkazový řádek ho spustí řádek po řádku.
-- První řádek cvičení je @echo off, ať se příkazy nevypisují podruhé do okna.
-- rem je komentář. Do příkazu se nepočítá.
-- Soubor se jmenuje slozky.bat a leží v Dokumentech, ne na cizím flash disku, kterému nerozumíte.
-
-> Tip: Dvojklik spustí bat v té složce, kde soubor je. Neznámý bat z mailu se nespouští, stejně jako pdf.exe.
-
-## Složky
-*mkdir*
-
-- mkdir založí složku. Když už existuje, příkaz ohlásí chybu a pokračuje se dál, složku nesmaže.
-- set "ROOT=%USERPROFILE%\Documents\Skola" uloží cestu. %USERPROFILE% je složka účtu, u každého jiná.
-- mkdir "%ROOT%\1-rocnik\word" založí i mezistupně, když je napíšete takto v nových Windows. Když ne, založte nejdřív 1-rocnik a pak word.
-- Uvozovky kolem cesty jsou povinné v okamžiku, kdy v cestě je mezera. V Documents mezera není, v názvu účtu být může.
-
-> Tip: echo Hotovo vypíše větu. pause nechá okno otevřené, ať výsledek přečtete. Bez pause dvojklik okno hned zavře.
-
-## Co se nepouští
-*del*
-
-- del smaže soubor. Ve školním bat na založení složek žádný del není.
-- Příkaz, který maže podle masky celý disk, se nepíše ani z legrace. Obnova není Ctrl+Z.
-- cd /d přejde na jiný disk a složku. Než něco mažete, echo vypíše, kde jste. V tomto cvičení nemažete.
-- Bat, kterému nerozumíte řádek po řádku, nespouštějte. Krátký soubor se čte celý.
-
-> Tip: Když chcete zkusit del, zkuste ho na jeden soubor test.txt, který jste sami právě vytvořili. Ne na složku Windows.
-
-## Zadání: slozky.bat
-*Cvičení*
-
-- slozky.bat nastaví ROOT na %USERPROFILE%\Documents\Skola a založí 1-rocnik\word, 1-rocnik\excel a 1-rocnik\system.
-- Na konci je echo s cestou a pause. Není tam del.
-- Spusťte ho jednou. Složky v Průzkumníku opravdu jsou.
-- Spusťte ho podruhé. Smí ohlásit, že složka už existuje. Nemá nic smazat.
-
-> Tip: Úprava bat je v Poznámkovém bloku, ne ve Wordu. Word do něj doplní formátování a příkazový řádek pak řádek nepochopí.
-
-# Kvíz
-
-## Co má být první řádek školního bat?
-- del C:\
-- [x] @echo off
-- formát disku
-- heslo účtu
-> @echo off jen ztiší opisování příkazů. Nic nemaže.
-
-## Co dělá mkdir?
-- Smaže složku
-- [x] Založí složku
-- Otevře Word
-- Změní heslo
-> mkdir je make directory. Mazání je jiný příkaz a v tomto cvičení není.
-
-## Kam ukazuje %USERPROFILE%?
-- Na stejnou složku na všech účtech počítače
-- [x] Na profil přihlášeného účtu
-- Na web WordPressu
-- Na tabulku tridy
-> Každý účet má vlastní profil. Stejný bat tedy založí Skola v Dokumentech toho, kdo ho spustil.
-
-## Který bat ve cvičení nespouštíte?
-- Ten, který jen zakládá složky a na konci čeká
-- [x] Neznámý soubor z mailu
-- slozky.bat, který jste sami přečetli
-- Soubor, který ohlásí, že složka už existuje
-> Dávka je program. Z mailu se nespouští, stejně jako soubor s příponou exe.
diff --git a/content/programove-vybaveni/4-rocnik/06-uzivatele-a-skupiny.en.md b/content/programove-vybaveni/4-rocnik/06-uzivatele-a-skupiny.en.md
deleted file mode 100644
index 840ca87..0000000
--- a/content/programove-vybaveni/4-rocnik/06-uzivatele-a-skupiny.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Users and groups in Windows
-popis: The zaci account is in Users. It is not in Administrators.
-nazev_cviceni: Quiz: groups
-nadpis_kvizu: Put the account in the right group
----
diff --git a/content/programove-vybaveni/4-rocnik/06-uzivatele-a-skupiny.md b/content/programove-vybaveni/4-rocnik/06-uzivatele-a-skupiny.md
deleted file mode 100644
index 75a3c74..0000000
--- a/content/programove-vybaveni/4-rocnik/06-uzivatele-a-skupiny.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-4-uzivatele
-nazev: Uživatelé a skupiny ve Windows
-popis: Účet zaci je ve Users. Ve skupině Administrators není.
-nazev_cviceni: Kvíz: skupiny
-nadpis_kvizu: Zařaďte účet do skupiny
----
-
-# Výklad
-
-## Skupina
-*Práva najednou*
-
-- Skupina sdružuje účty, které mají mít stejná práva. Neměníte právo každému účtu zvlášť po jednom souboru.
-- Users jsou běžní uživatelé. Administrators smí měnit systém a instalovat.
-- Účet zaci patří do Users. Do Administrators ne.
-- Nový účet je často v Users sám. Práce navíc je nestrkat ho mezi správce, ne dopisovat Users.
-
-> Tip: Kdo je správce, vypíše net localgroup Administrators. Jméno zaci v tom seznamu být nemá.
-
-## Příkazy
-*Bez hesla v řádku*
-
-- net user zaci * /add účet založí a na heslo se zeptá. Hvězdička znamená dotaz, ne heslo zapsané do bat.
-- net user zaci vypíše účet. Je tam vidět, jestli je aktivní.
-- net localgroup Users zaci /add ho přidá do Users, kdyby tam nebyl.
-- net localgroup Administrators zaci /delete ho ze správců odebere, pokud tam omylem je. Příkaz chce spustit správce.
-
-> Tip: lusrmgr.msc otevře stejné účty v okně, ale jen ve Windows Pro, Education a Enterprise. V edici Home toto okno není a zůstává net user.
-
-## Kdo příkaz spouští
-*Správce*
-
-- Běžný uživatel účet nezaloží. Okno se musí spustit jako správce, nebo systém požádá o souhlas.
-- Souhlas správce není důvod založit účet rovnou jako správce. zaci zůstává běžný.
-- whoami na účtu zaci nesmí skončit na Administrators.
-- Cvičný účet se po hodině dá odebrat: net user zaci /delete. Nejdřív si ověřte, že nemaže účet učitele.
-
-> Tip: Jméno účtu zaci je cvičné. Účet, který se jmenuje jako učitel nebo správce, se nedeaktivuje a nemaže.
-
-## Zadání: zápis, ne cizí účty
-*Cvičení*
-
-- Do ucty.txt napište příkaz, kterým se zaci založí, včetně hvězdičky místo hesla.
-- Napište, v které skupině má být a ve které být nemá.
-- Pokud učitel dovolí, účet opravdu založte a net localgroup Administrators opište. zaci v seznamu není.
-- Heslo do ucty.txt nepište. Cizí účty na počítači neměňte.
-
-> Tip: Když lusrmgr.msc hlásí, že neexistuje, máte edici Home. Použijte net user a do zápisu napište, že grafické okno v této edici není.
-
-# Kvíz
-
-## Do které skupiny patří zaci?
-- Administrators
-- [x] Users
-- Do žádné, účet bez skupiny smí instalovat
-- Do skupiny hesel
-> Běžná práce je skupina Users. Správci jsou Administrators.
-
-## Jak se účet založí, aniž by heslo zůstalo v příkazu?
-- [x] net user zaci * /add
-- net user zaci heslo123 /add
-- mkdir zaci
-- DELETE FROM zaci
-> Hvězdička se zeptá. Heslo napsané v příkazu zůstane v historii.
-
-## Kde lusrmgr.msc není?
-- Ve Windows Pro
-- [x] Ve Windows Home
-- Ve Windows Education
-- Ve Windows Enterprise
-> Grafická správa účtů je v Pro, Education a Enterprise. Home ji nemá.
-
-## Co whoami nesmí ukázat při denní práci na účtu zaci?
-- Že jde o běžného uživatele
-- [x] Že účet je ve skupině Administrators
-- Název počítače
-- To, že účet existuje
-> Denní účet není správce. whoami to prozradí dřív, než něco nainstalujete.
diff --git a/content/programove-vybaveni/4-rocnik/07-zasady.en.md b/content/programove-vybaveni/4-rocnik/07-zasady.en.md
deleted file mode 100644
index a14b512..0000000
--- a/content/programove-vybaveni/4-rocnik/07-zasady.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Local policy
-popis: A minimum password of 8 characters and a lock after 300 seconds. A school domain overrides the local policy.
-nazev_cviceni: Quiz: policy
-nadpis_kvizu: Fill in the value and the command
----
diff --git a/content/programove-vybaveni/4-rocnik/07-zasady.md b/content/programove-vybaveni/4-rocnik/07-zasady.md
deleted file mode 100644
index f850445..0000000
--- a/content/programove-vybaveni/4-rocnik/07-zasady.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-4-zasady
-nazev: Místní zásady
-popis: Minimální heslo 8 znaků a zámek po 300 sekundách. Doména školy místní zásadu přebije.
-nazev_cviceni: Kvíz: zásady
-nadpis_kvizu: Doplňte hodnotu a příkaz
----
-
-# Výklad
-
-## Kde se zásady berou
-*gpedit*
-
-- Místní zásady otevře gpedit.msc. Je ve Windows Pro, Education a Enterprise, v edici Home není.
-- Konfigurace počítače platí pro stroj. Konfigurace uživatele platí pro účty, které se přihlásí.
-- Školní cvičení mění dvě věci: délku hesla a zámek při nečinnosti. Ostatní zásady nechte být.
-- Když je počítač v doméně školy, doménová zásada vyhraje nad místní. Místní změna se pak jakoby ztratí a není to chyba klikání.
-
-> Tip: Než něco změníte, napište si původní hodnotu. Vrátit Neznámé číslo zpět nejde, když jste si ho nezapsali.
-
-## Heslo
-*Osm znaků*
-
-- Cesta vede přes Konfigurace počítače, Nastavení systému Windows, Nastavení zabezpečení, Zásady účtů, Zásady hesel.
-- Minimální délka hesla se nastaví na 8.
-- Nula znamená, že heslo smí být prázdné. To školní účet nechce.
-- Zásada neopraví heslo, které už je krátké, dokud ho uživatel příště nemění. Platí pro nová hesla.
-
-> Tip: Heslo v zásadě nenastavujete. Nastavujete pravidlo. Samotné heslo účtu zaci se zadává při net user, ne tady.
-
-## Zámek
-*300 sekund*
-
-- Nečinnost: Konfigurace počítače, Nastavení systému Windows, Nastavení zabezpečení, Místní zásady, Možnosti zabezpečení.
-- Položka Interaktivní přihlášení: limit nečinnosti počítače se nastaví na 300 sekund, tedy 5 minut.
-- Nula nebo nenastaveno znamená, že tato zásada zámek nevynucuje.
-- Po pěti minutách bez myši a klávesnice se má počítač zamknout, ne vypnout a ne smazat Dokumenty.
-
-> Tip: Zámek je Win+L hned. Zásada totéž udělá sama, když odejdete od stroje a zapomenete.
-
-## Zadání: dvě hodnoty
-*Cvičení*
-
-- Pokud gpedit.msc máte, nastavte minimální délku hesla na 8 a limit nečinnosti na 300 sekund. Původní hodnoty si zapište.
-- gpupdate /force zásady načte hned, bez čekání na restart. Příkaz chce okno spuštěné jako správce.
-- Do zasady.txt napište obě hodnoty a jestli je počítač v doméně. Heslo nepište.
-- V edici Home napište, že gpedit.msc v ní není, a hodnoty jen popište. Nevymýšlejte, že jste je nastavili.
-
-> Tip: Když po gpupdate hodnota v gpedit zůstane a systém se nechová podle ní, počítač je v doméně a doména místní číslo přebíjí.
-
-# Kvíz
-
-## Jaká je školní minimální délka hesla?
-- 0
-- [x] 8
-- 300
-- Prázdné heslo
-> Osm znaků je pravidlo z první hodiny o účtech. Nula by prázdné heslo dovolila.
-
-## Na kolik sekund se nastaví limit nečinnosti?
-- 5
-- [x] 300
-- 8
-- Na nulu, ať se počítač nezamyká
-> 300 sekund je pět minut. Číslo 5 by bylo pět sekund, to v této položce není.
-
-## Čím se místní zásady načtou hned?
-- Smazáním účtu zaci
-- [x] gpupdate /force
-- mkdir
-- SELECT * FROM zaci
-> gpupdate zásady znovu načte. Bez něj si počítač může počkat na své vlastní obnovení.
-
-## Co platí, když je počítač v doméně školy a místní heslo nastavíte jinak?
-- Vždy vyhraje místní gpedit
-- [x] Vyhraje doménová zásada
-- Hesla se vypnou
-- WordPress přestane běžet
-> Doména je nad místním počítačem. Místní změna, která jí odporuje, se neuplatní.
diff --git a/content/programove-vybaveni/4-rocnik/08-maturitni-zadani.en.md b/content/programove-vybaveni/4-rocnik/08-maturitni-zadani.en.md
deleted file mode 100644
index fdca7fe..0000000
--- a/content/programove-vybaveni/4-rocnik/08-maturitni-zadani.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Exam assignment
-popis: Folders, a letter, grades, a register, a site, a report and an account with a password rule.
-nazev_cviceni: Quiz: the whole assignment
-nadpis_kvizu: Walk the hand-in
----
diff --git a/content/programove-vybaveni/4-rocnik/08-maturitni-zadani.md b/content/programove-vybaveni/4-rocnik/08-maturitni-zadani.md
deleted file mode 100644
index ef404a3..0000000
--- a/content/programove-vybaveni/4-rocnik/08-maturitni-zadani.md
+++ /dev/null
@@ -1,79 +0,0 @@
----
-id: sw-4-maturita
-nazev: Maturitní zadání
-popis: Složky, dopis, známky, evidence, web, sestava a účet s zásadou hesla.
-nazev_cviceni: Kvíz: celé zadání
-nadpis_kvizu: Projdi odevzdání
----
-
-# Výklad
-
-## Systém
-*Složky a účet*
-
-- slozky.bat založí Skola\1-rocnik\word, excel a system v Dokumentech toho účtu, který ho spustí.
-- V bat není del a není v něm heslo. Je v něm @echo off, mkdir a pause.
-- Účet zaci je běžný uživatel ve skupině Users, není v Administrators.
-- Minimální délka hesla je 8 znaků. Zámek po nečinnosti je 300 sekund, pokud edice Windows zásady má.
-
-> Tip: Do textového souboru patří příkazy a hodnoty. Skutečné heslo ne.
-
-## Office
-*Dopis a známky*
-
-- zadost.docx je jedna strana, datum 12. 6. 2026, řádek Věc, k tomu PDF.
-- znamky.xlsx počítá stav vzorcem KDYŽ. Známka 5 je neprospěl, 1 až 4 prospěl.
-- Pod známkami je průměr, ne součet.
-- prodej.xlsx a prodej.pbix: tabulka načtená jednou, průřez třídy, PDF bez výběru, ať jsou vidět obě třídy.
-
-> Tip: pbix bez xlsx zdroje učitel neobnoví. Xlsx vedle pbix nechte.
-
-## Data a web
-*Evidence a Školní den*
-
-- Access nebo MySQL: tabulky tridy a zaci, vztah 1:N, Anna Nováková ve třídě 1.A.
-- Dotaz ukáže název 1.A, ne jen číslo. UPDATE a DELETE, pokud jsou v SQL, mají WHERE.
-- WordPress: název Školní den, stránka O škole, příspěvek s datem, účet autora. Správce je jeden.
-- V textech o webu a účtech není heslo.
-
-> Tip: Stačí jeden databázový nástroj, ale celý. Access s integritou, nebo SQL s JOINem.
-
-## Složka odevzdání
-*Pořádek*
-
-- Všechno leží pod Skola, ne na ploše a ne v Stažených.
-- Přípony jsou vidět. Názvy nemají final2.
-- Na obhajobu otevřete nejdřív bat a složky, pak Excel a Power BI, pak databázi a nakonec web.
-- U každé části umíte říct, kdo smí heslo, kdo smí instalovat a proč pětka není prospěl.
-
-> Tip: Když se zeptají, proč zaci není správce, odpověď je: denní účet nemá právo měnit systém.
-
-# Kvíz
-
-## Co slozky.bat nesmí obsahovat?
-- mkdir
-- @echo off
-- [x] del přes celý disk nebo cizí složky
-- pause
-> Cvičení složky zakládá. Mazání do něj nepatří a neznámé mazání už vůbec ne.
-
-## Jaký je stav známky 5?
-- prospěl
-- [x] neprospěl
-- průměr
-- 1.A
-> Prochází 1 až 4. Pětka je neprospěl. Rozhoduje vzorec KDYŽ, ne rukou psaný text.
-
-## Jakou roli má účet, který ve WordPressu jen píše novinky?
-- Správce
-- [x] Autor
-- Jediný sdílený účet učitele
-- Administrators ve Windows
-> Autor zveřejní vlastní příspěvek. Doplňky a uživatele spravuje jeden správce.
-
-## Jaké jsou dvě hodnoty zásad?
-- Heslo 0 znaků a zámek 8 sekund
-- [x] Heslo aspoň 8 znaků a zámek po 300 sekundách
-- Heslo v souboru bat a zámek vypnutý
-- 300 znaků hesla a 8 sekund
-> Osm znaků je délka hesla. 300 sekund je pět minut nečinnosti.
diff --git a/content/technicka-grafika/1-rocnik/01-format-a-meritko.en.md b/content/technicka-grafika/1-rocnik/01-format-a-meritko.en.md
deleted file mode 100644
index 2de20d7..0000000
--- a/content/technicka-grafika/1-rocnik/01-format-a-meritko.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Sheet size, scale and the title block
-popis: A sizes, drawing scale and the title block.
-nazev_cviceni: Quiz: sheet and scale
-nadpis_kvizu: Check the sheet, the scale and the title block
----
diff --git a/content/technicka-grafika/1-rocnik/01-format-a-meritko.md b/content/technicka-grafika/1-rocnik/01-format-a-meritko.md
deleted file mode 100644
index 2ef74ba..0000000
--- a/content/technicka-grafika/1-rocnik/01-format-a-meritko.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-1-format-a-meritko
-nazev: Formát, měřítko a razítko
-popis: Řada A, měřítko výkresu a popisové pole.
-nazev_cviceni: Kvíz: formát a měřítko
-nadpis_kvizu: Ověřte si formát, měřítko a razítko
----
-
-# Výklad
-
-## Řada formátů A
-*Papír*
-
-- Základní formát A4 má 210 × 297 mm. Delší strana je na výšku.
-- A3 vznikne složením dvou A4: 297 × 420 mm.
-- A2 je 420 × 594 mm. Každý větší formát má dvojnásobnou plochu.
-- Na škole se součást kreslí nejčastěji na A4 nebo A3.
-
-> Tip: kratší strana většího formátu je delší stranou menšího. A3 je na šířku, A4 na výšku.
-
-## Co znamená měřítko
-*Měřítko*
-
-- Měřítko 1:1 znamená, že výkres je ve skutečné velikosti.
-- Měřítko 1:2 zmenšuje kresbu na polovinu. Měřítko 2:1 ji zvětšuje na dvojnásobek.
-- Kóta se vždy píše ve skutečném rozměru, ne v rozměru nakreslené čáry.
-- Do razítka se měřítko zapíše tak, jak se kreslilo, třeba 1:2.
-
-> Tip: díra o průměru 20 mm má na výkresu 1:2 čáru dlouhou 10 mm, ale kóta říká 20.
-
-## Popisové pole
-*Razítko*
-
-- Razítko je v pravém dolním rohu. Čte se zprava, když je výkres ve správné poloze.
-- Patří do něj název, měřítko, materiál, datum a jméno.
-- Číslo výkresu a název součásti se nesmí vyměnit za ozdobný text.
-- Rámeček je od okraje papíru odsazený, na školním A4 obvykle o 5 až 10 mm.
-
-> Tip: než začnete kreslit součást, vyhraďte si hladinu pro rámeček a razítko.
-
-## Zadání: rámeček A4
-*Cvičení*
-
-- Nakreslete obdélník 210 × 297 a dovnitř rámeček odsazený o 10 mm od každého okraje.
-- Vpravo dole vyhraďte razítko 180 × 40 mm, které lícuje s vnitřním rámečkem.
-- Do razítka vepište název Destička, měřítko 1:1 a své jméno.
-- Soubor uložte jako A4-ramecek.dwg, jednotky milimetry.
-
-> Tip: vnitřní rámeček má 190 × 277 mm. Levý dolní roh dejte do bodu 0,0.
-
-
-# Kvíz
-
-## Jaké rozměry má formát A4?
-- 297 × 420 mm
-- [x] 210 × 297 mm
-- 420 × 594 mm
-- 148 × 210 mm
-> A3 je 297 × 420 mm. A4 je polovina této plochy, 210 × 297 mm.
-
-## Co se stane s kótou na výkresu v měřítku 1:2?
-- Píše se polovina skutečného rozměru
-- [x] Píše se skutečný rozměr
-- Kóty se vynechají
-- Měřítko se do razítka nepíše
-> Kresba je poloviční, číslo na kótě zůstává skutečné. Měřítko 1:2 je v razítku.
-
-## Kam se na výkresu umísťuje razítko?
-- Doprostřed plochy
-- [x] Do pravého dolního rohu
-- Nad součást, vlevo
-- Mimo rámeček
-> Razítko lícuje s vnitřním rámečkem vpravo dole, aby šlo výkres složit a razítko zůstalo navrchu.
-
-## Jak je velký vnitřní rámeček A4 při odsazení 10 mm?
-- 210 × 297 mm
-- [x] 190 × 277 mm
-- 200 × 287 mm
-- 180 × 40 mm
-> Od 210 i od 297 se odečte 10 mm z každé strany, tedy 20 mm. 190 × 277 mm.
diff --git a/content/technicka-grafika/1-rocnik/02-druhy-car.en.md b/content/technicka-grafika/1-rocnik/02-druhy-car.en.md
deleted file mode 100644
index afc6ade..0000000
--- a/content/technicka-grafika/1-rocnik/02-druhy-car.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Line types and line weights
-popis: Outlines, hidden edges, centrelines and dimensions.
-nazev_cviceni: Quiz: lines
-nadpis_kvizu: Name the line
----
diff --git a/content/technicka-grafika/1-rocnik/02-druhy-car.md b/content/technicka-grafika/1-rocnik/02-druhy-car.md
deleted file mode 100644
index 542e176..0000000
--- a/content/technicka-grafika/1-rocnik/02-druhy-car.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-1-druhy-car
-nazev: Druhy a tloušťky čar
-popis: Obrys, skryté hrany, osy a kóty podle normy.
-nazev_cviceni: Kvíz: čáry
-nadpis_kvizu: Poznejte druh čáry
----
-
-# Výklad
-
-## Tlustá plná čára
-*Obrys*
-
-- Viditelné hrany a obrysy se kreslí souvislou tlustou čarou.
-- V AutoCADu jí patří vlastní hladina, třeba OBRYS, s tloušťkou 0,5 mm.
-- Tloušťka se nastavuje na hladině, ne u každé čáry zvlášť.
-- Obrys nesmí být stejně tenký jako kóta, jinak se výkres nedá číst.
-
-> Tip: hladinu OBRYS nastavte jako aktuální dřív, než začnete kreslit tvar.
-
-## Tenké čáry
-*Kóty a hrany*
-
-- Kóty, odkazy, šrafy a pomocné čáry jsou souvislé a tenké, kolem 0,25 mm.
-- Skryté hrany, které z daného pohledu nejsou vidět, jsou čárkované a tenké.
-- Osa díry nebo válce je čerchovaná: dlouhá čárka, tečka, dlouhá čárka.
-- Osa přesahuje obrys jen o kousek, nepřetahuje se přes celý výkres.
-
-> Tip: čárkovaná čára je skrytá hrana. Čerchovaná je osa. Nejsou totéž.
-
-## Jak to nastavit v AutoCADu
-*Hladiny*
-
-- Příkaz `_LAYER` otevře správce hladin. Založte OBRYS, SKRYTE, OSA a KOTA.
-- U SKRYTE zvolte typ čáry DASHED, u OSA typ CENTER.
-- Typ čáry se načítá příkazem `_LINETYPE`. Bez načtení ho hladina nenabídne.
-- Měřítko typu čáry `_LTSCALE` zvětší nebo zmenší mezery v čárkování, ať je čára čitelná.
-
-> Tip: když čárkování vypadá jako plná čára, je LTSCALE moc velké nebo moc malé.
-
-## Zadání: tři čáry jedné díry
-*Cvičení*
-
-- Nakreslete obdélník 60 × 40 tlustou plnou čarou.
-- Svislou osu veďte středem a nechte ji přesáhnout obrys o 3 mm na každou stranu.
-- Jednu zadní hranu, která z pohledu není vidět, nakreslete tenkou čárkovanou čarou.
-- Každý druh čáry musí ležet na své hladině.
-
-> Tip: osa dlouhá 46 mm při výšce tělesa 40 mm přesahuje o 3 mm nahoře i dole.
-
-
-# Kvíz
-
-## Jakou čarou se kreslí viditelný obrys?
-- Tenkou čárkovanou
-- [x] Tlustou plnou
-- Tenkou čerchovanou
-- Tlustou čerchovanou
-> Viditelné hrany jsou souvislé a tlustší než kóty, osy i skryté hrany.
-
-## Co znamená tenká čárkovaná čára?
-- Osu díry
-- [x] Skrytou hranu
-- Obrys
-- Šrafu
-> Čárkovaná čára je hrana, kterou v daném pohledu nevidíme. Osa je čerchovaná.
-
-## Jakou čarou se kreslí osa válce?
-- Tlustou plnou
-- Tenkou čárkovanou
-- [x] Tenkou čerchovanou
-- Vlnovkou
-> Osa je dlouhá čárka, tečka, dlouhá čára. V AutoCADu je to typ CENTER.
-
-## Proč má mít obrys vlastní hladinu?
-- Aby šel smazat jen razítko
-- [x] Aby měla tloušťka a typ čáry jedno místo
-- Hladiny v AutoCADu nejsou
-- Obrys se kreslí jen od ruky
-> Tloušťka i typ se nastaví na hladině. Všechny obrysy je pak mají stejné.
diff --git a/content/technicka-grafika/1-rocnik/03-prostredi-autocadu.en.md b/content/technicka-grafika/1-rocnik/03-prostredi-autocadu.en.md
deleted file mode 100644
index 2d39a07..0000000
--- a/content/technicka-grafika/1-rocnik/03-prostredi-autocadu.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: The AutoCAD workspace
-popis: Model space, the command line, units and limits.
-nazev_cviceni: Quiz: the workspace
-nadpis_kvizu: Check the first drawing setup
----
diff --git a/content/technicka-grafika/1-rocnik/03-prostredi-autocadu.md b/content/technicka-grafika/1-rocnik/03-prostredi-autocadu.md
deleted file mode 100644
index 7a16b82..0000000
--- a/content/technicka-grafika/1-rocnik/03-prostredi-autocadu.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-1-prostredi
-nazev: Prostředí AutoCADu
-popis: Model, příkazový řádek, jednotky a meze.
-nazev_cviceni: Kvíz: prostředí
-nadpis_kvizu: Ověřte si první nastavení výkresu
----
-
-# Výklad
-
-## Kam se kreslí
-*Model*
-
-- Součást se kreslí v modelu. Papír, rámeček a výřezy patří až do rozvržení.
-- Příkazový řádek je dole. Čte se pořád: AutoCAD se ptá na další bod.
-- Stisk Enter nebo mezerník zopakuje poslední příkaz.
-- Esc příkaz zruší. Neukládejte rozpracovaný příkaz klávesou Esc uprostřed bodu.
-
-> Tip: když AutoCAD „nic nedělá“, podívejte se, na co se příkazový řádek ptá.
-
-## Příkaz, který funguje vždy
-*Podtržítko*
-
-- Český AutoCAD má přeložené příkazy. Úsečka se jmenuje ÚSEČKA.
-- Mezinárodní jméno se píše s podtržítkem: `_LINE`. Funguje v každé jazykové verzi.
-- Na pásu karet je stejný příkaz pod ikonou. Ikona i text dělají totéž.
-- V těchto lekcích je u každého kroku mezinárodní příkaz. Naučte se ho psát.
-
-> Tip: `_LINE` a ÚSEČKA spustí stejné kreslení. Podtržítko se hodí, když čeština na klávesnici zlobí.
-
-## Jednotky a meze
-*Milimetry*
-
-- `_UNITS` nastavte na desetinné milimetry. Úhly nechte ve stupních.
-- Přesnost 0 stačí na celé milimetry, 0,0 když budete potřebovat desetinu.
-- `_LIMITS` určí levý dolní a pravý horní roh kreslicí plochy, třeba 0,0 a 420,297.
-- `_ZOOM` a volba All ukáže celé meze. Kolečko myši přibližuje ke kurzoru.
-
-> Tip: nový výkres začněte jednotkami. Přepočet z palců na milimetry uprostřed práce nic nespraví.
-
-## Zadání: prázdný výkres A3
-*Cvičení*
-
-- Založte soubor v milimetrech a nastavte meze 0,0 až 420,297.
-- Příkazem `_RECTANG` nakreslete obrys papíru z bodu 0,0 do bodu 420,297.
-- Přibližte vše příkazem `_ZOOM` a volbou Extents.
-- Uložte soubor jako A3-prazdny.dwg.
-
-> Tip: Extents přiblíží to, co je nakreslené. All přiblíží meze, i když jsou prázdné.
-
-
-# Kvíz
-
-## Kde se kreslí tvar součásti?
-- V rozvržení, přes razítko
-- [x] V modelu
-- Jen na pásu karet
-- V příkazu PLOT
-> Model je prostor součásti. Rozvržení je papír, na který se model později vloží výřezem.
-
-## Co udělá příkaz `_LINE` v českém AutoCADu?
-- Nastaví jednotky
-- [x] Spustí kreslení úsečky
-- Smaže poslední čáru
-- Otevře hladiny
-> Podtržítko volá mezinárodní příkaz. `_LINE` je úsečka, ať je AutoCAD v jakémkoli jazyce.
-
-## Jakou jednotku má školní výkres?
-- Palec
-- [x] Milimetr
-- Metr
-- Pixel
-> `_UNITS` nastavte na desetinné milimetry dřív, než zadáte první rozměr.
-
-## Co ukáže `_ZOOM` s volbou Extents?
-- Celé nastavené meze, i prázdné
-- [x] Všechno, co je nakreslené
-- Jen razítko
-- Poslední příkaz
-> Extents orámuje nakreslené objekty. All ukáže meze výkresu.
diff --git a/content/technicka-grafika/1-rocnik/04-souradnice.en.md b/content/technicka-grafika/1-rocnik/04-souradnice.en.md
deleted file mode 100644
index 95648e1..0000000
--- a/content/technicka-grafika/1-rocnik/04-souradnice.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Coordinates
-popis: Absolute points, relative moves and polar entry.
-nazev_cviceni: Quiz: coordinates
-nadpis_kvizu: Find the next point
----
diff --git a/content/technicka-grafika/1-rocnik/04-souradnice.md b/content/technicka-grafika/1-rocnik/04-souradnice.md
deleted file mode 100644
index 626ba0c..0000000
--- a/content/technicka-grafika/1-rocnik/04-souradnice.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-1-souradnice
-nazev: Souřadnice
-popis: Absolutní bod, relativní posun a polární zápis.
-nazev_cviceni: Kvíz: souřadnice
-nadpis_kvizu: Dopočítejte další bod
----
-
-# Výklad
-
-## Absolutní souřadnice
-*Od počátku*
-
-- Bod 40,25 leží 40 mm vpravo a 25 mm nahoru od počátku 0,0.
-- Čárka odděluje X a Y. Desetinná čárka v čísle se v AutoCADu píše tečkou: 10.5
-- Osa X míří vpravo, osa Y nahoru. Tak je natočený model při startu.
-- Absolutní bod nezávisí na tom, kde jste skončili minule.
-
-> Tip: když se výkres „rozjede“ mimo papír, někdo zadal absolutní bod místo posunu.
-
-## Relativní souřadnice
-*Zavináč*
-
-- `@30,0` znamená 30 mm vpravo od posledního bodu, výška se nemění.
-- `@0,40` je 40 mm nahoru. `@-20,0` je 20 mm doleva.
-- Zavináč se píše před čísla. Bez něj AutoCAD čte absolutní bod.
-- Při kreslení `_LINE` se tak skládá tvar bod za bodem.
-
-> Tip: obdélník 70 × 40 z bodu 10,10: `@70,0` pak `@0,40` pak `@-70,0` a zavřít volbou Close.
-
-## Polární souřadnice
-*Délka a úhel*
-
-- `@50<90` je úsečka dlouhá 50 mm směrem 90 stupňů, tedy kolmo nahoru.
-- Úhel se měří od kladné osy X proti směru hodinových ručiček.
-- `@40<0` míří vpravo, `@40<180` vlevo, `@40<270` dolů.
-- Špičatá závorka se píše bez mezery. Mezera by příkaz ukončila.
-
-> Tip: úhel 45 stupňů je šikmo vpravo nahoru. Délka je vždy skutečná, ne průmět.
-
-## Zadání: obdélník ze souřadnic
-*Cvičení*
-
-- Spusťte `_LINE` v bodě 20,20.
-- Pokračujte `@80,0`, potom `@0,50`, potom `@-80,0`.
-- Útvar zavřete volbou Close, ne ručním bodem.
-- Změřte `_DIST` mezi levým dolním a pravým horním rohem. Má vyjít 94,3 mm, tedy přepona 80 a 50.
-
-> Tip: Close spojí poslední bod s prvním. Ruční bod 20,20 nemusí trefit počátek a obrys zůstane otevřený.
-
-
-# Kvíz
-
-## Co znamená zápis `@0,40`?
-- Bod 0,40 od počátku výkresu
-- [x] 40 mm nahoru od posledního bodu
-- 40 mm vpravo od počátku
-- Úhel 40 stupňů
-> Zavináč čte posun od posledního bodu. Nula v X a 40 v Y je čistě nahoru.
-
-## Kam míří `@60<180`?
-- 60 mm nahoru
-- [x] 60 mm doleva
-- 180 mm vpravo
-- Do počátku
-> Úhel 180 stupňů je záporná osa X, tedy doleva. 60 je délka úsečky.
-
-## Jak AutoCAD pozná desetinné číslo?
-- Čárkou: 10,5,0
-- [x] Tečkou: 10.5
-- Zlomkem 21/2
-- Mezerou 10 5
-> Čárka odděluje X a Y. Desetina se píše tečkou, třeba 10.5,20.
-
-## Proč útvar zavřít volbou Close?
-- Aby se smazala první úsečka
-- [x] Aby se poslední bod trefil do prvního
-- Close nastaví měřítko
-- Close je totéž co Esc
-> Ručně opsaný první bod často mine o zlomek milimetru a obrys nejde vyšrafovat.
diff --git a/content/technicka-grafika/1-rocnik/05-zakladni-entity.en.md b/content/technicka-grafika/1-rocnik/05-zakladni-entity.en.md
deleted file mode 100644
index 2ca03d0..0000000
--- a/content/technicka-grafika/1-rocnik/05-zakladni-entity.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Lines, circles, arcs and rectangles
-popis: The four commands every part starts with.
-nazev_cviceni: Quiz: basic objects
-nadpis_kvizu: Pick the right command
----
diff --git a/content/technicka-grafika/1-rocnik/05-zakladni-entity.md b/content/technicka-grafika/1-rocnik/05-zakladni-entity.md
deleted file mode 100644
index e41ea6b..0000000
--- a/content/technicka-grafika/1-rocnik/05-zakladni-entity.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-1-zakladni-entity
-nazev: Úsečka, kružnice, oblouk a obdélník
-popis: Čtyři příkazy, kterými začíná každá součást.
-nazev_cviceni: Kvíz: základní entity
-nadpis_kvizu: Vyberte správný příkaz
----
-
-# Výklad
-
-## Úsečka a obdélník
-*Hrany*
-
-- `_LINE` chce první a další bod. Končí se Enterem, zavření obrysu volbou Close.
-- `_RECTANG` chce dva protilehlé rohy. Z bodu 0,0 a `@90,40` vznikne obdélník 90 × 40.
-- Obdélník je jedna uzavřená křivka. Úsečky jsou samostatné objekty.
-- Uzavřená křivka se později líp šrafuje a vysouvá do 3D.
-
-> Tip: když budete tvar vysouvat, kreslete ho jako `_RECTANG` nebo uzavřenou `_PLINE`, ne jako čtyři volné úsečky.
-
-## Kružnice
-*Díry*
-
-- `_CIRCLE` se nejčastěji zadá středem a poloměrem.
-- Volba D přepne na průměr. U díry Ø12 je průměr 12 a poloměr 6.
-- Střed díry se kótuje, ne její okraj. Okraj je důsledek průměru.
-- Víc stejných děr se nekreslí znovu: první se zkopíruje nebo rozloží do pole.
-
-> Tip: v zadání Ø znamená průměr. Do příkazu s poloměrem dáte polovinu.
-
-## Oblouk
-*Zaoblení z příkazu*
-
-- `_ARC` umí oblouk třemi body nebo středem, počátkem a úhlem.
-- Zaoblení rohu ale nedělejte obloukem od ruky. Na to je později `_FILLET`.
-- Oblouk má směr. Špatné pořadí bodů ho vyklenu na druhou stranu.
-- U drážky se oblouk často nahradí `_PLINE` s volbou Arc uprostřed úseček.
-
-> Tip: tři body oblouku zadávejte v pořadí, v jakém oblouk plyne. Prostřední bod je na oblouku, ne střed kružnice.
-
-## Zadání: destička s dírou
-*Cvičení*
-
-- `_RECTANG` z 0,0 do `@90,50`.
-- Střed destičky je 45,25. Tam `_CIRCLE`, volba D, průměr 16.
-- Osu svislou i vodorovnou nechte přesáhnout obrys o 3 mm. Patří na hladinu OSA.
-- Uložte jako desticka-90x50.dwg.
-
-> Tip: polovina z 90 je 45 a polovina z 50 je 25. Střed neměřte od oka.
-
-
-# Kvíz
-
-## Čím nakreslíte obdélník 90 × 40 jedním objektem?
-- Čtyřmi úsečkami `_LINE`
-- [x] `_RECTANG` a druhým rohem `@90,40`
-- `_CIRCLE` s poloměrem 90
-- `_ARC` o úhlu 90
-> `_RECTANG` chce dva rohy. Relativní `@90,40` je šířka a výška od prvního rohu.
-
-## Zadání říká díra Ø12. Jaký poloměr má kružnice?
-- 12
-- [x] 6
-- 24
-- 3
-> Průměr je 12, poloměr je polovina. V `_CIRCLE` buď poloměr 6, nebo volba D a průměr 12.
-
-## Kde leží střed destičky 90 × 50, která začíná v 0,0?
-- 90,50
-- [x] 45,25
-- 0,0
-- 25,45
-> Střed je v polovině šířky a polovině výšky.
-
-## Kdy dát přednost uzavřené křivce před čtyřmi úsečkami?
-- Nikdy, úsečka je přesnější
-- [x] Když se obrys bude šrafovat nebo vysouvat
-- Jen u razítka
-- Když je tvar otevřený
-> Šrafa i vysunutí potřebují uzavřenou oblast. Čtyři úsečky, které se netrefí, oblast neutvoří.
diff --git a/content/technicka-grafika/1-rocnik/06-uchopeni-a-orto.en.md b/content/technicka-grafika/1-rocnik/06-uchopeni-a-orto.en.md
deleted file mode 100644
index 08596b5..0000000
--- a/content/technicka-grafika/1-rocnik/06-uchopeni-a-orto.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Snaps, ortho and polar tracking
-popis: How to hit an endpoint and draw square.
-nazev_cviceni: Quiz: precise drawing
-nadpis_kvizu: Pick the snap and the toggle
----
diff --git a/content/technicka-grafika/1-rocnik/06-uchopeni-a-orto.md b/content/technicka-grafika/1-rocnik/06-uchopeni-a-orto.md
deleted file mode 100644
index c9f7f0e..0000000
--- a/content/technicka-grafika/1-rocnik/06-uchopeni-a-orto.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-1-uchopeni
-nazev: Úchop, orto a polární trasování
-popis: Jak trefit konec čáry a kreslit kolmo.
-nazev_cviceni: Kvíz: přesné kreslení
-nadpis_kvizu: Vyberte úchop a přepínač
----
-
-# Výklad
-
-## Úchopy
-*Koncový bod*
-
-- Úchop přitáhne kurzor ke geometrii. Bez něj minuta minete konec čáry.
-- Zapíná se klávesou F3 nebo v stavovém řádku. Nastavení je `_OSNAP`.
-- Na začátek stačí koncový bod, střed a střed kružnice.
-- Kolmice a tečna se hodí, až budete napojovat čáru na kružnici.
-
-> Tip: úchop Střed u úsečky je její polovina. Střed u kružnice je její osa. Jsou to dva různé úchopy.
-
-## Orto a polární úhel
-*F8 a F10*
-
-- Orto, klávesa F8, pustí kurzor jen vodorovně a svisle.
-- Polární trasování F10 drží i šikmé úhly, obvykle po 45 nebo po 15 stupních.
-- Orto a volné šikmé kreslení nejdou dohromady. Na šikmou čáru orto vypněte.
-- Délku při zapnutém ortu napište jen číslem a Enter. Směr určuje myš.
-
-> Tip: vodorovná čára dlouhá 40 mm je F8, kurzor doprava, napsat 40 a Enter. Nemusíte počítat druhý bod.
-
-## Proč se výkres rozpadne
-*Minutí*
-
-- Dvě čáry, které se „téměř“ dotýkají, nejdou oříznout ani vyšrafovat.
-- Mezera vznikne, když kliknete vedle koncového bodu a úchop je vypnutý.
-- `_ZOOM` na roh ukáže mezeru, která na celém výkresu není vidět.
-- Opravený roh se napojí úchopem, ne posunem od oka.
-
-> Tip: než odevzdáte výkres, přibližte každý roh. Mezera menší než milimetr na tisku vynikne.
-
-## Zadání: kříž přesně ve středu
-*Cvičení*
-
-- Nakreslete obdélník 80 × 60 z bodu 0,0.
-- Svislou osu veďte z úchopu Střed dolní hrany do úchopu Střed horní hrany.
-- Vodorovnou osu veďte ze Středu levé hrany do Středu pravé.
-- Osy se mají protnout v bodě 40,30. Ověřte to `_ID`.
-
-> Tip: `_ID` vypíše souřadnice úchopu. Když průsečík není 40,30, jedna osa minula střed hrany.
-
-
-# Kvíz
-
-## Která klávesa zapíná úchopy?
-- F8
-- [x] F3
-- F10
-- Esc
-> F3 jsou úchopy, F8 je orto, F10 je polární trasování.
-
-## Co dělá orto?
-- Chytá konce čar
-- [x] Drží kreslení ve vodorovném a svislém směru
-- Nastaví milimetry
-- Maže poslední úsečku
-> Se zapnutým ortem kurzor neuhne do šikma. Délku pak stačí napsat číslem.
-
-## Proč dvě čáry, které se na obrazovce dotýkají, nejdou vyšrafovat?
-- Šrafa vyžaduje kóty
-- [x] Mezi nimi je mezera, konec minul úchop
-- Hladina OSA to zakazuje
-- Orto je zapnuté
-> Od oka minutý konec nechá škvíru. Přiblížení a úchop Koncový bod ji ukážou.
-
-## Který úchop najde polovinu úsečky?
-- Střed kružnice
-- [x] Střed
-- Koncový bod
-- Kolmice
-> Úchop Střed na úsečce je její polovina. Na kružnici je to jiný úchop, střed kružnice.
diff --git a/content/technicka-grafika/1-rocnik/07-posun-kopie-otoceni.en.md b/content/technicka-grafika/1-rocnik/07-posun-kopie-otoceni.en.md
deleted file mode 100644
index 0409703..0000000
--- a/content/technicka-grafika/1-rocnik/07-posun-kopie-otoceni.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Move, copy, rotate and mirror
-popis: Change a finished shape instead of drawing it again.
-nazev_cviceni: Quiz: editing
-nadpis_kvizu: Match the command to the task
----
diff --git a/content/technicka-grafika/1-rocnik/07-posun-kopie-otoceni.md b/content/technicka-grafika/1-rocnik/07-posun-kopie-otoceni.md
deleted file mode 100644
index 611ca1a..0000000
--- a/content/technicka-grafika/1-rocnik/07-posun-kopie-otoceni.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-1-posun-kopie
-nazev: Posun, kopie, otočení a zrcadlení
-popis: Úprava hotového tvaru místo kreslení znovu.
-nazev_cviceni: Kvíz: úpravy
-nadpis_kvizu: Vyberte příkaz podle zadání
----
-
-# Výklad
-
-## Posun a kopie
-*Z místa na místo*
-
-- `_MOVE` objekt odnese. Na původním místě nic nezůstane.
-- `_COPY` nechá vzor a přidá další. Ptá se na základní bod a pak na nové body.
-- Základní bod vyberte úchopem, třeba roh nebo střed. Od oka se kopie rozjede.
-- Víc kopií za sebou vznikne opakovaným zadáním dalšího bodu, bez nového příkazu.
-
-> Tip: posun o 30 mm doprava je základní bod kdekoli, druhý bod `@30,0`.
-
-## Otočení
-*Kolem bodu*
-
-- `_ROTATE` chce základní bod a úhel. Kladný úhel je proti směru hodin.
-- 90 otočí doleva, -90 nebo 270 doprava.
-- Volba Reference vezme současný úhel hrany a řekne, jaký má být nový.
-- Otáčí se kolem vybraného bodu, ne kolem středu obrazovky.
-
-> Tip: díru v rohu otočíte kolem středu destičky, ne kolem středu díry, jinak odjede z rozteče.
-
-## Zrcadlení
-*Souměrnost*
-
-- `_MIRROR` chce dva body osy zrcadlení. Osa nemusí být nakreslená předem.
-- Svislá osa jsou dva body se stejným X. Vodorovná dva body se stejným Y.
-- Na otázku, zda smazat zdroj, dejte Ne, když má zůstat obě poloviny.
-- Text se při zrcadlení může otočit naruby. Proměnná MIRRTEXT s hodnotou 0 nechá text čitelný.
-
-> Tip: souměrnou součást nakreslete z poloviny a zrcadlete. Druhou polovinu neobkreslujte.
-
-## Zadání: druhá díra zrcadlem
-*Cvičení*
-
-- Destička 100 × 40, levý dolní roh v 0,0.
-- Díru Ø10 dejte středem do bodu 20,20.
-- Zrcadlete ji přes svislou osu destičky. Osa prochází body 50,0 a 50,40.
-- Zdroj nemažte. Druhá díra má střed v 80,20.
-
-> Tip: 100 minus 20 je 80. Zrcadlo kolem x = 50 převede 20 na 80 a výšku 20 nechá.
-
-
-# Kvíz
-
-## Čím odnesete objekt, aby na původním místě nezůstal?
-- `_COPY`
-- [x] `_MOVE`
-- `_MIRROR`
-- `_ROTATE`
-> Kopie zdroj nechá. Posun ho přestěhuje.
-
-## Jaký úhel otočí tvar o 90 stupňů ve směru hodin?
-- 90
-- [x] -90
-- 180
-- 45
-> Kladný úhel jde proti hodinám. Po směru hodin je záporný, tedy -90.
-
-## Přes co se zrcadlí díra se středem 20,20 na destičce široké 100?
-- Přes bod 0,0
-- [x] Přes svislou osu x = 50
-- Přes vodorovnou osu y = 20
-- Přes hranu x = 100
-> Osa souměrnosti destičky je uprostřed šířky. 20 a 80 jsou od ní stejně daleko.
-
-## Co má být základní bod posunu?
-- Libovolné místo vedle objektu, od oka
-- [x] Úchop, třeba roh nebo střed
-- Vždy počátek 0,0
-- Střed obrazovky
-> Úchop podrží rozměr. Kliknutí vedle rohu posune tvar o neznámý kousek navíc.
diff --git a/content/technicka-grafika/1-rocnik/08-zadani-desticka.en.md b/content/technicka-grafika/1-rocnik/08-zadani-desticka.en.md
deleted file mode 100644
index 1920b3e..0000000
--- a/content/technicka-grafika/1-rocnik/08-zadani-desticka.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Assignment: a plate
-popis: The whole first-year drawing on one part.
-nazev_cviceni: Quiz: the plate
-nadpis_kvizu: Check the finished drawing
----
diff --git a/content/technicka-grafika/1-rocnik/08-zadani-desticka.md b/content/technicka-grafika/1-rocnik/08-zadani-desticka.md
deleted file mode 100644
index 08464d1..0000000
--- a/content/technicka-grafika/1-rocnik/08-zadani-desticka.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-1-zadani-desticka
-nazev: Zadání: destička
-popis: Celý výkres z prvního ročníku na jedné součásti.
-nazev_cviceni: Kvíz: destička
-nadpis_kvizu: Zkontrolujte hotový výkres
----
-
-# Výklad
-
-## Co se odevzdává
-*Zadání*
-
-- Destička 120 × 70 mm, tloušťka se nekreslí, jde o jeden pohled.
-- Čtyři díry Ø8. Středy jsou 15 mm od levého i pravého kraje a 15 mm od dolního i horního kraje.
-- Uprostřed je podélná drážka 40 × 16 se zaoblenými konci. Zatím stačí obdélník, zaoblení přijde ve druháku.
-- Výkres je na A4, měřítko 1:1, rámeček a razítko z první lekce.
-
-> Tip: středy děr jsou body 15,15, pak 105,15, pak 15,55 a 105,55. 120 minus 15 je 105, 70 minus 15 je 55.
-
-## Hladiny a čáry
-*Pořádek*
-
-- OBRYS je tlustá plná čára. OSA je tenká čerchovaná. KOTA zatím nechte prázdnou.
-- Každá díra má svislou i vodorovnou osu, která přesahuje obrys o 3 mm.
-- Destička má jednu vodorovnou a jednu svislou osu souměrnosti přes celý tvar.
-- Skryté hrany tu nejsou. Pohled je zepředu a díry jsou průchozí, jejich okraj je vidět.
-
-> Tip: průchozí díra v tomto pohledu není čárkovaná. Čárkovaná by byla hrana schovaná za materiálem.
-
-## Postup
-*Po řadě*
-
-- Nejdřív rámeček a razítko, potom obrys `_RECTANG` 120 × 70.
-- Jednu díru `_CIRCLE` o průměru 8 a další tři `_COPY` nebo dvě `_MIRROR`.
-- Drážku `_RECTANG` 40 × 16 se středem ve středu destičky. Její levý dolní roh je 40,27.
-- Osy dokreslete úchopem Střed. `_DIST` mezi protilehlými dírami ve vodorovném směru má být 90.
-
-> Tip: střed destičky je 60,35. Drážka sahá 20 mm doleva a doprava a 8 mm nahoru a dolů.
-
-## Kontrola před odevzdáním
-*Co musí sedět*
-
-- V razítku je název Destička, měřítko 1:1 a materiál, třeba ocel.
-- Žádná čára obrysu neleží na hladině OSA a žádná osa není plná.
-- Přiblížené rohy nemají mezeru. Kružnice se dotýká svých os ve středu.
-- Soubor se jmenuje desticka-120x70.dwg a je v milimetrech.
-
-> Tip: vypněte hladinu OBRYS. Zůstat mají jen osy a rámeček. Zapněte ji zpět a totéž udělejte s OSA.
-
-
-# Kvíz
-
-## Kde leží střed pravé horní díry?
-- 15,15
-- [x] 105,55
-- 120,70
-- 60,35
-> Od pravého kraje 15 mm je x = 105. Od horního kraje 15 mm je y = 55.
-
-## Jaká je vodorovná rozteč levé a pravé díry?
-- 120
-- 15
-- [x] 90
-- 105
-> 105 minus 15 je 90. Tolik má ukázat `_DIST` mezi středy.
-
-## Kam patří obrys destičky?
-- Na hladinu OSA
-- [x] Na hladinu OBRYS, tlustou plnou čarou
-- Čárkovaně, protože díry jsou průchozí
-- Do razítka
-> Viditelný obrys je tlustá plná čára. Osa je samostatná hladina.
-
-## Proč se průchozí díra v tomto pohledu nekreslí čárkovaně?
-- Čárkovaná čára je zakázaná
-- [x] Její okraj je z pohledu vidět
-- Díra se kreslí jen osou
-- Měřítko je 1:2
-> Čárkovaně se kreslí hrana, kterou materiál zakrývá. Okraj díry v čelním pohledu vidíme.
diff --git a/content/technicka-grafika/2-rocnik/01-orez-a-ekvidistanta.en.md b/content/technicka-grafika/2-rocnik/01-orez-a-ekvidistanta.en.md
deleted file mode 100644
index 0b5fb23..0000000
--- a/content/technicka-grafika/2-rocnik/01-orez-a-ekvidistanta.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Trim, extend and offset
-popis: Fix lines that overshoot, and keep a constant gap.
-nazev_cviceni: Quiz: trim and offset
-nadpis_kvizu: Pick the command and the distance
----
diff --git a/content/technicka-grafika/2-rocnik/01-orez-a-ekvidistanta.md b/content/technicka-grafika/2-rocnik/01-orez-a-ekvidistanta.md
deleted file mode 100644
index d132ae3..0000000
--- a/content/technicka-grafika/2-rocnik/01-orez-a-ekvidistanta.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-2-orez-ekvidistanta
-nazev: Ořez, prodloužení a ekvidistanta
-popis: Úprava čar, které přesahují nebo mají stálý odstup.
-nazev_cviceni: Kvíz: ořez a odstup
-nadpis_kvizu: Vyberte příkaz a vzdálenost
----
-
-# Výklad
-
-## Ořez a prodloužení
-*Na hranu*
-
-- `_TRIM` uřízne kus objektu po hraně, kterou vyberete jako řeznou.
-- V novějším AutoCADu stačí Enter bez výběru hran a řeže se o všechno, co čára protíná.
-- `_EXTEND` dělá opak: natáhne čáru až k vybrané hraně.
-- Maže se ta část, na kterou kliknete. Klik na špatnou stranu uřízne to, co mělo zůstat.
-
-> Tip: když ořez „nebere“, čáry se ve skutečnosti neprotínají. Přibližte roh a napojte ho úchopem.
-
-## Ekvidistanta
-*Stálý odstup*
-
-- `_OFFSET` vytvoří rovnoběžnou křivku v zadané vzdálenosti.
-- Hodí se na tloušťku stěny, rámeček i osazení. Zadáte vzdálenost a stranu.
-- U kružnice vznikne soustředná kružnice. Průměr se změní o dvojnásobek odstupu.
-- Ekvidistanta obdélníku udělá menší nebo větší obdélník se stejnými rohy, ne se zaoblením.
-
-> Tip: stěna tlustá 8 mm je ekvidistanta obrysu o 8 mm dovnitř. U kružnice Ø40 tak vznikne Ø24.
-
-## Co neřeže
-*Otevřený roh*
-
-- Ořez potřebuje průsečík. Dvě čáry, které se jen míjejí, se neoříznou.
-- Nejdřív je prodlužte nebo je nakreslete znovu s úchopem, teprve pak řežte.
-- Ekvidistanta otevřené křivky zůstane otevřená. Rohy se samy nedovřou.
-- Po ořezu zkontrolujte, že nezůstaly pahýly kratší než milimetr. Ty `_ERASE` smaže.
-
-> Tip: pahýl po ořezu je pořád objekt. Na tisku udělá tečku, která do výkresu nepatří.
-
-## Zadání: rámeček stěny
-*Cvičení*
-
-- Vnější obdélník 100 × 60. Ekvidistantou o 8 mm dovnitř udělejte vnitřní obrys.
-- Vnitřní obdélník má tedy 84 × 44.
-- Čáry, které po dalších úpravách přesáhnou vnější roh, ořízněte.
-- Oba obrysy nechte na hladině OBRYS. Kóty zatím nepřidávejte.
-
-> Tip: 100 minus dvakrát 8 je 84. 60 minus 16 je 44. Když vnitřek vyjde jinak, odstup šel ven, ne dovnitř.
-
-
-# Kvíz
-
-## Který příkaz uřízne přesahující kus čáry?
-- `_OFFSET`
-- [x] `_TRIM`
-- `_EXTEND`
-- `_MOVE`
-> Ořez bere kus za řeznou hranou. Prodloužení čáru naopak natahuje.
-
-## Ekvidistanta kružnice Ø40 o 8 mm dovnitř má průměr?
-- 32
-- [x] 24
-- 48
-- 8
-> Poloměr klesne o 8, průměr o 16. 40 minus 16 je 24.
-
-## Proč ořez na dvou čarách nic neudělá?
-- Orto je zapnuté
-- [x] Čáry se neprotínají
-- Chybí razítko
-- Objekt je kružnice
-> Bez průsečíku není kde řezat. Nejdřív čáry napojte, potom řežte.
-
-## Vnitřní obrys obdélníku 100 × 60 po ekvidistantě 8 mm dovnitř je?
-- 100 × 60
-- 92 × 52
-- [x] 84 × 44
-- 108 × 68
-> Z šířky i výšky se ubere 8 mm z každé strany, tedy 16 mm. 84 × 44.
diff --git a/content/technicka-grafika/2-rocnik/02-zaobleni-a-zkoseni.en.md b/content/technicka-grafika/2-rocnik/02-zaobleni-a-zkoseni.en.md
deleted file mode 100644
index 30afc38..0000000
--- a/content/technicka-grafika/2-rocnik/02-zaobleni-a-zkoseni.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Fillets and chamfers
-popis: A corner by a radius or by two distances.
-nazev_cviceni: Quiz: corners
-nadpis_kvizu: Work out the corner
----
diff --git a/content/technicka-grafika/2-rocnik/02-zaobleni-a-zkoseni.md b/content/technicka-grafika/2-rocnik/02-zaobleni-a-zkoseni.md
deleted file mode 100644
index 17e40f1..0000000
--- a/content/technicka-grafika/2-rocnik/02-zaobleni-a-zkoseni.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-2-zaobleni-zkoseni
-nazev: Zaoblení a zkosení
-popis: Roh poloměrem nebo dvěma vzdálenostmi.
-nazev_cviceni: Kvíz: rohy
-nadpis_kvizu: Dopočítejte roh
----
-
-# Výklad
-
-## Zaoblení
-*Poloměr*
-
-- `_FILLET` spojí dvě úsečky obloukem. Volba R nastaví poloměr.
-- Poloměr 8 znamená oblouk o poloměru 8 mm, ne průměr.
-- R0 ostrý roh jen napojí, oblouk nevznikne. Hodí se na nedotažené čáry.
-- Příkaz si poloměr pamatuje. Než zaoblíte jiný roh, R zkontrolujte.
-
-> Tip: u drážky se zaobleným koncem je poloměr polovina šířky drážky. Drážka 16 mm má R8.
-
-## Zkosení
-*Dvě vzdálenosti*
-
-- `_CHAMFER` srazí roh úsečkou. Volba D zadá dvě vzdálenosti od vrcholu.
-- Stejné vzdálenosti 2 a 2 srazí roh pod 45 stupňů, pokud jsou hrany kolmé.
-- Různé vzdálenosti, třeba 2 a 4, dají šikminu, která není 45 stupňů.
-- Zkosení se kótuje délkou a úhlem, nebo oběma délkami. Ne poloměrem.
-
-> Tip: na výkresu je zkosení 2 × 45°. V příkazu jsou to vzdálenosti 2 a 2, ne úhel napsaný místo D.
-
-## Kdy který roh
-*Dílec*
-
-- Zaoblení bývá tam, kde se obrábí frézou nebo kde ostrá hrana vadí.
-- Zkosení bývá na hraně díry a na konci hřídele, aby díl navedl protikus.
-- Na jednom rohu nedávejte zaoblení i zkosení. Zadání říká jen jedno.
-- Po zaoblení se změní délka rovné hrany. Kóta rovné části už není celá šířka.
-
-> Tip: hrana dlouhá 40 mm se dvěma zaobleními R5 na koncích má rovný úsek 30 mm.
-
-## Zadání: drážka a sražení
-*Cvičení*
-
-- Obdélník 80 × 30. Na obou kratších koncích `_FILLET` s R8, na obou dlouhých hranách.
-- Vznikne oválná drážka: rovný střed je dlouhý 64 mm, šířka zůstane 30? Pozor, R8 na výšce 30 nejde, šířka drážky musí být 16.
-- Překreslete tvar jako drážku 64 × 16 a konce zaoblete R8.
-- Jeden vnější roh destičky 80 × 50 mimo drážku srazte `_CHAMFER` 2 a 2.
-
-> Tip: poloměr zaoblení konce drážky se rovná polovině její šířky. 16 / 2 = 8.
-
-
-# Kvíz
-
-## Co nastaví `_FILLET` volbou R8?
-- Zkosení 8 × 8
-- [x] Oblouk o poloměru 8 mm
-- Průměr 8
-- Osazení o 8 mm
-> R je poloměr oblouku. Průměr oblouku by byl 16.
-
-## Jak v příkazu zadáte zkosení 2 × 45° na kolmém rohu?
-- R2
-- [x] Vzdálenosti 2 a 2
-- Úhel 45 místo obou vzdáleností, bez délek
-- Ekvidistantu 2
-> Stejné vzdálenosti na kolmých hranách dají 45 stupňů. Do příkazu patří délky.
-
-## Jaký poloměr má konec drážky široké 16 mm?
-- 16
-- [x] 8
-- 4
-- 32
-> Konec je půlkruh. Jeho poloměr je polovina šířky.
-
-## Co udělá zaoblení R0?
-- Smaže roh
-- [x] Napojí čáry ostrým rohem
-- Udělá kružnici
-- Nastaví měřítko
-> Nulový poloměr je způsob, jak dvě minuté čáry spojit do ostrého rohu.
diff --git a/content/technicka-grafika/2-rocnik/03-hladiny.en.md b/content/technicka-grafika/2-rocnik/03-hladiny.en.md
deleted file mode 100644
index ecdd7b2..0000000
--- a/content/technicka-grafika/2-rocnik/03-hladiny.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Layers
-popis: Colour, linetype and weight in one place.
-nazev_cviceni: Quiz: layers
-nadpis_kvizu: Decide which layer an object belongs on
----
diff --git a/content/technicka-grafika/2-rocnik/03-hladiny.md b/content/technicka-grafika/2-rocnik/03-hladiny.md
deleted file mode 100644
index 6a93333..0000000
--- a/content/technicka-grafika/2-rocnik/03-hladiny.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-2-hladiny
-nazev: Hladiny
-popis: Barva, typ čáry a tloušťka na jednom místě.
-nazev_cviceni: Kvíz: hladiny
-nadpis_kvizu: Rozhodněte, co patří na kterou hladinu
----
-
-# Výklad
-
-## Proč hladiny
-*Pořádek*
-
-- Hladina nese barvu, typ čáry i tloušťku. Objekt, který na ní vznikne, je zdědí.
-- Obrys, osa, skrytá hrana, šrafa, kóta a rámeček jsou samostatné hladiny.
-- Vypnutá hladina na obrazovce zmizí, ale tisknout se pořád může. Zmrazená se netiskne a nepřepočítává.
-- Zamčená hladina je vidět, ale nejde omylem posunout.
-
-> Tip: kóty nikdy nekreslete na OBRYS. Až budete obrys měnit, kóty by odjely s ním.
-
-## Aktuální hladina
-*Než kliknete*
-
-- Nový objekt padne na aktuální hladinu. Je to ta, která je ve správci vybraná jako současná.
-- `_LAYER` založí hladinu. Poklepáním ji nastavíte jako aktuální.
-- Objekt na špatné hladině přesuňte ve vlastnostech, příkaz `_PROPERTIES` nebo horní lišta.
-- Barvu a typ čáry nechte ByLayer. Barva nastavená na objektu se se hladinou přestane řídit.
-
-> Tip: ByLayer znamená „ber to, co říká hladina“. Je to výchozí a na školním výkresu má tak zůstat.
-
-## Názvy, které obstojí
-*Úmluva*
-
-- OBRYS, OSA, SKRYTE, SRAFA, KOTA, RAM, TEXT. Krátce a bez mezer.
-- OBRYS je bílá nebo černá, tloušťka 0,50. Ostatní tenké, 0,25, každá svou barvou.
-- SKRYTE má typ DASHED, OSA typ CENTER. Ostatní Continuous.
-- Hladinu 0 nemažte a nekreslete na ni součást. Je systémová.
-
-> Tip: hladina 0 je schovaná v blocích. Součást na ní se později špatně barví.
-
-## Zadání: šest hladin
-*Cvičení*
-
-- V souboru destičky založte OBRYS, OSA, SKRYTE, SRAFA, KOTA a RAM.
-- Obrys přesuňte na OBRYS, osy na OSA, rámeček na RAM.
-- OSA musí mít typ CENTER a po `_LTSCALE` 0,5 nebo 1 musí být čerchování vidět.
-- Vypněte OBRYS. Na obrazovce smí zůstat osy a rámeček, nic z tvaru.
-
-> Tip: když osa vypadá plně, typ čáry není načtený, nebo je LTSCALE mimo rozumné měřítko.
-
-
-# Kvíz
-
-## Kam patří kóta?
-- Na hladinu OBRYS
-- [x] Na vlastní hladinu KOTA
-- Na hladinu 0
-- Kamkoli, hladina je jen barva pozadí
-> Kóta má vlastní tenkou čáru a při úpravě obrysu nesmí odjet s ním.
-
-## Co znamená ByLayer?
-- Objekt barvu hladiny ignoruje
-- [x] Objekt si bere barvu a typ čáry z hladiny
-- Hladina je zamčená
-- Objekt se netiskne
-> ByLayer je správné nastavení. Barva „natvrdo“ na objektu se hladinou už neřídí.
-
-## Čím se liší vypnutá a zmrazená hladina?
-- Ničím
-- [x] Zmrazená se navíc netiskne a výkres ji nepočítá
-- Vypnutá se nedá znovu zapnout
-- Zmrazená je vidět červeně
-> Vypnutí je jen obrazovka. Zmrazení hladinu vyřadí i z tisku a z přepočtu.
-
-## Proč nekreslit součást na hladinu 0?
-- Hladina 0 nejde použít
-- [x] Je systémová a v blocích se chová jinak
-- Nepouští tlusté čáry
-- Smí na ni jen razítko
-> Hladina 0 má zvláštní roli uvnitř bloků. Součást patří na pojmenované hladiny.
diff --git a/content/technicka-grafika/2-rocnik/04-srafy.en.md b/content/technicka-grafika/2-rocnik/04-srafy.en.md
deleted file mode 100644
index 7619115..0000000
--- a/content/technicka-grafika/2-rocnik/04-srafy.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Hatches
-popis: Sectioned material, hatch scale and a closed boundary.
-nazev_cviceni: Quiz: hatches
-nadpis_kvizu: Decide what gets hatched
----
diff --git a/content/technicka-grafika/2-rocnik/04-srafy.md b/content/technicka-grafika/2-rocnik/04-srafy.md
deleted file mode 100644
index 300909f..0000000
--- a/content/technicka-grafika/2-rocnik/04-srafy.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-2-srafy
-nazev: Šrafy
-popis: Řez materiálem, měřítko šrafy a uzavřená oblast.
-nazev_cviceni: Kvíz: šrafy
-nadpis_kvizu: Rozhodněte, co se šrafuje
----
-
-# Výklad
-
-## Co šrafa říká
-*Materiál*
-
-- Šrafa v řezu znamená materiál, který řezná rovina proťala.
-- Ocel se na školním výkresu šrafuje tenkými čarami pod 45 stupňů.
-- Dva sousední díly mají šrafu na opačnou stranu, nebo jinou rozteč.
-- Díra, drážka a prázdno se nešrafují. Není tam materiál.
-
-> Tip: šrafa pod 45 stupni nesmí být rovnoběžná s obrysem. Když je obrys šikmý, úhel šrafy změňte.
-
-## Příkaz
-*Uzavřená oblast*
-
-- `_HATCH` a vzor ANSI31. Klikněte dovnitř uzavřené oblasti.
-- Oblast musí být dovřená. Mezera v rohu šrafu zastaví nebo ji vylije ven.
-- Měřítko vzoru zvětší rozteč čar. Moc hustá šrafa na tisku zčerná.
-- Šrafa patří na hladinu SRAFA, tenkou čarou. Ne na OBRYS.
-
-> Tip: asociativní šrafa se přizpůsobí, když obrys později natáhnete. Na školní práci ji nechte zapnutou.
-
-## Co se v podélném řezu nešrafuje
-*Výjimky*
-
-- Hřídel, žebro, šroub a pero v podélném řezu zůstávají bez šrafy.
-- V příčném řezu se tatáž hřídel šrafuje. Rozhoduje směr řezu, ne součást.
-- Tenká stěna řezaná podélně se šrafuje. Žebro ne, aby nepůsobilo jako plný kus.
-- Obrys šrafované plochy je pořád tlustá čára. Šrafa ho nenahrazuje.
-
-> Tip: podélný řez hřídele ukáže osazení obrysem. Šrafa by z hřídele udělala zdánlivě plný hranol.
-
-## Zadání: řez destičkou
-*Cvičení*
-
-- Destičku 80 × 20 v řezu A-A vyšrafujte ANSI31, měřítko tak, aby rozteč byla zhruba 2 až 3 mm.
-- Díru Ø10 uprostřed nešrafujte. Šrafa končí na její hraně.
-- Šrafu dejte na hladinu SRAFA. Vypnutím této hladiny musí zmizet jen šrafa, obrys zůstane.
-- Když šrafa vyteče ven, najděte mezeru v obrysu a roh napojte.
-
-> Tip: jedna oblast vlevo od díry a jedna vpravo. Obě mohou být jedna šrafa, pokud příkaz umí víc oblastí.
-
-
-# Kvíz
-
-## Co znamená šrafa v řezu?
-- Skrytou hranu
-- [x] Materiál proťatý řeznou rovinou
-- Závit
-- Měřítko 1:2
-> Šrafuje se jen to, co řez skutečně prochází. Díra je prázdná.
-
-## Jaký vzor se na škole používá pro ocel?
-- Plná výplň
-- [x] ANSI31, tenké čáry pod 45 stupňů
-- Tečkování
-- Čerchovaná osa
-> ANSI31 je běžný strojařský vzor. Úhel změňte, jen když by splynul s obrysem.
-
-## Proč šrafa vyteče mimo součást?
-- Je zapnuté orto
-- [x] Obrys není uzavřený
-- Chybí kóta
-- Hladina je ByLayer
-> I malá mezera v rohu oblast otevře. Napojte roh a šrafu opakujte.
-
-## Šrafuje se hřídel v podélném řezu?
-- Ano, vždy
-- [x] Ne
-- Jen když má drážku
-- Jen ve 3D
-> Hřídel, žebro a šroub se v podélném řezu nechávají prázdné. V příčném řezu se hřídel šrafuje.
diff --git a/content/technicka-grafika/2-rocnik/05-koty-linearni.en.md b/content/technicka-grafika/2-rocnik/05-koty-linearni.en.md
deleted file mode 100644
index 46918a3..0000000
--- a/content/technicka-grafika/2-rocnik/05-koty-linearni.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Linear dimensions
-popis: The real size, extension lines and a dimension style.
-nazev_cviceni: Quiz: linear dimensions
-nadpis_kvizu: Read the dimension correctly
----
diff --git a/content/technicka-grafika/2-rocnik/05-koty-linearni.md b/content/technicka-grafika/2-rocnik/05-koty-linearni.md
deleted file mode 100644
index 20556d0..0000000
--- a/content/technicka-grafika/2-rocnik/05-koty-linearni.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-2-koty-linearni
-nazev: Lineární kóty
-popis: Skutečný rozměr, pomocné čáry a styl kót.
-nazev_cviceni: Kvíz: lineární kóty
-nadpis_kvizu: Přečtěte kótu správně
----
-
-# Výklad
-
-## Co kóta říká
-*Skutečný rozměr*
-
-- Kóta je skutečný rozměr součásti, ne délka čáry na papíře.
-- Na výkresu 1:2 je čára poloviční a číslo zůstává celé.
-- `_DIMLINEAR` chce počátek první pomocné čáry, druhé a polohu kótovací čáry.
-- Body chytejte úchopem. Kóta z bodu vedle rohu je o milimetr špatně a tváří se přesně.
-
-> Tip: číslo na kótě nepřepisujte ručně. Když nesedí, je špatně geometrie, ne text.
-
-## Vzhled
-*Čitelné číslo*
-
-- Pomocné čáry odstupují od obrysu asi o 1 mm a přesahují kótovací čáru.
-- Sousední kóty mají stejný odstup, třeba 10 mm od obrysu a 7 mm mezi sebou.
-- Číslo se píše nad kótovací čáru, ne přes obrys. Kóta neprotíná šrafu, když to jde jinak.
-- `_DIMSTYLE` nastaví šipky, písmo a jednotky jednou pro celý výkres.
-
-> Tip: výška písma na A4 bývá 3,5 mm. Menší text se na tisku nedá číst.
-
-## Řetězec a základna
-*Víc rozměrů*
-
-- Řetězec klade kóty za sebou. Součet dílčích kót má dát celkový rozměr.
-- Kóta od základny měří všechno od jedné hrany. Hodí se na rozteče od dorazu.
-- Celkový rozměr a součet dílčích se na jedné hraně neopakují bez důvodu.
-- Kóta patří tam, odkud se rozměr bude vyrábět, ne doprostřed šrafy.
-
-> Tip: 20 + 45 + 15 = 80. Když součet nesedí s celkovou kótou, jedna pomocná čára minula roh.
-
-## Zadání: destička 120 × 70
-*Cvičení*
-
-- Okótujte šířku 120 a výšku 70.
-- Vodorovně přidejte 15 od levého kraje ke středu díry a 90 mezi středy děr.
-- Svisle 15 od dolní hrany ke středu díry.
-- Všechny kóty na hladinu KOTA. `_DIST` mezi rohy musí ukázat stejná čísla jako kóty.
-
-> Tip: kótujte středy děr, ne jejich okraje. Okraj určí až kóta průměru.
-
-
-# Kvíz
-
-## Co se píše do lineární kóty na výkresu 1:2?
-- Polovina skutečného rozměru
-- [x] Skutečný rozměr
-- Dvojnásobek
-- Měřítko 1:2 místo čísla
-> Měřítko mění délku čáry na papíře. Číslo kóty je rozměr součásti.
-
-## Proč nepřepisovat text kóty ručně?
-- AutoCAD to zakazuje vždy
-- [x] Špatné číslo skrývá chybu geometrie
-- Text musí být v palcích
-- Kóta by zmizela z hladiny
-> Správná kóta měří geometrii. Ruční text 120 na čáře dlouhé 119 je lež.
-
-## Od čeho se kótuje poloha díry?
-- Od okraje kružnice
-- [x] Od středu
-- Od osy razítka
-- Od nejbližší šrafy
-> Výroba potřebuje střed. Průměr díry je samostatná kóta.
-
-## Co má platit pro řetězec 20, 45 a 15 na součásti dlouhé 80?
-- Součet má být 100
-- [x] Součet má být 80
-- Celková kóta se vynechá vždy
-- Řetězec se kreslí čerchovaně
-> 20 + 45 + 15 = 80. Nesoulad znamená minutý úchop na některé pomocné čáře.
diff --git a/content/technicka-grafika/2-rocnik/06-koty-prumeru-a-uhlu.en.md b/content/technicka-grafika/2-rocnik/06-koty-prumeru-a-uhlu.en.md
deleted file mode 100644
index 9b60f02..0000000
--- a/content/technicka-grafika/2-rocnik/06-koty-prumeru-a-uhlu.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Diameter, radius and angle dimensions
-popis: The diameter symbol, a fillet radius and a chamfer angle.
-nazev_cviceni: Quiz: diameter and angle
-nadpis_kvizu: Match the symbol to the size
----
diff --git a/content/technicka-grafika/2-rocnik/06-koty-prumeru-a-uhlu.md b/content/technicka-grafika/2-rocnik/06-koty-prumeru-a-uhlu.md
deleted file mode 100644
index 17ce4d0..0000000
--- a/content/technicka-grafika/2-rocnik/06-koty-prumeru-a-uhlu.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-2-koty-prumeru
-nazev: Kóty průměru, poloměru a úhlu
-popis: Značka průměru, poloměr zaoblení a úhel zkosení.
-nazev_cviceni: Kvíz: průměr a úhel
-nadpis_kvizu: Doplňte značku ke správnému rozměru
----
-
-# Výklad
-
-## Průměr a poloměr
-*Díra a oblouk*
-
-- `_DIMDIAMETER` kótuje kružnici. Před číslo patří značka průměru.
-- `_DIMRADIUS` kótuje oblouk. Před číslo patří R.
-- Díra má kótu průměru, i když jste ji kreslili poloměrem. Výroba vrtá průměrem.
-- Zaoblení rohu má kótu poloměru. Jedno R na výkresu platí pro všechny stejné rohy, když to poznámka říká.
-
-> Tip: díru Ø8 nekótujte jako R4. Nástroj se jmenuje podle průměru.
-
-## Úhel
-*Zkosení a klín*
-
-- `_DIMANGULAR` měří úhel mezi dvěma úsečkami nebo obloukem.
-- Zkosení 2 × 45° se často zapíše poznámkou, ne obloukovou kótou v každém rohu.
-- Úhel se měří ve stupních. 45 stupňů je polovina pravého úhlu.
-- Kóta úhlu nesmí překrýt číslo sousední délky. Odsuňte ji dál od vrcholu.
-
-> Tip: pravý úhel se na strojařském výkresu obvykle nekótuje. 90 stupňů se předpokládá.
-
-## Kolik desetin
-*Přesnost*
-
-- Školní kóta celého milimetru nemá zbytečné desetinné nuly, pokud styl neříká jinak.
-- Průměr a poloměr zaokrouhlete stejně jako délky. Nemíchejte 8 a 8,00 bez důvodu.
-- `_DIMSTYLE` nastaví přesnost i značky najednou. Neupravujte každou kótu ručně.
-- Kóta, která ukazuje 7,99 místo 8, minula střed. Geometrii opravte, text ne.
-
-> Tip: 7,99 je skoro jistě úchop vedle středu. `_ID` středu díry to ukáže.
-
-## Zadání: díra a roh
-*Cvičení*
-
-- Na destičce okótujte díru Ø8 příkazem `_DIMDIAMETER`.
-- Zaoblení R8 na drážce okótujte `_DIMRADIUS` jednou. K ostatním stejné poznámkou.
-- Zkosení 2 × 45° zapište k příslušné hraně.
-- Žádná kóta průměru nesmí ukazovat 4 ani R8 u díry, která se vrtá.
-
-> Tip: čtyři stejné díry se kótují jednou, s poznámkou 4× Ø8, plus rozteč středů.
-
-
-# Kvíz
-
-## Jak se kótuje vrtaná díra o průměru 8 mm?
-- R8
-- R4
-- [x] Ø8
-- 8 bez značky, jako délka
-> Vrták se volí podle průměru. Poloměr by výrobce přepočítával a snadno spletl.
-
-## Co znamená R8?
-- Průměr 8
-- [x] Poloměr zaoblení 8 mm
-- Rozteč 8 mm
-- Tloušťka čáry 8
-> R je poloměr oblouku. U díry se R nepoužívá.
-
-## Kóta díry ukazuje 7,99. Co s tím?
-- Přepsat text na 8
-- [x] Opravit geometrii, střed minul úchop
-- Smazat všechny kóty
-- Změnit měřítko na 1:2
-> Text by lež zakryl. Střed díry má ležet přesně tam, kde ho kóty polohy uvádějí.
-
-## Jak jednou kótou popsat čtyři stejné díry Ø8?
-- Čtyřikrát opsat Ø8
-- [x] 4× Ø8 a rozteč středů
-- Jen jednu díru a ostatní nechat bez kóty
-- R4 krát čtyři
-> Počet, průměr a poloha středů stačí. Opakovat stejnou kótu u každé díry výkres zanáší.
diff --git a/content/technicka-grafika/2-rocnik/07-pole.en.md b/content/technicka-grafika/2-rocnik/07-pole.en.md
deleted file mode 100644
index 23f02f0..0000000
--- a/content/technicka-grafika/2-rocnik/07-pole.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Rectangular and polar arrays
-popis: Repeating holes in a row or on a circle.
-nazev_cviceni: Quiz: arrays
-nadpis_kvizu: Work out the spacing and the count
----
diff --git a/content/technicka-grafika/2-rocnik/07-pole.md b/content/technicka-grafika/2-rocnik/07-pole.md
deleted file mode 100644
index c0f8fd0..0000000
--- a/content/technicka-grafika/2-rocnik/07-pole.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-2-pole
-nazev: Obdélníkové a polární pole
-popis: Opakování děr v řadě nebo na kružnici.
-nazev_cviceni: Kvíz: pole
-nadpis_kvizu: Dopočítejte rozteč a počet
----
-
-# Výklad
-
-## Obdélníkové pole
-*Řady a sloupce*
-
-- `_ARRAYRECT` rozkopíruje objekt do řádků a sloupců.
-- Zadává se počet sloupců, počet řádků a rozteč, ne celkový rozměr.
-- Rozteč 30 při třech sloupcích dá od první osy ke třetí 60 mm.
-- Pole může zůstat svázané. Úprava jedné díry se propíše do ostatních.
-
-> Tip: tři díry v řadě mají dvě mezery. Celková délka je dvojnásobek rozteče, ne trojnásobek.
-
-## Polární pole
-*Na kružnici*
-
-- `_ARRAYPOLAR` chce střed otáčení, počet prvků a úhel vyplnění.
-- Čtyři díry po 360 stupních jsou po 90 stupních.
-- Vzorová díra musí ležet na roztečné kružnici dřív, než pole spustíte.
-- Střed pole je střed příruby, ne střed první díry.
-
-> Tip: roztečná kružnice Ø60 má poloměr 30. Vzorová díra leží třeba v bodě 30,0.
-
-## Roztečná kružnice se nekreslí tlustě
-*Osa*
-
-- Kružnice, na které leží středy děr, je tenká čerchovaná osa.
-- Není to hrana součásti a nepatří na OBRYS.
-- Kótuje se jejím průměrem, často jako Ø60, a počtem děr.
-- Samotné díry mají svou kótu průměru jen jednou.
-
-> Tip: na výkresu čtete 4× Ø8 na Ø60. Osm je díra, 60 je kružnice středů.
-
-## Zadání: čtyři díry na kružnici
-*Cvičení*
-
-- Příruba je kružnice Ø80, střed 0,0.
-- Vzorovou díru Ø8 dejte středem do 30,0.
-- `_ARRAYPOLAR` kolem 0,0, čtyři prvky, 360 stupňů.
-- Roztečnou kružnici Ø60 dejte na hladinu OSA. Ověřte `_DIST` mezi sousedními středy.
-
-> Tip: sousední středy svírají 90 stupňů na poloměru 30. Vzdálenost je 30 × √2, asi 42,4 mm.
-
-
-# Kvíz
-
-## Tři díry v řadě mají rozteč 30. Jak daleko je první osa od třetí?
-- 30
-- 90
-- [x] 60
-- 120
-> Mezi třemi dírami jsou dvě mezery. 2 × 30 = 60.
-
-## Kam se umísťuje vzorová díra pro pole na Ø60?
-- Do středu příruby
-- [x] Na poloměr 30 od středu
-- 60 mm od středu
-- Kamkoli, pole si poloměr dopočítá
-> Ø60 znamená poloměr 30. Střed vzorové díry musí na této kružnici už ležet.
-
-## Čím je nakreslená kružnice středů děr?
-- Tlustým obrysem
-- [x] Tenkou čerchovanou osou
-- Šrafou
-- Kótovací čarou
-> Není to hrana materiálu. Je to osa rozteče a patří na hladinu OSA.
-
-## Co znamená zápis 4× Ø8 na Ø60?
-- Čtyři díry o průměru 60
-- [x] Čtyři díry Ø8, jejich středy leží na Ø60
-- Osazení 4 mm
-- Zkosení 8 × 60
-> První průměr je díra, druhý je roztečná kružnice.
diff --git a/content/technicka-grafika/2-rocnik/08-zadani-priruba.en.md b/content/technicka-grafika/2-rocnik/08-zadani-priruba.en.md
deleted file mode 100644
index 1bb0ae0..0000000
--- a/content/technicka-grafika/2-rocnik/08-zadani-priruba.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Assignment: a flange
-popis: A drawing with an offset, a hole pattern, a hatch and dimensions.
-nazev_cviceni: Quiz: the flange
-nadpis_kvizu: Check the flange before you hand it in
----
diff --git a/content/technicka-grafika/2-rocnik/08-zadani-priruba.md b/content/technicka-grafika/2-rocnik/08-zadani-priruba.md
deleted file mode 100644
index 6c75f73..0000000
--- a/content/technicka-grafika/2-rocnik/08-zadani-priruba.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-2-zadani-priruba
-nazev: Zadání: příruba
-popis: Výkres s ekvidistantou, polem děr, šrafou a kótami.
-nazev_cviceni: Kvíz: příruba
-nadpis_kvizu: Zkontrolujte přírubu před odevzdáním
----
-
-# Výklad
-
-## Tvar
-*Rozměry*
-
-- Příruba Ø100, střed 0,0. Středová díra Ø40.
-- Čtyři díry Ø9 na roztečné kružnici Ø70, po 90 stupních.
-- Vnější hrana má zkosení 1 × 45°, které v tomto pohledu vidíte jako tenkou kružnici Ø98.
-- Tloušťka příruby 12 mm se okótuje v druhém pohledu nebo v řezu. V čelním pohledu je kružnice.
-
-> Tip: rozteč Ø70 znamená vzorovou díru ve vzdálenosti 35 mm od středu.
-
-## Řez
-*Aby byla vidět tloušťka*
-
-- Druhý pohled je řez přes osu. Ukáže tloušťku 12 a průchod díry Ø40.
-- V řezu se šrafuje materiál příruby. Díra Ø40 zůstane prázdná.
-- Díry Ø9 v tomto řezu nemusí vyjít všechny. Řezná rovina bere dvě protilehlé, nebo žádnou, podle úhlu.
-- Zvolte řez rovinou, která prochází dvěma protilehlými dírami Ø9, ať je v řezu vidět i jejich průchod.
-
-> Tip: rovina pod úhlem 0 stupňů vezme díry na ose X. Jejich průchod je Ø9 skrz tloušťku 12.
-
-## Kóty a hladiny
-*Jednou*
-
-- Kóty: Ø100, Ø40, 4× Ø9 na Ø70, tloušťka 12, zkosení 1 × 45°.
-- OBRYS, OSA, SRAFA, KOTA, RAM. Typy čar jako ve druháku.
-- Razítko: Příruba, 1:1, ocel.
-- Formát A4 na šířku nebo A3, když se řez vedle čela nevejde čitelně.
-
-> Tip: kóta 4× Ø9 se neopakuje u každé díry. Roztečná kružnice má vlastní kótu Ø70.
-
-## Kontrola
-*Před tiskem*
-
-- `_DIST` od středu k ose díry Ø9 je 35.
-- Šrafa nezasahuje do díry Ø40 ani do děr Ø9.
-- Vypnutí hladiny KOTA nechá čistý tvar. Čísla nezmizí z obrysu, protože na něm nemají být.
-- Soubor priruba.dwg, jednotky milimetry, žádná kóta přepsaná ručně.
-
-> Tip: když kóta Ø100 ukazuje 99,9, vnější kružnice nemá průměr 100. Opravte kružnici, ne text.
-
-
-# Kvíz
-
-## Jak daleko od středu leží osa díry na roztečné kružnici Ø70?
-- 70
-- [x] 35
-- 9
-- 100
-> Rozteč se kótuje průměrem. Poloměr, tedy vzdálenost středu díry, je 35.
-
-## Jak jednou kótou popsat čtyři malé díry?
-- Ø9
-- Ø70
-- [x] 4× Ø9
-- R4,5
-> Počet krát průměr. Poloha je až kóta roztečné kružnice.
-
-## Co se v řezu příruby nešrafuje?
-- Těleso příruby
-- [x] Průchod díry Ø40
-- Obrys
-- Razítko se šrafuje vždy
-> Šrafuje se materiál. Díra je prázdná, šrafa končí na její hraně.
-
-## Kóta vnějšího průměru ukazuje 99,9. Co uděláte?
-- Přepíšete ji na 100
-- [x] Opravíte kružnici na Ø100
-- Změníte měřítko na 1:2
-- Smažete řez
-> Kóta měří geometrii. Ruční text by zakryl, že kružnice je špatně.
diff --git a/content/technicka-grafika/3-rocnik/01-pohledy.en.md b/content/technicka-grafika/3-rocnik/01-pohledy.en.md
deleted file mode 100644
index a25979f..0000000
--- a/content/technicka-grafika/3-rocnik/01-pohledy.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Views and first-angle projection
-popis: Front, top and side views the way they are drawn here.
-nazev_cviceni: Quiz: views
-nadpis_kvizu: Put the view in the right place
----
diff --git a/content/technicka-grafika/3-rocnik/01-pohledy.md b/content/technicka-grafika/3-rocnik/01-pohledy.md
deleted file mode 100644
index f247128..0000000
--- a/content/technicka-grafika/3-rocnik/01-pohledy.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-3-pohledy
-nazev: Pohledy a evropské promítání
-popis: Nárys, půdorys a bokorys tak, jak se kreslí u nás.
-nazev_cviceni: Kvíz: pohledy
-nadpis_kvizu: Umístěte pohled na správné místo
----
-
-# Výklad
-
-## Tři základní pohledy
-*Nárys*
-
-- Nárys je pohled zepředu. Vybírá se tak, aby ukázal tvar co nejlíp.
-- Půdorys je pohled shora. Bokorys je pohled zleva.
-- V evropském promítání leží půdorys pod nárysem, ne nad ním.
-- Bokorys zleva leží vpravo od nárysu. Předmět si představte překlopený k vám.
-
-> Tip: americké promítání má půdorys nahoře. Na českém školním výkresu se nekreslí.
-
-## Co se v pohledu opakuje
-*Stejná hrana*
-
-- Výška nárysu a výška bokorysu je stejné číslo. Šířka nárysu a šířka půdorysu taky.
-- Hloubka půdorysu se rovná šířce bokorysu. Pomůže si 45stupňová pomocná čára.
-- Skrytá hrana se v pohledu, kde není vidět, kreslí tenkou čárkovanou čarou.
-- Hrana, která v daném pohledu splývá s obrysem, se podruhé nekreslí.
-
-> Tip: když je nárys vysoký 40, bokorys nemůže být vysoký 36. Někde chybí hrana nebo měřítko.
-
-## Zarovnání
-*Jedna osa*
-
-- Pohledy se zarovnají. Svislé hrany nárysu a půdorysu leží na stejných svislicích.
-- Vodorovné hrany nárysu a bokorysu leží na stejných vodorovných přímkách.
-- Mezi pohledy nechte mezeru na kóty, třeba 30 až 40 mm. Pohledy se nesmí překrývat.
-- Osa souměrnosti prochází všemi pohledy, ve kterých je součást souměrná.
-
-> Tip: `_XLINE` pod 45 stupňů přenese hloubku z půdorysu do bokorysu. Po dokreslení ji smažte nebo dejte na pomocnou hladinu.
-
-## Zadání: kvádr s osazením
-*Cvičení*
-
-- Kvádr 60 × 40 × 30. Zepředu je na něm osazení: pravá část je o 10 mm nižší, v délce 20 mm.
-- Nárys je 60 široký a 30 vysoký. Půdorys pod ním je 60 × 40.
-- Bokorys vpravo je 40 široký a 30 vysoký. Osazení v něm vyjde jako skrytá nebo viditelná hrana podle směru.
-- Pohledy zarovnejte. Výška 30 musí sedět v nárysu i v bokorysu.
-
-> Tip: osazení dlouhé 20 a nižší o 10 znamená, že v nárysu je horní hrana 40 mm v plné výšce a 20 mm ve výšce 20.
-
-
-# Kvíz
-
-## Kam se v evropském promítání kreslí půdorys?
-- Nad nárys
-- [x] Pod nárys
-- Vlevo od nárysu
-- Místo bokorysu
-> Evropské promítání klade pohled shora pod pohled zepředu. Nahoře by byl americký zvyk.
-
-## Co musí mít nárys a bokorys společné?
-- Šířku
-- [x] Výšku
-- Hloubku jako jediné číslo bez převodu
-- Razítko
-> Výška předmětu je v obou pohledech stejná. Hloubka se přenáší mezi půdorysem a bokorysem.
-
-## Jak se kreslí hrana, kterou v daném pohledu není vidět?
-- Tlustou plnou
-- [x] Tenkou čárkovanou
-- Čerchovanou osou
-- Nezobrazuje se nikdy
-> Skrytá hrana je čárkovaná. Vynechat ji smíte jen tam, kde by výkres zbytečně zanesla a norma to dovolí.
-
-## Kvádr je vysoký 30. Jak vysoký je jeho bokorys?
-- 40
-- [x] 30
-- 60
-- Podle měřítka jinak
-> Výška se mezi nárysem a bokorysem nemění. Měřítko je pro celý výkres jedno.
diff --git a/content/technicka-grafika/3-rocnik/02-rezy.en.md b/content/technicka-grafika/3-rocnik/02-rezy.en.md
deleted file mode 100644
index 3b30d5d..0000000
--- a/content/technicka-grafika/3-rocnik/02-rezy.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Sections and cross-sections
-popis: The cutting plane, the arrows and what gets hatched.
-nazev_cviceni: Quiz: sections
-nadpis_kvizu: Read the cutting plane
----
diff --git a/content/technicka-grafika/3-rocnik/02-rezy.md b/content/technicka-grafika/3-rocnik/02-rezy.md
deleted file mode 100644
index 0845683..0000000
--- a/content/technicka-grafika/3-rocnik/02-rezy.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-3-rezy
-nazev: Řezy a průřezy
-popis: Řezná rovina, šipky a co se šrafuje.
-nazev_cviceni: Kvíz: řezy
-nadpis_kvizu: Přečtěte řeznou rovinu
----
-
-# Výklad
-
-## Úplný řez
-*Rovina A-A*
-
-- Řez ukáže vnitřek, který by jinak byl samá čárkovaná čára.
-- Řezná rovina se v pohledu označí tlustou čerchovanou čarou se šipkami a písmeny, třeba A.
-- Obraz řezu se popíše A-A. Šipky říkají, kterým směrem se díváme.
-- Materiál proťatý rovinou se šrafuje. Prázdné díry ne.
-
-> Tip: šipky řezu míří na plochu, kterou uvidíte. Obrácené šipky otočí celý obraz.
-
-## Poloviční řez
-*Souměrná součást*
-
-- Souměrná součást smí mít půl pohledu a půl řezu v jednom obrazu.
-- Dělí je osa. Pohled bývá nad osou nebo vlevo, řez pod osou nebo vpravo.
-- Na ose mezi polovinou pohledu a řezu se nekreslí tlustý obrys.
-- Skryté hrany v neřezané polovině se často vynechají, protože řez už vnitřek ukázal.
-
-> Tip: hřídel s dírou na ose je typický poloviční řez. Nahoře obrys, dole díra a šrafa.
-
-## Co řezem není
-*Průřez*
-
-- Průřez ukáže jen tvar v řezné rovině, bez toho, co leží za ní.
-- Řez ukáže i hrany za rovinou. Proto je na řezu víc čar než na průřezu.
-- Tenký průřez se smí nakreslit přímo do pohledu, pootočený na místě.
-- Žebro řezané podélně se nešrafuje ani v úplném řezu.
-
-> Tip: když za rovinou vidíte další osazení, je to řez. Když je tam jen vyšrafovaný profil, je to průřez.
-
-## Zadání: pouzdro
-*Cvičení*
-
-- Pouzdro je válec Ø50, díra Ø30, délka 40. Nakreslete nárys jako úplný řez A-A osou.
-- V řezu vyšrafujte mezikruží. Díra Ø30 zůstane prázdná po celé délce 40.
-- Šipky A umístěte do půdorysu nebo do místa, odkud je rovina jasná.
-- Kóty Ø50, Ø30 a 40. Hřídel to není, šrafa tu být má.
-
-> Tip: tloušťka stěny je (50 − 30) / 2 = 10 mm. V řezu je vidět vlevo i vpravo od díry.
-
-
-# Kvíz
-
-## Co říkají šipky u řezné roviny?
-- Tloušťku čáry
-- [x] Směr pohledu na řez
-- Měřítko šrafy
-- Počet děr
-> Obraz řezu je to, co bychom viděli ve směru šipek. Obrácené šipky obraz otočí.
-
-## Šrafuje se díra Ø30 v řezu pouzdra?
-- Ano, je uprostřed
-- [x] Ne, není tam materiál
-- Jen v měřítku 1:2
-- Jen když je pouzdro z oceli
-> Šrafuje se stěna. Průchozí díra zůstane bílá.
-
-## Čím se liší průřez od řezu?
-- Průřez se nešrafuje
-- [x] Průřez neukazuje hrany za řeznou rovinou
-- Řez nemá písmena
-- Průřez je vždy v měřítku 2:1
-> Řez dokreslí i to, co leží za rovinou. Průřez je jen proťatý tvar.
-
-## Kam se u souměrného polovičního řezu kreslí řez?
-- Přes celý obraz
-- [x] Jen na jednu stranu od osy
-- Mimo pohled, bez označení
-- Do razítka
-> Druhá polovina zůstane pohledem. Dělí je osa, ne tlustá hrana.
diff --git a/content/technicka-grafika/3-rocnik/03-bloky.en.md b/content/technicka-grafika/3-rocnik/03-bloky.en.md
deleted file mode 100644
index f9b3ccc..0000000
--- a/content/technicka-grafika/3-rocnik/03-bloky.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Blocks
-popis: A repeated part as one object with a base point.
-nazev_cviceni: Quiz: blocks
-nadpis_kvizu: Insert the block correctly
----
diff --git a/content/technicka-grafika/3-rocnik/03-bloky.md b/content/technicka-grafika/3-rocnik/03-bloky.md
deleted file mode 100644
index 933c73f..0000000
--- a/content/technicka-grafika/3-rocnik/03-bloky.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-3-bloky
-nazev: Bloky
-popis: Opakovaný díl jako jeden objekt se základním bodem.
-nazev_cviceni: Kvíz: bloky
-nadpis_kvizu: Vložte blok správně
----
-
-# Výklad
-
-## K čemu blok je
-*Jeden díl*
-
-- Blok je pojmenovaná skupina objektů, která se vkládá jako jeden kus.
-- Šroub, značka nebo razítko se nekreslí pokaždé znovu.
-- Úprava definice bloku se propíše do všech vložení. Kopie by zůstaly staré.
-- Blok patří do výkresu. `_WBLOCK` ho navíc uloží do vlastního souboru.
-
-> Tip: čtyři stejné šrouby jsou čtyři vložení jednoho bloku, ne čtyři překreslené šrouby.
-
-## Základní bod
-*Kam se chytá*
-
-- `_BLOCK` se zeptá na jméno, základní bod a objekty.
-- Základní bod je místo, za které budete blok vkládat. U šroubu třeba průsečík osy a dosedací plochy.
-- Špatný základní bod posune každé vložení o stejnou chybu.
-- Objekty bloku nedávejte na hladinu 0, pokud nemají přebírat barvu místa vložení. Na škole nechte jejich vlastní hladiny.
-
-> Tip: základní bod chytejte úchopem. Bod „někde u šroubu“ se při vložení mine o několik milimetrů.
-
-## Vložení
-*Místo, měřítko, úhel*
-
-- `_INSERT` vybere jméno, bod vložení, měřítko a otočení.
-- Měřítko 1 a úhel 0 vloží blok tak, jak byl nakreslen.
-- Měřítko 2 ho zvětší. Na výkresu 1:1 se měřítko bloku nemění, mění se až měřítko výřezu.
-- Rozbití bloku příkazem `_EXPLODE` z něj zase udělá obyčejné čáry. Dělejte to jen když máte měnit jediné vložení.
-
-> Tip: rozbitý blok se už neaktualizuje s definicí. Rozbijte ho, jen když opravdu chcete výjimku.
-
-## Zadání: šroub M8 jako blok
-*Cvičení*
-
-- V nárysu nakreslete zjednodušený šroub: dřík Ø8 dlouhý 20 a hlava široká 13 vysoká 5,5.
-- Základní bod dejte na osu, do dosedací plochy pod hlavou.
-- Blok pojmenujte SROUB-M8 a vložte ho dvakrát, po 40 mm.
-- Změňte délku dříku v definici na 25. Obě vložení se musí prodloužit.
-
-> Tip: dosedací plocha je rozhraní hlavy a dříku. Od ní se měří, jak hluboko šroub sedí.
-
-
-# Kvíz
-
-## Čím se liší blok od obyčejné kopie?
-- Ničím
-- [x] Úprava definice se propíše do všech vložení
-- Blok nejde otočit
-- Kopie zabere méně místa
-> Kopie jsou nezávislé. Vložení bloku sdílejí jednu definici.
-
-## Co je základní bod bloku?
-- Střed razítka
-- [x] Bod, za který se blok vkládá
-- Vždy 0,0 výkresu
-- Pravý horní roh objektů
-> Špatný základní bod posune každé vložení. Chytejte ho úchopem na osu nebo na dosedací plochu.
-
-## Kdy blok rozbít příkazem `_EXPLODE`?
-- Vždy po vložení
-- [x] Jen když jedno vložení má být výjimka
-- Aby se zmenšil soubor
-- Před každým tiskem
-> Rozbitý blok se s definicí už neaktualizuje. Ostatní vložení nechte celé.
-
-## Měřítko vložení 1 na výkresu 1:1 znamená?
-- Blok se kreslí poloviční
-- [x] Blok se vloží ve velikosti, v jaké byl nakreslen
-- Blok se převrátí
-- Měřítko razítka se změní na 1:2
-> Měřítko výkresu řeší až výřez na papíře. Blok součásti nechte v měřítku 1.
diff --git a/content/technicka-grafika/3-rocnik/04-atributy-a-kusovnik.en.md b/content/technicka-grafika/3-rocnik/04-atributy-a-kusovnik.en.md
deleted file mode 100644
index b19bfc1..0000000
--- a/content/technicka-grafika/3-rocnik/04-atributy-a-kusovnik.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Attributes and the parts list
-popis: Text in a block that changes with every insert.
-nazev_cviceni: Quiz: attributes
-nadpis_kvizu: Read the parts list
----
diff --git a/content/technicka-grafika/3-rocnik/04-atributy-a-kusovnik.md b/content/technicka-grafika/3-rocnik/04-atributy-a-kusovnik.md
deleted file mode 100644
index 03df34e..0000000
--- a/content/technicka-grafika/3-rocnik/04-atributy-a-kusovnik.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-3-atributy
-nazev: Atributy a kusovník
-popis: Text v bloku, který se mění s každým vložením.
-nazev_cviceni: Kvíz: atributy
-nadpis_kvizu: Přečtěte kusovník
----
-
-# Výklad
-
-## Atribut je popisek bloku
-*Značka*
-
-- Atribut je text uložený v bloku. Při každém vložení se zeptá znovu.
-- `_ATTDEF` založí značku, výzvu a výchozí hodnotu. Značka je třeba OZN.
-- Do bloku se atribut vloží spolu s geometrií. Samotný atribut na výkresu nic neopakuje.
-- Hodnotu už vloženého bloku změníte `_ATTEDIT` nebo dvojklikem, ne přepsáním čáry.
-
-> Tip: značka OZN není text, který se tiskne. Tiskne se hodnota, třeba 1 nebo M8×20.
-
-## Kusovník
-*Co je na výkresu*
-
-- Kusovník je tabulka nad razítkem. Řádek je jedna položka sestavy.
-- Sloupce bývají pozice, název, počet, materiál nebo norma a rozměr.
-- Pozice v kolečku na výkresu musí souhlasit s řádkem tabulky.
-- Počet se počítá z vložení, ne od oka. Dva stejné šrouby jsou počet 2, ne dva řádky.
-
-> Tip: tabulku `_TABLE` dejte na hladinu TEXT nebo RAM. Čáry tabulky nejsou obrys součásti.
-
-## Pořadí
-*Nejdřív definice*
-
-- Nejdřív nakreslete díl, přidejte atribut, teprve pak z toho udělejte blok.
-- Atribut přidaný až po vytvoření bloku se do starých vložení sám nedostane.
-- `_BATTMAN` nebo nové definování bloku vložení srovná. Na škole je jistější blok založit znovu.
-- Výchozí hodnota atributu je nápověda. U šroubu třeba M8×20, a při vložení ji smíte změnit.
-
-> Tip: prázdná hodnota v kusovníku znamená, že jste při vložení výzvu odklepli bez textu.
-
-## Zadání: tři položky
-*Cvičení*
-
-- Sestava má přírubu (pozice 1, 1 kus, ocel), šroub M8×20 (pozice 2, 4 kusy) a matici M8 (pozice 3, 4 kusy).
-- Šroub je blok s atributem OZN. Čtyři vložení mají stejnou hodnotu.
-- Kusovník nad razítkem má tři řádky. Pozice 2 má počet 4, ne 1.
-- Odkazové čáry z koleček 1, 2 a 3 míří na správný díl a nekříží se bez důvodu.
-
-> Tip: čtyři matice jsou pořád jedna položka. Do tabulky nepište čtyři řádky se stejným šroubem.
-
-
-# Kvíz
-
-## Co se při vložení bloku s atributem ptá znovu?
-- Barva hladiny
-- [x] Hodnota atributu, třeba označení
-- Základní bod definice
-- Měřítko výkresu
-> Geometrie bloku zůstává. Mění se text, který jste založili jako atribut.
-
-## Čtyři stejné šrouby v kusovníku jsou?
-- Čtyři řádky
-- [x] Jeden řádek s počtem 4
-- Pozice 1 až 4
-- Atribut bez bloku
-> Stejný díl je jedna položka. Počet říká, kolikrát je vložený.
-
-## Kdy se atribut musí založit?
-- Až po tisku
-- [x] Před vytvořením bloku, spolu s geometrií
-- Jen v razítku
-- Atribut nejde uložit do bloku
-> Atribut přidaný později se do existující definice sám nepřidá.
-
-## Kam patří kusovník?
-- Přes nárys
-- [x] Nad razítko
-- Na hladinu OBRYS
-- Mimo rámeček
-> Čte se spolu s razítkem. Čáry tabulky nejsou hrany součásti.
diff --git a/content/technicka-grafika/3-rocnik/05-rozvrzeni-a-vyrezy.en.md b/content/technicka-grafika/3-rocnik/05-rozvrzeni-a-vyrezy.en.md
deleted file mode 100644
index 14a29ca..0000000
--- a/content/technicka-grafika/3-rocnik/05-rozvrzeni-a-vyrezy.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Layouts and viewports
-popis: The sheet of paper that looks into the model.
-nazev_cviceni: Quiz: viewports
-nadpis_kvizu: Set the viewport scale
----
diff --git a/content/technicka-grafika/3-rocnik/05-rozvrzeni-a-vyrezy.md b/content/technicka-grafika/3-rocnik/05-rozvrzeni-a-vyrezy.md
deleted file mode 100644
index 0f26aa9..0000000
--- a/content/technicka-grafika/3-rocnik/05-rozvrzeni-a-vyrezy.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-3-rozvrzeni
-nazev: Rozvržení a výřezy
-popis: Papír, na kterém leží pohled do modelu.
-nazev_cviceni: Kvíz: výřezy
-nadpis_kvizu: Nastavte měřítko výřezu
----
-
-# Výklad
-
-## Model a papír
-*Dva prostory*
-
-- V modelu je součást v milimetrech 1:1. Papír je rozvržení, záložka nad příkazovým řádkem.
-- Na papír přijde rámeček, razítko a výřez. Výřez je okno do modelu.
-- Dvojklik dovnitř výřezu přepne do modelu. Dvojklik ven vrátí papír.
-- Kóty součásti nechte v modelu. Razítko kreslete v rozvržení, ať má na papíře stálou velikost.
-
-> Tip: rámeček nakreslený v modelu se při změně měřítka výřezu zmenší spolu se součástí. Proto patří na papír.
-
-## Měřítko výřezu
-*1:2 na papíře*
-
-- Výřez má vlastní měřítko. Součást v modelu zůstává 1:1.
-- Měřítko 1:2 znamená, že 1 mm na papíře je 2 mm na součásti.
-- Ve stavovém řádku výřezu se měřítko vybírá ze seznamu. Starší zápis je `_ZOOM` a `1/2xp`.
-- Výřez zamkněte, ať kolečko myši měřítko omylem nerozhodí.
-
-> Tip: nezamčený výřez je nejčastější důvod, proč ráno výkres není 1:1, i když včera byl.
-
-## Co je vidět
-*Hladiny ve výřezu*
-
-- Hladinu lze ve výřezu zmrazit a v modelu ji nechat. Pomocné čáry tak na papír nejdou.
-- Rámeček výřezu dejte na hladinu VYREZ a tu před tiskem vypněte. Na papíře zůstane jen obsah.
-- Jeden výkres smí mít víc výřezů: čelo, řez, detail v jiném měřítku.
-- Detail 2:1 je druhý výřez. V razítku se pak píše měřítko hlavní, detail má vlastní popisku.
-
-> Tip: detail zvětšený 2:1 nekreslete znovu. Je to stejný model v druhém výřezu.
-
-## Zadání: A3 a měřítko 1:2
-*Cvičení*
-
-- Rozvržení nastavte na A3 na šířku. Rámeček a razítko nakreslete v rozvržení.
-- Výřez ukazuje přírubu Ø100 v měřítku 1:2. Na papíře má mít průměr 50 mm.
-- Výřez zamkněte. Hladinu jeho rámečku vypněte.
-- `_DIST` na papíře přes průměr příruby má ukázat 50, ne 100.
-
-> Tip: měření v rozvržení, mimo výřez, čte milimetry papíru. 100 mm součásti v měřítku 1:2 je na papíře 50 mm.
-
-
-# Kvíz
-
-## Kde se kreslí tvar součásti v měřítku 1:1?
-- V rozvržení
-- [x] V modelu
-- V razítku
-- V hladině VYREZ
-> Model zůstává skutečný. Měřítko pro papír nastavuje až výřez.
-
-## Součást Ø100 ve výřezu 1:2 má na papíře průměr?
-- 100 mm
-- [x] 50 mm
-- 200 mm
-- 1 mm
-> 1:2 zmenšuje na polovinu. Kóta ve modelu pořád říká 100.
-
-## Proč výřez zamknout?
-- Aby nešel tisknout
-- [x] Aby kolečko myši nezměnilo měřítko
-- Zamknutí smaže model
-- Aby šel rámeček smazat
-> Nezamčený výřez se při zoomu rozhodí a výkres už není v měřítku z razítka.
-
-## Kam patří rámeček a razítko?
-- Do modelu, na hladinu OBRYS
-- [x] Do rozvržení
-- Do definice bloku šroubu
-- Mimo papír
-> V rozvržení mají stálou velikost bez ohledu na měřítko výřezu.
diff --git a/content/technicka-grafika/3-rocnik/06-tisk.en.md b/content/technicka-grafika/3-rocnik/06-tisk.en.md
deleted file mode 100644
index 2111b75..0000000
--- a/content/technicka-grafika/3-rocnik/06-tisk.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Plotting and PDF
-popis: The sheet, the plot area and a black pen.
-nazev_cviceni: Quiz: plotting
-nadpis_kvizu: Choose the plotter settings
----
diff --git a/content/technicka-grafika/3-rocnik/06-tisk.md b/content/technicka-grafika/3-rocnik/06-tisk.md
deleted file mode 100644
index 9294a6f..0000000
--- a/content/technicka-grafika/3-rocnik/06-tisk.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-3-tisk
-nazev: Tisk a PDF
-popis: Papír, oblast tisku a černobílé pero.
-nazev_cviceni: Kvíz: tisk
-nadpis_kvizu: Vyberte nastavení plotru
----
-
-# Výklad
-
-## Plotr
-*Na papír nebo do PDF*
-
-- `_PLOT` tiskne aktuální rozvržení. Oblast tisku nechte Layout, ne Extents.
-- Tiskárna DWG To PDF uloží výkres, který jde odevzdat bez posunu měřítka.
-- Formát papíru musí sedět s rozvržením. A3 v tisku a A4 v rozvržení ořízne razítko.
-- Měřítko tisku z rozvržení je 1:1. Měřítko součásti už drží výřez.
-
-> Tip: tisk 1:2 z rozvržení, které už má výřez 1:2, zmenší výkres podruhé. Na papíře bude 1:4.
-
-## Pero
-*Černá čára*
-
-- Tabulka per monochrome.ctb tiskne vše černě a tloušťku bere z hladin.
-- Barevný tisk ve škole nechte jen na náhled. Odevzdává se černobílý výkres.
-- Tloušťka obrysu 0,5 mm a tenká čára 0,25 mm musí být v tabulce nebo na hladině vidět.
-- Náhled tisku ukáže, jestli razítko neleze z papíru. Bez náhledu netiskněte.
-
-> Tip: když je na PDF všechno vlásečnicové, tloušťky hladin se netisknou. V `_PLOT` je zapněte.
-
-## Co do PDF nepatří
-*Čistý výkres*
-
-- Rámeček výřezu je vypnutý. Pomocné konstrukce taky.
-- Kóty jsou čitelné a nejsou přes šrafu, pokud to šlo jinak.
-- PDF pojmenujte jako výkres, třeba priruba-A3.pdf, ne Plot1.pdf.
-- Vytiskněte i kontrolní kópii na papír, když učitel odevzdání na papíře chce.
-
-> Tip: otevřete PDF a změřte rámeček A3. Má mít 420 × 297 mm. Jinak je špatně formát nebo měřítko tisku.
-
-## Zadání: PDF příruby
-*Cvičení*
-
-- Rozvržení A3, výřez 1:1 nebo 1:2 podle toho, co se vejde i s kótami.
-- `_PLOT`, tiskárna DWG To PDF, oblast Layout, měřítko 1:1, pero monochrome.ctb.
-- Náhled zkontroluje, že razítko je celé a šrafa není černá plocha.
-- Soubor uložte jako priruba-A3.pdf vedle dwg.
-
-> Tip: hustá šrafa v náhledu znamená velké měřítko vzoru nebo malé. Vraťte se do `_HATCH` a rozteč zvětšete.
-
-
-# Kvíz
-
-## Jaké měřítko tisku se dává při tisku rozvržení?
-- Stejné jako měřítko výřezu, znovu
-- [x] 1:1
-- Fit to paper
-- 2:1, ať je výkres větší
-> Měřítko součásti už má výřez. Tisk papíru je 1:1, jinak se měřítko zdvojí.
-
-## K čemu je monochrome.ctb?
-- Zvětší písmo
-- [x] Tiskne černě a dodrží tloušťky
-- Zamkne výřez
-- Smaže hladinu OSA
-> Barevné čáry na obrazovce jsou pro práci. Odevzdává se černý výkres s tlustým obrysem.
-
-## Proč nenechat oblast tisku na Extents?
-- Extents je rychlejší
-- [x] Vzalo by i objekty mimo papír a rozhodilo by měřítko
-- Extents netiskne kóty
-- Extents vyžaduje 3D
-> Layout tiskne papír tak, jak jste ho složili. Extents hledá vše nakreslené a měřítko si dopočítá samo.
-
-## Co má mít rámeček A3 v hotovém PDF?
-- 210 × 297 mm
-- [x] 420 × 297 mm
-- 100 × 70 mm
-- Libovolný rozměr, rozhoduje obrazovka
-> A3 je 420 × 297 mm. Když PDF vyjde menší, je špatně formát papíru nebo měřítko tisku.
diff --git a/content/technicka-grafika/3-rocnik/07-izometrie.en.md b/content/technicka-grafika/3-rocnik/07-izometrie.en.md
deleted file mode 100644
index 609e526..0000000
--- a/content/technicka-grafika/3-rocnik/07-izometrie.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Isometric drawing
-popis: A pictorial view that does not shrink edges the way perspective does.
-nazev_cviceni: Quiz: isometrics
-nadpis_kvizu: Pick the axis and the ellipse
----
diff --git a/content/technicka-grafika/3-rocnik/07-izometrie.md b/content/technicka-grafika/3-rocnik/07-izometrie.md
deleted file mode 100644
index 3dda985..0000000
--- a/content/technicka-grafika/3-rocnik/07-izometrie.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-3-izometrie
-nazev: Izometrie
-popis: Názorný obraz, ve kterém se rozměry nezkreslují stejně jako v perspektivě.
-nazev_cviceni: Kvíz: izometrie
-nadpis_kvizu: Vyberte osu a elipsu
----
-
-# Výklad
-
-## Tři osy
-*120 stupňů*
-
-- Izometrie má tři osy po 120 stupních. Svislá zůstává svislá.
-- Délky se nanášejí ve skutečné velikosti. Není to perspektiva, hrany se do dálky nezkracují.
-- `_ISODRAFT` přepne kreslicí rovinu. F5 cyklí mezi levou, pravou a horní rovinou.
-- Orto v izometrii drží čáry podél aktuálních os, ne podél obyčejného X a Y.
-
-> Tip: než nakreslíte hranu, podívejte se, která izorovina svítí. Čára v špatné rovině neleží na stěně.
-
-## Kružnice je elipsa
-*Isokružnice*
-
-- Kružnice v izometrii se kreslí jako elipsa. `_ELLIPSE` a volba Isocircle.
-- Nejdřív F5 na rovinu, ve které kružnice leží. Horní díra chce horní rovinu.
-- Průměr se zadává skutečný. Elipsa ho zobrazí zkoseně, kóta zůstává Ø.
-- Obyčejná kružnice `_CIRCLE` v izometrii je chyba. Na papíře by byla pořád kulatá a neladila by s osami.
-
-> Tip: díra na horní stěně a díra na přední stěně jsou dvě různě natočené elipsy. F5 mezi nimi přepněte.
-
-## K čemu izometrie je
-*Názornost*
-
-- Izometrie nenahrazuje nárys a řez. Výrobu řídí okótované pohledy.
-- Na školní práci bývá izometrie v rohu, bez kót nebo jen s hlavními.
-- Skryté hrany se v izometrii často vynechají, ať obraz zůstane čitelný.
-- Tlustý obrys nechte na viditelných hranách. Osy děr smíte naznačit tenčeji.
-
-> Tip: když se izometrie neshoduje s nárysem, platí nárys. Izometrii překreslete.
-
-## Zadání: kvádr s dírou
-*Cvičení*
-
-- V izometrii nakreslete kvádr 60 × 40 × 30.
-- Na horní stěně je díra Ø16 uprostřed. Použijte isokružnici v horní rovině.
-- Soustředně naznačte tenčí elipsu, jen pokud by šlo o zahloubení. Tady zahloubení není.
-- Porovnejte rozměry s nárysem téhož kvádru. 60, 40 a 30 musí sedět na obou.
-
-> Tip: střed horní stěny najdete v polovině šířky a hloubky. Elipsa se středem v rohu je špatně.
-
-
-# Kvíz
-
-## Čím se v izometrii kreslí kružnice?
-- `_CIRCLE`
-- [x] `_ELLIPSE` s volbou Isocircle
-- `_ARC` o 360 stupňů
-- `_POLYGON`
-> Obyčejná kružnice zůstane kulatá a s izometrickými osami neladí.
-
-## Co přepíná klávesa F5 při zapnutém `_ISODRAFT`?
-- Hladiny
-- [x] Izorovina: levá, pravá, horní
-- Měřítko tisku
-- Orto
-> Každá stěna má vlastní natočení elipsy. F5 vybere stěnu, na kterou kreslíte.
-
-## Zkracují se v izometrii hrany směrem dozadu?
-- Ano, jako v perspektivě
-- [x] Ne, délky se nanášejí skutečné
-- Jen svislé
-- Jen při měřítku 1:2
-> Izometrie není perspektiva. Hrana dlouhá 60 mm se nanese jako 60 mm.
-
-## Co platí, když izometrie nesedí s nárysem?
-- Platí izometrie, je názornější
-- [x] Platí okótovaný nárys
-- Oba se nechají a učitel si vybere
-- Smaže se razítko
-> Výrobu určují pohledy a kóty. Izometrie je doplněk a musí se jim přizpůsobit.
diff --git a/content/technicka-grafika/3-rocnik/08-zadani-pouzdro.en.md b/content/technicka-grafika/3-rocnik/08-zadani-pouzdro.en.md
deleted file mode 100644
index 64e0b7f..0000000
--- a/content/technicka-grafika/3-rocnik/08-zadani-pouzdro.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Assignment: a sleeve
-popis: A sectioned front view, a second view, dimensions and an A3 plot.
-nazev_cviceni: Quiz: the sleeve
-nadpis_kvizu: Check the sleeve drawing
----
diff --git a/content/technicka-grafika/3-rocnik/08-zadani-pouzdro.md b/content/technicka-grafika/3-rocnik/08-zadani-pouzdro.md
deleted file mode 100644
index f6d3a4f..0000000
--- a/content/technicka-grafika/3-rocnik/08-zadani-pouzdro.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-3-zadani-pouzdro
-nazev: Zadání: pouzdro
-popis: Nárys v řezu, půdorys, kóty a tisk na A3.
-nazev_cviceni: Kvíz: pouzdro
-nadpis_kvizu: Zkontrolujte výkres pouzdra
----
-
-# Výklad
-
-## Tvar
-*Rozměry*
-
-- Pouzdro je dutý válec. Vnější Ø60, díra Ø36, délka 80.
-- Na pravém konci je osazení: vnější Ø48 v délce 20. Díra Ø36 jde skrz.
-- Na Ø60 jsou čtyři díry M6 do hloubky 12, na roztečné kružnici Ø48. Nejsou průchozí.
-- Závit M6 se v nárysu kreslí zjednodušeně: velký průměr tenkou čarou, malý tlustou, v délce 12 mm.
-
-> Tip: rozteč děr M6 je Ø48, tedy 24 mm od osy. Hloubka 12 není délka pouzdra.
-
-## Pohledy
-*Řez a bok*
-
-- Nárys je úplný řez osou. Šrafuje se stěna, díra Ø36 zůstane prázdná.
-- Půdorys nebo bokorys ukáže čtyři díry M6 na Ø48. Jednu okótujte 4× M6 a hloubku 12.
-- Evropské promítání: druhý pohled pod nárysem nebo vpravo, zarovnaný.
-- Izometrii přidejte vpravo dole, bez kót, ať je tvar jasný.
-
-> Tip: v řezu osou jsou vidět dvě protilehlé díry M6, pokud rovina A-A jimi prochází. Hloubka 12 se kótuje tady.
-
-## Výkres
-*A3*
-
-- Model 1:1. Rozvržení A3, měřítko výřezu 1:1, tisk 1:1, monochrome.ctb.
-- Hladiny OBRYS, OSA, SKRYTE, SRAFA, KOTA, ZAVIT, RAM.
-- Razítko: Pouzdro, ocel, 1:1. Kóty Ø60, Ø36, Ø48, délka 80, osazení 20 a Ø48, 4× M6 hloubka 12.
-- Závitová hladina je tenká. Není to obrys válce.
-
-> Tip: malý průměr závitu M6 je zhruba 5 mm. Tenká čára velkého průměru je 6 mm. Neměňte je za Ø36.
-
-## Kontrola
-*Odevzdání*
-
-- Stěna v řezu má tloušťku (60 − 36) / 2 = 12 mm.
-- Osazení dlouhé 20 nešrafujte jinak než zbytek stěny. Je to pořád jeden kus.
-- PDF priruba nahraďte souborem pouzdro-A3.pdf. Razítko je celé na papíře.
-- Žádná kóta není přepsaná. `_DIST` přes vnější průměr dá 60.
-
-> Tip: dva kusy by měly opačný sklon šrafy. Tady je kus jeden, šrafa má jeden směr.
-
-
-# Kvíz
-
-## Jaká je tloušťka stěny pouzdra Ø60 s dírou Ø36?
-- 24 mm
-- [x] 12 mm
-- 36 mm
-- 6 mm
-> Rozdíl průměrů je 24 mm materiálu na obě strany. Na jednu stěnu připadá 12 mm.
-
-## Jak se zjednodušeně kreslí závit M6?
-- Obě čáry tlusté
-- [x] Velký průměr tenkou čarou, malý tlustou
-- Jen osou
-- Šrafou ANSI31
-> Tenká čára je velký průměr 6 mm. Tlustá je malý průměr, u M6 zhruba 5 mm.
-
-## Kam patří čtyři díry M6?
-- Průchozí skrz Ø36
-- [x] Do hloubky 12 mm na roztečné kružnici Ø48
-- Na osazení Ø48 jako drážka
-- Do razítka
-> Nejsou to průchozí díry hlavní dutiny. Hloubka 12 a rozteč Ø48 jsou v zadání.
-
-## Proč má šrafa pouzdra jeden směr?
-- Dva směry jsou zakázané vždy
-- [x] Je to jeden kus
-- Řez je poloviční
-- Měřítko je 1:2
-> Opačný sklon by říkal, že se dotýkají dva díly. Osazení je pořád totéž pouzdro.
diff --git a/content/technicka-grafika/4-rocnik/01-prostor-3d.en.md b/content/technicka-grafika/4-rocnik/01-prostor-3d.en.md
deleted file mode 100644
index b7db361..0000000
--- a/content/technicka-grafika/4-rocnik/01-prostor-3d.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Model space in 3D
-popis: Axes, the user coordinate system and orbiting the model.
-nazev_cviceni: Quiz: space
-nadpis_kvizu: Find the axis and the AutoCAD edition
----
diff --git a/content/technicka-grafika/4-rocnik/01-prostor-3d.md b/content/technicka-grafika/4-rocnik/01-prostor-3d.md
deleted file mode 100644
index 2c94303..0000000
--- a/content/technicka-grafika/4-rocnik/01-prostor-3d.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-4-prostor
-nazev: Modelový prostor ve 3D
-popis: Osy, uživatelský souřadný systém a obíhání kolem modelu.
-nazev_cviceni: Kvíz: prostor
-nadpis_kvizu: Najděte osu a verzi AutoCADu
----
-
-# Výklad
-
-## Tři osy
-*X, Y, Z*
-
-- K ose X a Y přibude Z. Na obrazovce při startu míří Z k vám.
-- Pravidlo pravé ruky: prsty od X k Y, palec ukazuje Z.
-- `_BOX` a další tělesa potřebují plný AutoCAD. Verze LT tělesa nevytvoří.
-- 2D výkres z dřívějška zůstává. 3D model je druhý soubor, nebo stejný model v jiném pohledu.
-
-> Tip: když příkaz `_BOX` AutoCAD nezná, máte LT. Tělesa v ní nejdou.
-
-## Pohled
-*Orbit*
-
-- ViewCube v rohu nebo `_ORBIT` otáčí pohled. Model se neotáčí, otáčí se kamera.
-- Přední, horní a levý pohled odpovídají nárysu, půdorysu a bokorysu.
-- `_UCS` posune a natočí souřadnice. Nová tělesa vznikají vůči aktuálnímu UCS.
-- `_UCS` a volba World vrátí původní osy. Ztracený UCS je nejčastější zmatek ve 3D.
-
-> Tip: než vysunete profil, zkontrolujte ikonu os. Z má trčet z plochy, kterou vysouváte.
-
-## Drát a stínování
-*Ať něco vidíte*
-
-- Drátový model ukáže všechny hrany, i zadní. Na začátku se v něm líp trefuje.
-- Stínovaný pohled schová zadní hrany. Přepíná se `_VSCURRENT` nebo nabídkou pohledu.
-- Zoom kolečkem pořád platí. Výřez na papír přijde až v lekci o výkresu z modelu.
-- Jednotky zůstávají milimetry. Těleso 60 × 40 × 30 je v tomtéž souboru jako 2D profil.
-
-> Tip: ztracený model najdete `_ZOOM` Extents. Často leží daleko od počátku, protože UCS byl posunutý.
-
-## Zadání: tři osy a kvádr
-*Cvičení*
-
-- V novém souboru v milimetrech nakreslete `_BOX` se základnou 60 × 40 a výškou 30, roh v 0,0,0.
-- Přepněte na přední, horní a levý pohled. V každém má být vidět jiná dvojice rozměrů.
-- `_UCS` World a `_DIST` mezi dvěma rohy podél X má dát 60.
-- Uložte jako kvadr-60x40x30.dwg. LT na tento soubor nestačí.
-
-> Tip: přední pohled ukáže 60 × 30, horní 60 × 40, levý 40 × 30.
-
-
-# Kvíz
-
-## Který AutoCAD umí tělesa `_BOX`?
-- Každá verze včetně LT
-- [x] Plný AutoCAD, ne LT
-- Jen verze na Mac bez příkazů
-- Žádný, tělesa jsou v jiném programu
-> LT je 2D. Příkaz `_BOX` v ní není.
-
-## Co dělá `_ORBIT`?
-- Otočí těleso v souřadnicích
-- [x] Otočí pohled kamery
-- Nastaví měřítko tisku
-- Smaže osu Z
-> Model zůstane. Změní se, odkud se na něj díváte.
-
-## Co vrátí souřadnice do původního směru?
-- `_ZOOM` Extents
-- [x] `_UCS` a volba World
-- Vypnutí hladiny 0
-- `_PLOT`
-> Posunutý UCS nechá nová tělesa „ležet na boku“. World je výchozí osy výkresu.
-
-## Co ukáže přední pohled na kvádr 60 × 40 × 30, když 30 je výška?
-- 60 × 40
-- [x] 60 × 30
-- 40 × 30
-- Jen osu Z
-> Přední pohled je šířka a výška. Hloubka 40 je v půdorysu a v bokorysu.
diff --git a/content/technicka-grafika/4-rocnik/02-telesa-a-vysunuti.en.md b/content/technicka-grafika/4-rocnik/02-telesa-a-vysunuti.en.md
deleted file mode 100644
index 8684db3..0000000
--- a/content/technicka-grafika/4-rocnik/02-telesa-a-vysunuti.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Solids and extrude
-popis: A box, a cylinder and a profile pulled up by a height.
-nazev_cviceni: Quiz: extrude
-nadpis_kvizu: Pick the profile and the height
----
diff --git a/content/technicka-grafika/4-rocnik/02-telesa-a-vysunuti.md b/content/technicka-grafika/4-rocnik/02-telesa-a-vysunuti.md
deleted file mode 100644
index d115dcf..0000000
--- a/content/technicka-grafika/4-rocnik/02-telesa-a-vysunuti.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-4-vysunuti
-nazev: Tělesa a vysunutí
-popis: Kvádr, válec a profil tažený o výšku.
-nazev_cviceni: Kvíz: vysunutí
-nadpis_kvizu: Vyberte profil a výšku
----
-
-# Výklad
-
-## Hotová tělesa
-*Kvádr a válec*
-
-- `_BOX` chce roh nebo střed základny, půdorys a výšku.
-- `_CYLINDER` chce střed, poloměr nebo průměr a výšku.
-- Výška se měří ve směru osy Z aktuálního UCS.
-- Tato tělesa jsou plná. Nejsou to stěny z ploch.
-
-> Tip: válec Ø40 a výška 25 má v příkazu buď poloměr 20, nebo volbu průměru 40.
-
-## Vysunutí profilu
-*Z 2D do výšky*
-
-- `_EXTRUDE` vezme uzavřenou křivku nebo oblast a táhne ji o výšku.
-- Profil musí být zavřený. Čtyři nenapojené úsečky vysunout nejdou.
-- Kladná výška jde ve směru Z. Záporná na opačnou stranu.
-- `_PRESSPULL` umí přitáhnout uzavřenou oblast, na kterou kliknete, bez předchozího výběru.
-
-> Tip: obdélník 80 × 50 vysunutý o 12 je destička z druháku, teď jako těleso.
-
-## Profil nejdřív ve 2D
-*Plocha*
-
-- Profil nakreslete v rovině, ze které chcete táhnout. Nejčastěji půdorys, Z ven z obrazovky.
-- `_REGION` z uzavřené křivky udělá jednu plochu. Vysouvá se spolehlivěji než volné čáry.
-- Po vysunutí profil smíte smazat, nebo ho nechte na hladině PROFIL a tu zmrazte.
-- Jeden profil, jedna výška. Osazení vznikne až druhým tělesem a booleanem v další lekci.
-
-> Tip: otevřená křivka se vysune na plochu, ne na těleso. Pro součást potřebujete těleso.
-
-## Zadání: destička jako těleso
-*Cvičení*
-
-- Profil 120 × 70, `_EXTRUDE` o 10 mm. To je tloušťka destičky.
-- Díru zatím nekreslete. Ověřte `_MASSPROP`: objem má být 120 × 70 × 10 = 84 000 mm³.
-- Přední pohled ukáže 120 × 10. Horní pohled 120 × 70.
-- Uložte jako desticka-3d.dwg.
-
-> Tip: když objem vyjde dvojnásobek, výška je 20 místo 10. Kóta textu to nespraví, `_MASSPROP` čte model.
-
-
-# Kvíz
-
-## Jaký poloměr má `_CYLINDER` o průměru 40?
-- 40
-- [x] 20
-- 80
-- 10
-> Příkaz bez přepnutí bere poloměr. Průměr je volba, nebo napište polovinu.
-
-## Co `_EXTRUDE` potřebuje, aby vzniklo těleso?
-- Otevřenou úsečku
-- [x] Uzavřený profil
-- Razítko
-- Už hotový válec
-> Neuzavřený profil vytvoří jen plochu. Těleso má mít objem.
-
-## Kam míří kladná výška vysunutí?
-- Vždy doprava na obrazovce
-- [x] Ve směru osy Z aktuálního UCS
-- Do záporného X
-- Do razítka
-> Špatně natočený UCS vysune destičku „na bok“. Před příkazem zkontrolujte osy.
-
-## Objem destičky 120 × 70 × 10 mm je?
-- 840 mm³
-- [x] 84 000 mm³
-- 200 mm³
-- 12 000 mm³
-> 120 × 70 = 8 400, krát 10 je 84 000. `_MASSPROP` má ukázat totéž.
diff --git a/content/technicka-grafika/4-rocnik/03-rotace.en.md b/content/technicka-grafika/4-rocnik/03-rotace.en.md
deleted file mode 100644
index 4f4c6b4..0000000
--- a/content/technicka-grafika/4-rocnik/03-rotace.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Revolving a profile
-popis: A part symmetric about an axis is the section spun around.
-nazev_cviceni: Quiz: revolve
-nadpis_kvizu: Pick the profile and the axis
----
diff --git a/content/technicka-grafika/4-rocnik/03-rotace.md b/content/technicka-grafika/4-rocnik/03-rotace.md
deleted file mode 100644
index 9a77666..0000000
--- a/content/technicka-grafika/4-rocnik/03-rotace.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-4-rotace
-nazev: Rotace profilu
-popis: Součást souměrná podle osy vznikne otočením řezu.
-nazev_cviceni: Kvíz: rotace
-nadpis_kvizu: Vyberte profil a osu
----
-
-# Výklad
-
-## Kdy rotovat
-*Místo skládání válců*
-
-- `_REVOLVE` otočí uzavřený profil kolem osy o zadaný úhel.
-- 360 stupňů udělá plné rotační těleso. 180 udělá polovinu.
-- Hodí se na hřídel, pouzdro, přírubu a každou součást souměrnou podle osy.
-- Profil je polovina řezu. Osa rotace je osa součásti a profil ji smí jen lemovat, ne křížit.
-
-> Tip: pouzdro z třetího ročníku je jeden profil stěny otočený o 360 stupňů, ne válec minus válec. Oba postupy jsou správně, rotace je u čistě rotačního dílu kratší.
-
-## Profil
-*Polovina*
-
-- Nakreslete jen polovinu řezu, vpravo od osy. Uzavřete ho.
-- U pouzdra Ø60 / Ø36 dlouhého 80 je profil obdélník 12 × 80. Dvanáct je tloušťka stěny.
-- Osa je svislá hrana díry. Profil se osy dotýká jen vnější? Ne: vnitřní hrana profilu je o poloměr díry od osy.
-- Vnitřní hrana leží 18 mm od osy, vnější 30 mm. Šířka profilu je 12 mm.
-
-> Tip: 36 / 2 = 18 a 60 / 2 = 30. Profil je pás mezi těmito poloměry.
-
-## Osa a úhel
-*360*
-
-- Osu vyberte dva body, nebo jednu úsečku nakreslenou na hladině OSA.
-- Úhel 360. Menší úhel nechá výseč a na školní hřídeli bývá omyl.
-- Profil nesmí osu křížit. Křížení AutoCAD odmítne, nebo udělá těleso, které se protíná samo.
-- Po rotaci profil zmrazte. V řezu ho zase použijete jako kontrolu.
-
-> Tip: když vznikne plný válec bez díry, profil začínal na ose. Vnitřní hrana musí být 18 mm od osy, ne 0.
-
-## Zadání: pouzdro rotací
-*Cvičení*
-
-- Osa je úsečka z 0,0 do 0,80.
-- Profil: obdélník s rohy 18,0 a 30,80.
-- `_REVOLVE` o 360 stupňů kolem této osy.
-- `_MASSPROP` objem mezikruží: π × (30² − 18²) × 80. Má vyjít zhruba 144 764 mm³.
-
-> Tip: 900 − 324 = 576, krát π je asi 1 810, krát 80 je asi 144 800. Řád tisíců, ne desítek.
-
-
-# Kvíz
-
-## Jaký úhel udělá plné rotační těleso?
-- 90
-- 180
-- [x] 360
-- 45
-> Polovina otáčky je výseč. Uzavřené pouzdro potřebuje celých 360 stupňů.
-
-## Kde leží vnitřní hrana profilu pouzdra Ø36?
-- Na ose
-- [x] 18 mm od osy
-- 36 mm od osy
-- 60 mm od osy
-> Poloměr díry je polovina průměru. Profil díru ohraničuje, nesmí začínat na ose, jinak díra zmizí.
-
-## Co se stane, když profil osu kříží?
-- Vznikne správné pouzdro
-- [x] AutoCAD rotaci odmítne, nebo těleso protne samo sebe
-- Profil se jen posune
-- Vznikne šrafa
-> Osa smí být hranou profilu, ne smí jím procházet skrz.
-
-## Proč rotovat pouzdro místo skládání dvou válců?
-- Jinak to nejde
-- [x] U rotačního dílu stačí jeden profil
-- Rotace umí i hranaté osazení bez profilu
-- `_REVOLVE` funguje i v LT
-> Oba postupy jsou správně. Rotace je kratší, když je součást souměrná podle osy. LT ji neumí.
diff --git a/content/technicka-grafika/4-rocnik/04-boolean.en.md b/content/technicka-grafika/4-rocnik/04-boolean.en.md
deleted file mode 100644
index e6fc00f..0000000
--- a/content/technicka-grafika/4-rocnik/04-boolean.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Union, subtract and intersect
-popis: Solids add and subtract like sets.
-nazev_cviceni: Quiz: booleans
-nadpis_kvizu: Pick the order of the solids
----
diff --git a/content/technicka-grafika/4-rocnik/04-boolean.md b/content/technicka-grafika/4-rocnik/04-boolean.md
deleted file mode 100644
index b833fe4..0000000
--- a/content/technicka-grafika/4-rocnik/04-boolean.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-4-boolean
-nazev: Sjednocení, rozdíl a průnik
-popis: Tělesa se sčítají a odečítají jako množiny.
-nazev_cviceni: Kvíz: boolean
-nadpis_kvizu: Vyberte pořadí těles
----
-
-# Výklad
-
-## Sjednocení
-*Jeden kus*
-
-- `_UNION` slepí tělesa, která se dotýkají nebo pronikají, v jeden objekt.
-- Osazení na hřídeli jsou dva válce. Po sjednocení je to jedna součást.
-- Tělesa, která se nedotýkají, sjednocení také spojí do jedné skupiny, ale výrobek to není.
-- Než sjednotíte, zkontrolujte přesah. Mezera 0,01 mm nechá dva kusy.
-
-> Tip: `_INTERFERE` ukáže, jestli se dvě tělesa opravdu pronikají, nebo se jen zdají dotýkat.
-
-## Rozdíl
-*Díra*
-
-- `_SUBTRACT` nejdřív vyberete těleso, které má zůstat, potvrdíte, a teprve pak těleso, které se odečte.
-- Obrácené pořadí smaže součást a nechá díru jako váleček. To je nejčastější chyba.
-- Díra Ø10 je válec Ø10 delší, než je tloušťka stěny, ať rozdíl prořízne celou stěnu.
-- Válec díry po rozdílu zmizí. Nemusíte ho mazat ručně.
-
-> Tip: válec díry nechte o milimetr přečuhovat z obou stran. Jinak na stěně zůstane blanka.
-
-## Průnik
-*Společný objem*
-
-- `_INTERSECT` nechá jen objem, který mají tělesa společný. Zbytek zmizí.
-- Hodí se, když potřebujete tvar daný dvěma podmínkami, třeba válec seříznutý kvádrem.
-- Na běžnou díru průnik není. Díra je rozdíl.
-- Po průniku zůstanou obě původní tělesa pryč. Když je budete potřebovat, předem je zkopírujte.
-
-> Tip: sjednocení přidá, rozdíl ubere, průnik nechá jen překryv. U díry je to vždy rozdíl.
-
-## Zadání: destička s dírou
-*Cvičení*
-
-- Destička `_BOX` 120 × 70 × 10.
-- Válec Ø16, střed v 60,35, výška od -1 do 11, ať prochází skrz.
-- `_SUBTRACT`: nejdřív destička, potom válec.
-- `_MASSPROP` objem: 84 000 minus objem válce v tloušťce 10. Válec Ø16 a výška 10 má objem π × 8² × 10 ≈ 2 011. Výsledek je asi 81 989 mm³.
-
-> Tip: když zmizela destička a zbyl váleček, pořadí rozdílu bylo obrácené. Vraťte krok zpět.
-
-
-# Kvíz
-
-## Jakým pořadím vznikne díra?
-- Nejdřív válec, potom destička
-- [x] Nejdřív destička, potom válec
-- `_UNION`
-- `_INTERSECT`
-> První výběr zůstává. Druhý se odečte. Obrácené pořadí nechá jen váleček.
-
-## Proč má válec díry přečnívat z destičky?
-- Aby šel okótovat
-- [x] Aby ve stěně nezůstala blanka
-- Přečnívat nesmí, rozdíl by selhal
-- Aby vznikl závit
-> Válec zarovnaný přesně s plochou někdy nechá tenkou slupku. Milimetr přesahu ji odstraní.
-
-## Který příkaz slepí dva dotýkající se válce hřídele?
-- `_SUBTRACT`
-- [x] `_UNION`
-- `_INTERSECT`
-- `_REVOLVE`
-> Osazení je jeden kus. Sjednocení z dvou válců udělá jedno těleso.
-
-## Co nechá `_INTERSECT`?
-- Součet objemů
-- [x] Jen společný překryv
-- Díru
-- Původní tělesa vedle výsledku
-> Zbytek obou těles zmizí. Před průnikem si je zkopírujte, pokud je ještě budete potřebovat.
diff --git a/content/technicka-grafika/4-rocnik/05-rez-a-flatshot.en.md b/content/technicka-grafika/4-rocnik/05-rez-a-flatshot.en.md
deleted file mode 100644
index cd46853..0000000
--- a/content/technicka-grafika/4-rocnik/05-rez-a-flatshot.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Sectioning a solid and Flatshot
-popis: From the model back to lines for the drawing.
-nazev_cviceni: Quiz: the model section
-nadpis_kvizu: Decide what lands on the sheet
----
diff --git a/content/technicka-grafika/4-rocnik/05-rez-a-flatshot.md b/content/technicka-grafika/4-rocnik/05-rez-a-flatshot.md
deleted file mode 100644
index c8f2f53..0000000
--- a/content/technicka-grafika/4-rocnik/05-rez-a-flatshot.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-4-rez-modelu
-nazev: Řez tělesem a plochý snímek
-popis: Z modelu zpátky čáry pro výkres.
-nazev_cviceni: Kvíz: řez modelu
-nadpis_kvizu: Vyberte, co vznikne na papíře
----
-
-# Výklad
-
-## Řezná rovina
-*Uvnitř tělesa*
-
-- `_SECTIONPLANE` položí rovinu řezu. Živé zobrazení ukáže řez, aniž by těleso rozřízlo natrvalo.
-- `_SLICE` těleso rozřízne doopravdy. Polovinu můžete nechat, nebo smazat.
-- Na školní výkres řežte živě nebo si před `_SLICE` těleso zkopírujte. Původní model budete ještě potřebovat.
-- Řez osou pouzdra má ukázat stěnu 12 mm a díru Ø36, stejně jako ruční řez ve třetím ročníku.
-
-> Tip: rovina, která mine osu, ukáže jinou tloušťku stěny. U pouzdra veďte řez osou.
-
-## Plochý snímek
-*Čáry z pohledu*
-
-- `_FLATSHOT` promítne aktuální pohled do čar v rovině XY.
-- Viditelné hrany dá na jednu hladinu, skryté na druhou. Skryté pak přepněte na čárkované.
-- Než příkaz spustíte, nastavte pohled: přední pro nárys, horní pro půdorys.
-- Snímek je 2D. Dá se kótovat jako výkres z druháku. Model vedle něj nechte nedotčený.
-
-> Tip: plochý snímek pořízený v šikmém pohledu není nárys. Nejdřív srovnejte pohled, potom foťte.
-
-## Co snímek neudělá
-*Kóty a šrafa*
-
-- Snímek neumí šrafu řezu ani kóty. Ty doplníte vy, na hladiny SRAFA a KOTA.
-- Neumí ani razítko. To patří do rozvržení.
-- Čáry snímku bývají na nových hladinách. Přesuňte je na OBRYS a SKRYTE, ať platí školní tloušťky.
-- Když model potom změníte, snímek se sám neopraví. Po úpravě modelu ho vytvořte znovu.
-
-> Tip: výkresové pohledy z pozdější lekce se na rozdíl od snímku umí s modelem aktualizovat. Snímek je rychlá kontrola.
-
-## Zadání: nárys z tělesa
-*Cvičení*
-
-- Otevřete pouzdro z rotace. Přední pohled, `_FLATSHOT`, viditelné hrany na OBRYS.
-- Do řezu osou doplňte šrafu stěny ručně, nebo použijte živý řez a z něj čáry.
-- Okótujte Ø60, Ø36 a délku 80. Čísla musí sedět s `_MASSPROP` a s poloměry profilu.
-- Snímek nesmí zůstat šikmo. Nárys je v rovině, díra je obdélník 36 × 80 uvnitř obdélníku 60 × 80.
-
-> Tip: v nárysu řezu jsou dvě vodorovné čáry díry. Jejich vzdálenost je 36.
-
-
-# Kvíz
-
-## Čím rozříznete těleso natrvalo?
-- `_FLATSHOT`
-- [x] `_SLICE`
-- `_ORBIT`
-- `_PLOT`
-> Živá řezná rovina model nechá celý. `_SLICE` ho rozdělí. Předtím si nechte kopii.
-
-## Co musí předcházet plochému snímku nárysu?
-- Tisk
-- [x] Srovnaný přední pohled
-- Rozbití tělesa
-- Šrafa
-> Snímek promítne to, co právě vidíte. Šikmý orbit není nárys.
-
-## Doplní `_FLATSHOT` kóty?
-- Ano, z hladiny KOTA
-- [x] Ne
-- Jen průměry
-- Jen když je měřítko 1:1
-> Snímek jsou čáry. Kóty, šrafu a razítko dodáte vy.
-
-## Jak daleko jsou v nárysu řezu pouzdra od sebe čáry díry Ø36?
-- 18
-- 60
-- [x] 36
-- 12
-> V řezu je díra obdélník. Jeho výška je průměr, ne poloměr.
diff --git a/content/technicka-grafika/4-rocnik/06-zobrazeni.en.md b/content/technicka-grafika/4-rocnik/06-zobrazeni.en.md
deleted file mode 100644
index 3a7cfc8..0000000
--- a/content/technicka-grafika/4-rocnik/06-zobrazeni.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Visual styles
-popis: Wireframe, hidden lines and shading for checking the shape.
-nazev_cviceni: Quiz: display
-nadpis_kvizu: Pick a style for the task
----
diff --git a/content/technicka-grafika/4-rocnik/06-zobrazeni.md b/content/technicka-grafika/4-rocnik/06-zobrazeni.md
deleted file mode 100644
index 687c0a4..0000000
--- a/content/technicka-grafika/4-rocnik/06-zobrazeni.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-4-zobrazeni
-nazev: Vizuální styly
-popis: Drát, skryté hrany a stínování pro kontrolu tvaru.
-nazev_cviceni: Kvíz: zobrazení
-nadpis_kvizu: Vyberte styl podle úkolu
----
-
-# Výklad
-
-## Drát a skryté hrany
-*Kontrola*
-
-- Drátový model ukáže každou hranu. Hodí se, když saháte na zadní vrchol.
-- Styl skrytých hran schová, co je za materiálem. Tvar se čte jako na fotografii bez stínu.
-- `_VSCURRENT` styly přepíná. Na pásu karet je stejná nabídka u pohledu.
-- Styl nemění těleso. Špatně odečtená díra zůstane špatně v každém stylu.
-
-> Tip: nejdřív drát, ať trefíte hranu, potom skryté hrany, ať zkontrolujete, že díra opravdu prochází.
-
-## Stínování
-*Objem*
-
-- Konceptuální nebo stínovaný styl ukáže, jestli je díra průchozí, nebo je to jen namalovaná kružnice.
-- Barva tělesa se bere z hladiny. OBRYS a těleso smí mít vlastní hladinu MODEL.
-- Materiál a render na škole stačí náznak. Odevzdává se výkres, ne obrázek.
-- Stínování zpomalí otáčení. Na slabším počítači se vraťte do drátu, než budete modelovat dál.
-
-> Tip: když v řezu vidíte dno díry, válec byl krátký. Rozdíl stěnu neprošel.
-
-## Řezy bez rozbití
-*Pohled*
-
-- Živý řez v kombinaci se stínováním je nejrychlejší kontrola tloušťky stěny.
-- Pohled uložte `_VIEW`, ať se k němu vrátíte. Název třeba REZ-AA.
-- Izometrický stínovaný pohled smí přijít na výkres jako názorný doplněk, ne jako jediný obraz.
-- Barvy na obrazovce pořád tiskněte černě. Styl zobrazení není tabulka per.
-
-> Tip: uložený pohled nese i styl. Když REZ-AA otevřete a je z něj drát, někdo styl přepsal.
-
-## Zadání: tři kontroly destičky
-*Cvičení*
-
-- Destičku s dírou Ø16 zobrazte jako drát, jako skryté hrany a stínovaně.
-- Ve stínovaném horním pohledu musí být skrz díru vidět. Když není, opakujte rozdíl.
-- Uložte pohled HORNI a pohled PREDNI.
-- Do výkresu tyto pohledy zatím nevkládejte. Jde o kontrolu modelu.
-
-> Tip: díra, která ve stínu má dno stejné barvy jako destička, je slepá. Výška válce byla menší než 10 mm.
-
-
-# Kvíz
-
-## Který styl nechá vidět i zadní hrany?
-- Stínovaný
-- [x] Drátový
-- Skryté hrany
-- Monochrome.ctb
-> Drát nekryje nic. Skryté hrany a stín zadní geometrii schovají.
-
-## Změní vizuální styl objem tělesa?
-- Ano, stín ubere materiál
-- [x] Ne
-- Jen u rozdílu
-- Jen při tisku
-> Styl je způsob kresby na obrazovce. `_MASSPROP` zůstane stejný.
-
-## Co máte vidět skrz průchozí díru ve stínovaném pohledu?
-- Dno stejné barvy
-- [x] Prostor za destičkou
-- Šrafu ANSI31
-- Razítko
-> Dno znamená, že válec díry byl krátký a rozdíl stěnu neprošel.
-
-## Čím se na papír dostane černá čára, když je model na obrazovce modrý?
-- Vizuálním stylem
-- [x] Tabulkou per při tisku
-- Smazáním hladiny
-- `_ORBIT`
-> Barva na monitoru je pracovní. monochrome.ctb z ní na PDF udělá černou.
diff --git a/content/technicka-grafika/4-rocnik/07-vykres-z-modelu.en.md b/content/technicka-grafika/4-rocnik/07-vykres-z-modelu.en.md
deleted file mode 100644
index 4ad88b7..0000000
--- a/content/technicka-grafika/4-rocnik/07-vykres-z-modelu.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: A drawing from the 3D model
-popis: A front view and a section that update with the model.
-nazev_cviceni: Quiz: model views
-nadpis_kvizu: Tie the sheet to the solid
----
diff --git a/content/technicka-grafika/4-rocnik/07-vykres-z-modelu.md b/content/technicka-grafika/4-rocnik/07-vykres-z-modelu.md
deleted file mode 100644
index 00b0c55..0000000
--- a/content/technicka-grafika/4-rocnik/07-vykres-z-modelu.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-4-vykres-z-modelu
-nazev: Výkres z 3D modelu
-popis: Nárys a řez, které se s modelem aktualizují.
-nazev_cviceni: Kvíz: pohledy z modelu
-nadpis_kvizu: Propojte výkres s tělesem
----
-
-# Výklad
-
-## Základní pohled
-*Z tělesa*
-
-- V rozvržení příkaz základního pohledu vezme těleso a položí na papír nárys, půdorys nebo izometrii.
-- Měřítko pohledu nastavíte jako u výřezu, třeba 1:1 nebo 1:2.
-- Odvozené pohledy se položí nad, pod nebo vedle. Drží evropské promítání, když ho tak nastavíte.
-- Pohled je svázaný s modelem. Změna tělesa a aktualizace pohledu přepíše čáry.
-
-> Tip: plochý snímek se sám neopraví. Pohled z modelu ano. Na maturitní výkres použijte pohled.
-
-## Řez pohledu
-*A-A*
-
-- Z základního pohledu uděláte řez tažením řezné čáry. Písmena A doplní AutoCAD, nebo je přepište na A.
-- Šrafu řezu nastavte na ANSI31 a školní měřítko. Hřídel v podélném řezu pořád nešrafujte, i když to AutoCAD nabídne.
-- Skryté čáry v pohledu vypněte, pokud řez vnitřek ukázal. Výkres zůstane čistý.
-- Kóty klikejte na pohled, ne do modelu naslepo. Berou rozměr z tělesa.
-
-> Tip: kóta napojená na pohled se po aktualizaci přepočítá. Kóta nakreslená vedle jako obyčejná čára ne.
-
-## Papír
-*A3*
-
-- Rámeček a razítko jsou v rozvržení, měřítko 1:1 vůči papíru.
-- Tisk je Layout, 1:1, monochrome.ctb, jako ve třetím ročníku.
-- Izometrický pohled bez kót smí být třetí obraz. Nesmí být jediný.
-- Než odevzdáte, aktualizujte pohledy. Starý pohled lže, když jste model mezitím změnili.
-
-> Tip: ve stromu nebo přes aktualizaci pohledu uvidíte, jestli je obraz aktuální. Nevěřte čáře, která přežila smazanou díru.
-
-## Zadání: pouzdro na A3
-*Cvičení*
-
-- Z 3D pouzdra vytvořte nárys jako řez A-A a půdorys nebo bokorys s roztečí, pokud na modelu díry jsou.
-- Měřítko 1:1, formát A3. Kóty Ø60, Ø36 a 80.
-- Změňte v modelu délku na 90, aktualizujte pohled a přesvědčte se, že kóta délky ukáže 90.
-- Vraťte délku na 80, ať odevzdáte zadání, ne zkoušku. PDF pojmenujte pouzdro-z-modelu.pdf.
-
-> Tip: když po změně modelu kóta zůstane 80, není napojená na pohled. Smažte ji a okótujte znovu.
-
-
-# Kvíz
-
-## Čím se pohled z modelu liší od `_FLATSHOT`?
-- Ničím
-- [x] Po změně tělesa se dá aktualizovat
-- Umí jen izometrii
-- Funguje v LT
-> Snímek je jednorázová kopie čar. Pohled zůstává svázaný s tělesem.
-
-## Kam patří rámeček výkresu z modelu?
-- Dovnitř tělesa
-- [x] Do rozvržení
-- Na hladinu OBRYS modelu
-- Do definice bloku
-> Papír má vlastní měřítko. Rámeček v modelu by se zmenšil s pohledem.
-
-## Co uděláte po změně délky pouzdra v modelu?
-- Nic, kóty se nemění nikdy
-- [x] Aktualizujete pohled
-- Smažete razítko
-- Přepnete na LT
-> Neaktualizovaný pohled ukazuje starý tvar. Kóta napojená na něj se přepočítá spolu s ním.
-
-## Jaké měřítko má tisk tohoto rozvržení?
-- 1:2, protože model je velký
-- [x] 1:1
-- Podle `_MASSPROP`
-- Fit
-> Měřítko obrazu drží pohled. Tisk papíru je 1:1, jinak se měřítko zdvojí.
diff --git a/content/technicka-grafika/4-rocnik/08-maturitni-zadani.en.md b/content/technicka-grafika/4-rocnik/08-maturitni-zadani.en.md
deleted file mode 100644
index 5bfcb04..0000000
--- a/content/technicka-grafika/4-rocnik/08-maturitni-zadani.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Exam assignment
-popis: From a sketch through the model to a PDF that stands on its own.
-nazev_cviceni: Quiz: the exam piece
-nadpis_kvizu: Walk the whole drawing
----
diff --git a/content/technicka-grafika/4-rocnik/08-maturitni-zadani.md b/content/technicka-grafika/4-rocnik/08-maturitni-zadani.md
deleted file mode 100644
index 19d47ea..0000000
--- a/content/technicka-grafika/4-rocnik/08-maturitni-zadani.md
+++ /dev/null
@@ -1,80 +0,0 @@
----
-id: tg-4-maturitni-zadani
-nazev: Maturitní zadání
-popis: Od náčrtu přes model po PDF, které obstojí samo.
-nazev_cviceni: Kvíz: maturita
-nadpis_kvizu: Projdete celý výkres
----
-
-# Výklad
-
-## Součást
-*Přírubové pouzdro*
-
-- Dutý válec Ø70 / Ø40, délka 60. Na jednom konci příruba Ø110 tlustá 12.
-- Čtyři díry Ø11 na roztečné kružnici Ø90 v přírubě. Jsou průchozí skrz přírubu, ne skrz celé pouzdro.
-- Vnitřní hrana díry Ø40 má zkosení 1 × 45° na straně příruby.
-- Materiál ocel. Jednotky milimetry. Model 1:1.
-
-> Tip: tloušťka stěny válce je (70 − 40) / 2 = 15 mm. Příruba přesahuje válec o (110 − 70) / 2 = 20 mm.
-
-## Model
-*Pořadí*
-
-- Profil řezu rotujte, nebo složte válec Ø70, válec příruby Ø110 výšky 12 a odečtěte díru Ø40.
-- Čtyři díry Ø11 vzniknou polárním polem válce na poloměru 45 a rozdílem od příruby.
-- Válec díry Ø11 musí být delší než 12 mm, ať je díra průchozí. Do dutiny Ø40 díra Ø11 nezasahuje, leží v materiálu příruby.
-- `_MASSPROP` spočítejte orientačně a porovnejte s ručním odhadem. Řád objemu je statisíce mm³, ne tisíce.
-
-> Tip: rozteč Ø90 je poloměr 45. Díra se středem na poloměru 55 by už ležela mimo přírubu Ø110.
-
-## Výkres
-*A3, 1:1*
-
-- Nárys je řez A-A osou. Půdorys ukáže čtyři díry. Izometrie bez kót je doplněk.
-- Kóty: Ø110, Ø70, Ø40, tloušťka příruby 12, délka 60, 4× Ø11 na Ø90, zkosení 1 × 45°.
-- Hladiny OBRYS, OSA, SRAFA, KOTA, RAM. Šrafa jen v řezu, jeden směr, díry prázdné.
-- Razítko: Přírubové pouzdro, ocel, 1:1, jméno. Tisk A3, Layout, 1:1, monochrome.ctb.
-
-> Tip: evropské promítání. Půdorys pod nárysem. Čtyři díry se kótují jednou.
-
-## Než odevzdáte
-*Kontrola*
-
-- Každá kóta se dá z modelu ověřit. Žádný text není přepsaný.
-- Vypnutá hladina KOTA nechá čistý tvar. Vypnutá SRAFA nechá obrys řezu.
-- PDF se jmenuje prirubove-pouzdro-A3.pdf a rámeček má 420 × 297 mm.
-- Soubor dwg odevzdejte spolu s PDF. Bez modelu výkres nejde opravit.
-
-> Tip: otevřete PDF na celou stránku. Když je razítko useknuté nebo šrafa černá, tisk opakujte. Na obrazovce AutoCADu to vidět nebylo.
-
-
-# Kvíz
-
-## Jaká je tloušťka stěny válce Ø70 s dírou Ø40?
-- 30 mm
-- [x] 15 mm
-- 40 mm
-- 12 mm
-> Rozdíl průměrů je 30 mm na obě strany. Jedna stěna má 15 mm. 12 mm je tloušťka příruby, ne stěny.
-
-## Kam patří čtyři díry Ø11?
-- Skrz celou délku 60
-- [x] Skrz přírubu tlustou 12, na Ø90
-- Do díry Ø40
-- Na osu jako závit M11
-> Leží v materiálu příruby mezi Ø70 a Ø110. Rozteč 90, poloměr 45.
-
-## Jaké měřítko má model i tisk tohoto výkresu?
-- Model 1:2, tisk 1:2
-- [x] Model 1:1, tisk rozvržení 1:1
-- Model 2:1, tisk Fit
-- Měřítko se do razítka nepíše
-> Součást i papír jsou ve skutečných milimetrech. Měřítko pohledu je 1:1, protože se A3 vejde.
-
-## Co odevzdáváte spolu s PDF?
-- Jen izometrii
-- [x] I soubor dwg
-- Rozbitý blok razítka
-- Výkres bez kót, kóty jsou v PDF
-> Z PDF už model neopravíte. DwG je práce, PDF je odevzdaný tisk.
diff --git a/content/technicke-vybaveni/1-rocnik/01-architektura-pocitace.en.md b/content/technicke-vybaveni/1-rocnik/01-architektura-pocitace.en.md
deleted file mode 100644
index 46524d9..0000000
--- a/content/technicke-vybaveni/1-rocnik/01-architektura-pocitace.en.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-nazev: Computer architecture
-popis: 3 slides about von Neumann and the model, then a quiz.
-nazev_cviceni: Quiz: computer architecture
-nadpis_kvizu: Quiz on computer architecture
----
-
-# Překlady
-
-| Česky | Anglicky |
-|---|---|
-| ALU = aritmeticko-logická jednotka | ALU = arithmetic-logic unit |
diff --git a/content/technicke-vybaveni/1-rocnik/01-architektura-pocitace.md b/content/technicke-vybaveni/1-rocnik/01-architektura-pocitace.md
deleted file mode 100644
index 1affea6..0000000
--- a/content/technicke-vybaveni/1-rocnik/01-architektura-pocitace.md
+++ /dev/null
@@ -1,81 +0,0 @@
----
-id: hardware-1-architektura-pocitace
-puvodni: hw.1
-nazev: Architektura počítače
-popis: 3 snímky o Von Neumannovi a schématu a pak kvíz.
-nazev_cviceni: Kvíz: architektura počítače
-nadpis_kvizu: Kvíz k architektuře počítače
----
-
-# Výklad
-
-## Architektura počítače
-*Úvod*
-
-- Architektura počítače je základní model, podle kterého jsou počítače navrhovány.
-- Nejznámější model je Von Neumannovo schéma.
-- Používá se dodnes u většiny běžných počítačů.
-
-> Architektura popisuje, z jakých částí se počítač skládá a jak spolu spolupracují.
-
-## John von Neumann
-*Osobnost*
-
-- John von Neumann byl americký matematik.
-- Pracoval na poli digitálních počítačů a operační teorie kvantové mechaniky.
-- Je autorem Von Neumannovy algebry.
-- Byl tvůrcem teorie her.
-- Podílel se na vývoji atomové a vodíkové bomby.
-
-> Tip: zapamatuj si jméno – podle něj se jmenuje celé schéma.
-
-## Von Neumannovo schéma
-*Schéma*
-
-- Von Neumannovo schéma má pět hlavních částí:
-- ALU – aritmeticko-logická jednotka (výpočty a logika)
-- Řadič – řídí činnost ostatních částí
-- Operační paměť – uchovává program i data
-- Vstupní jednotka – přivádí data do počítače
-- Výstupní jednotka – předává výsledky ven
-
-> ALU = aritmeticko-logická jednotka
-
-
-# Kvíz
-
-## Kdo je autorem Von Neumannova schématu?
-- Alan Turing
-- [x] John von Neumann
-- Bill Gates
-- Steve Jobs
-
-## Čím byl John von Neumann?
-- [x] Americký matematik
-- Německý fyzik
-- Anglický chemik
-- Francouzský lékař
-
-## Co znamená zkratka ALU?
-- Automatická logická síť
-- [x] Aritmeticko-logická jednotka
-- Analogová linková sestava
-- Adresová listová sekce
-
-## Která část Von Neumannova schématu řídí ostatní části?
-- Operační paměť
-- Vstupní jednotka
-- [x] Řadič
-- Výstupní jednotka
-
-## Kde se podle Von Neumannova schématu uchovává program i data?
-- Jen na pevném disku
-- [x] V operační paměti
-- Jen ve vstupní jednotce
-- Jen v ALU
-
-## Čím se John von Neumann také zabýval?
-- Jen malířstvím
-- [x] Teorií her
-- Jen botanikou
-- Jen hudbou
diff --git a/content/technicke-vybaveni/1-rocnik/02-historie-pocitacu.en.md b/content/technicke-vybaveni/1-rocnik/02-historie-pocitacu.en.md
deleted file mode 100644
index 6ec9ca6..0000000
--- a/content/technicke-vybaveni/1-rocnik/02-historie-pocitacu.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: History of computers
-popis: 6 slides about computer generations, then a quiz.
-nazev_cviceni: Quiz: history of computers
-nadpis_kvizu: Quiz on the history of computers
----
diff --git a/content/technicke-vybaveni/1-rocnik/02-historie-pocitacu.md b/content/technicke-vybaveni/1-rocnik/02-historie-pocitacu.md
deleted file mode 100644
index 24cc5c1..0000000
--- a/content/technicke-vybaveni/1-rocnik/02-historie-pocitacu.md
+++ /dev/null
@@ -1,133 +0,0 @@
----
-id: hardware-1-historie-pocitacu
-puvodni: hw.2
-nazev: Historie počítačů
-popis: 6 snímků o generacích počítačů a pak kvíz.
-nazev_cviceni: Kvíz: historie počítačů
-nadpis_kvizu: Kvíz k historii počítačů
----
-
-# Výklad
-
-## Historie počítačů
-*Přehled*
-
-- Předchůdci do 19. století jsou mechanické pomůcky. Pascaline sčítala ozubenými koly, Babbage navrhl analytický stroj a Ada Lovelace k němu popsala postup výpočtu. Elektronika ještě chyběla.
-- Nultá generace, zhruba 1938–1944, spíná elektromagnetická relé. Stroje jsou kusové a často vojenské.
-- První generace, 1944–1955, bere elektronku. Počítač zabere sál a program se dává děrnými štítky.
-- Druhá generace, 1955–1964, nahrazuje elektronku tranzistorem. Objevují se vyšší programovací jazyky.
-- Třetí generace, 1964–1971, staví na integrovaném obvodu. Vznikají rodiny počítačů a operační systémy.
-- Čtvrtá generace od roku 1971 staví na mikroprocesoru. Počítač se vejde na stůl a později do kapsy.
-
-> Generace se dělí podle součástky, která v té době počítala: relé, elektronka, tranzistor, integrovaný obvod a nakonec mikroprocesor.
-
-## Počítače 0. generace (1938–1944)
-*0. generace*
-
-- Relé je elektromagnetický spínač. Je pomalejší a hlučnější než pozdější elektronka, ale umí už skládat logické obvody.
-- Konrad Zuse dokončil v roce 1941 Z3: programovatelný binární stroj s plovoucí řádovou čárkou.
-- Harvard Mark I (1944) byl elektromechanický a počítal balistické tabulky pro americkou armádu.
-- Každý stroj se stavěl na zakázku. Obsluhoval ho tým, ne jeden člověk u stolu.
-- Program býval na děrné pásce nebo na přepínačích, ne v dnešní operační paměti.
-- Typická práce byla vojenská: šifry a tabulky střelby, ne kancelář nebo domácnost.
-
-> Tip: počítačů byly jednotky kusů a často sloužily armádě. Základem je relé, ne elektronka.
-
-## Počítače 1. generace (1944–1955)
-*1. generace*
-
-- ENIAC (1945) měl kolem 18 tisíc elektronek a počítal hlavně balistiku.
-- Von Neumannova architektura ukládá program i data do stejné paměti. Na tom stojí i dnešní počítače.
-- UNIVAC I (1951) byl první sériově prodávaný komerční počítač v USA.
-- Program a data se děrovaly do štítků. Operátor vložil balík a čekal na výsledek.
-- Operační systém ani vyšší jazyk ještě nebyl. Psalo se ve strojovém kódu.
-- Sál potřeboval klimatizaci a stálý dohled. Do domácnosti se takový stroj nevešel.
-
-> Klíčový objev je elektronka. Spíná rychleji než relé, ale hřeje, často se pálí a počítač kvůli ní zabere celý sál.
-
-## Počítače 2. generace (1955–1964)
-*2. generace*
-
-- Tranzistor nahradí elektronku: je menší, spolehlivější a méně se hřeje.
-- Stroj už nemusí zabírat celý sál, pořád je to ale zařízení pro firmu nebo ústav.
-- Vnitřní paměť bývá feritová. Vnější paměť jsou magnetické pásky a bubny.
-- FORTRAN (1957) a COBOL (1959) dovolí psát program blíž lidské řeči než strojový kód.
-- Úlohy se zpracovávají dávkově. Operátor seřadí balíky a počítač je jede postupně.
-- Typické stroje této doby jsou IBM 1401 a IBM 7090.
-
-> Klíčový objev je tranzistor. V Bellových laboratořích vznikl roku 1947 a do počítačů se dostal zhruba od roku 1955.
-
-## Počítače 3. generace (1964–1971)
-*3. generace*
-
-- IBM System/360 (1964) je rodina počítačů se stejným instrukčním souborem. Program z menšího modelu běží i na větším.
-- Operační systém už řídí více úloh a periferie. Objevuje se sdílení času mezi uživateli.
-- Magnetický disk doplňuje pásku a zkracuje čekání na data.
-- Minipočítače jako PDP-8 se vejdou do jedné skříně. Kupují je laboratoře a firmy, ne jen stát.
-- Stroj je menší a levnější než sál první generace, pořád to ale není osobní počítač na stůl.
-- Hlavní vnější pamětí zůstává magnetické médium, ne děrný štítek.
-
-> Klíčový objev je integrovaný obvod: mnoho tranzistorů na jednom křemíkovém čipu. Kilby a Noyce ho představili na konci 50. let.
-
-## Počítače 4. generace
-*4. generace*
-
-- Intel 4004 z roku 1971 je první komerční mikroprocesor. Pozdější čipy z něj udělaly mozek osobního počítače.
-- Na stůl se počítač dostává postupně: Altair 1975, Apple II 1977 a IBM PC 1981.
-- Roku 1972 vychází Pong, první komerčně úspěšná arkádová hra. Lekce ji bere jako první počítačovou hru.
-- Roku 1971 představuje IBM osmipalcovou disketu. Menší formáty přišly až v dalších letech.
-- Roku 1979 Philips a Sony předvádějí kompaktní disk. V obchodech je od roku 1982.
-- Paměti a disky rostou, takže počítač opouští sál a později se vejde i do kapsy.
-
-> Klíčová součástka je programovatelný mikroprocesor: celý procesor na jednom čipu.
-
-
-# Kvíz
-
-## Kdy probíhala nultá generace počítačů?
-- [x] 1938–1944
-- 1944–1955
-- 1955–1964
-- 1964–1971
-
-## Co bylo typické pro počítače 0. generace?
-- Byly v každé domácnosti
-- [x] Jen několik kusů, často vojenský účel
-- Měly mikroprocesory
-- Používaly CD
-
-## Která technologie patří k 1. generaci?
-- Tranzistor
-- [x] Elektronka
-- Integrovaný obvod
-- Mikroprocesor
-
-## Čím se zadávaly data v 1. generaci?
-- USB fleškou
-- [x] Děrnými štítky
-- CD
-- Disketou
-
-## Která technologie patří k 2. generaci?
-- Elektronka
-- [x] Tranzistor
-- Mikroprocesor
-- CD
-
-## Která technologie patří k 3. generaci?
-- Elektronka
-- Tranzistor
-- [x] Integrovaný obvod
-- Disketa
-
-## Co je typické pro 4. generaci?
-- Jen děrné štítky
-- [x] Programovatelné mikroprocesory
-- Jen vojenské použití
-- Žádný software
-
-## Kdy vznikla první počítačová hra (podle lekce)?
-- 1964
-- [x] 1972
-- 1973
-- 1979
diff --git a/content/technicke-vybaveni/1-rocnik/03-zobrazovani-dat-v-pocitaci.en.md b/content/technicke-vybaveni/1-rocnik/03-zobrazovani-dat-v-pocitaci.en.md
deleted file mode 100644
index 07f58d5..0000000
--- a/content/technicke-vybaveni/1-rocnik/03-zobrazovani-dat-v-pocitaci.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Data representation
-popis: 2 slides about bits and bytes, then a quiz.
-nazev_cviceni: Quiz: data representation
-nadpis_kvizu: Quiz on data representation in computers
----
diff --git a/content/technicke-vybaveni/1-rocnik/03-zobrazovani-dat-v-pocitaci.md b/content/technicke-vybaveni/1-rocnik/03-zobrazovani-dat-v-pocitaci.md
deleted file mode 100644
index 93a950f..0000000
--- a/content/technicke-vybaveni/1-rocnik/03-zobrazovani-dat-v-pocitaci.md
+++ /dev/null
@@ -1,55 +0,0 @@
----
-id: hardware-1-zobrazovani-dat-v-pocitaci
-puvodni: hw.3
-nazev: Zobrazování dat v počítači
-popis: 2 snímky o bitu a bytu a pak kvíz.
-nazev_cviceni: Kvíz: zobrazování dat
-nadpis_kvizu: Kvíz k zobrazování dat v počítači
----
-
-# Výklad
-
-## Bit – nejmenší jednotka informace
-*Bit*
-
-- Bit je dvojková číslice – nabývá hodnoty 0 nebo 1.
-- Je to nejmenší zobrazitelná jednotka informace.
-- Z bitů se skládají větší jednotky (např. byte).
-
-> Bit = binary digit (dvojková číslice)
-
-## Byte – adresovatelná jednotka paměti
-*Byte*
-
-- Byte je nejmenší adresovatelná jednotka paměti.
-- Počítač tedy paměť adresuje po bytech, ne po jednotlivých bitech.
-- Jeden byte obvykle obsahuje 8 bitů.
-
-> Tip: 1 B = 8 b
-
-
-# Kvíz
-
-## Co je bit?
-- Největší jednotka paměti
-- [x] Dvojková číslice – nejmenší jednotka informace
-- Jen klávesa na klávesnici
-- Typ procesoru
-
-## Jaké hodnoty může nabývat bit?
-- Jen 0
-- Jen 1
-- [x] 0 nebo 1
-- 0 až 255
-
-## Co je byte?
-- [x] Nejmenší adresovatelná jednotka paměti
-- Nejmenší zobrazitelná jednotka informace
-- Jen síťový kabel
-- Jen grafická karta
-
-## Kolik bitů má obvykle jeden byte?
-- 2
-- 4
-- [x] 8
-- 16
diff --git a/content/technicke-vybaveni/1-rocnik/04-cela-a-desetinna-cisla.en.md b/content/technicke-vybaveni/1-rocnik/04-cela-a-desetinna-cisla.en.md
deleted file mode 100644
index 463c275..0000000
--- a/content/technicke-vybaveni/1-rocnik/04-cela-a-desetinna-cisla.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Integers and reals
-popis: 2 slides about integer and real, then a quiz.
-nazev_cviceni: Quiz: integer and real
-nadpis_kvizu: Quiz on integers and floating-point numbers
----
diff --git a/content/technicke-vybaveni/1-rocnik/04-cela-a-desetinna-cisla.md b/content/technicke-vybaveni/1-rocnik/04-cela-a-desetinna-cisla.md
deleted file mode 100644
index b8a7429..0000000
--- a/content/technicke-vybaveni/1-rocnik/04-cela-a-desetinna-cisla.md
+++ /dev/null
@@ -1,59 +0,0 @@
----
-id: hardware-1-cela-a-desetinna-cisla
-puvodni: hw.4
-nazev: Celá a desetinná čísla
-popis: 2 snímky o integer a real a pak kvíz.
-nazev_cviceni: Kvíz: integer a real
-nadpis_kvizu: Kvíz k celým a desetinným číslům
----
-
-# Výklad
-
-## Proměnná typu integer (celá čísla)
-*Integer*
-
-- Celá čísla se ukládají binárně v pevné řádové čárce.
-- Mají pevný počet bitů – standardně 16 nebo 32 (tedy 2 nebo 4 byty).
-- Při 16bitovém formátu je 2¹⁶ = 65 536 různých hodnot; u nezáporných čísel interval 0 až 2¹⁶ − 1, tedy 0–65 535.
-- Obecně u n bitů je M = 2ⁿ různých hodnot (u 16 bitů je nejvyšší nezáporné číslo 65 535).
-- Ostatní čísla se do tohoto intervalu převádějí transformací (mapováním na přirozená čísla z daného rozsahu).
-
-> Tip: n bitů → 2ⁿ různých hodnot (např. 0 až 2ⁿ − 1)
-
-## Proměnná typu real (desetinná čísla)
-*Real*
-
-- Desetinná čísla se ukládají v pohyblivé řádové čárce ve tvaru mantisa a exponent.
-- Mantisa je normalizovaná tak, že první platná číslice je hned za desetinnou čárkou.
-- Posun řádové čárky vyrovná odpovídající změna exponentu.
-- Standardně zabírá 4 nebo 8 bytů v paměti.
-- Více bytů se používá jen tehdy, když potřebujeme vyšší přesnost výpočtů.
-
-> Tip: tvar = mantisa × základ^exponent
-
-
-# Kvíz
-
-## Jak se ukládají celá čísla (integer)?
-- Jen jako text
-- [x] Binárně v pevné řádové čárce s pevným počtem bitů
-- Jen jako obrázek
-- Jen v pohyblivé řádové čárce bez exponentu
-
-## Kolik různých hodnot má 16bitový nezáporný integer?
-- 256
-- 1024
-- [x] 65 536 (0 až 65 535)
-- 4
-
-## Jak se ukládají desetinná čísla (real)?
-- Jen jako celá čísla bez tečky
-- [x] V pohyblivé řádové čárce jako mantisa a exponent
-- Jen jako 1 bit
-- Jen jako název souboru
-
-## Co znamená normalizace mantisy?
-- Že se číslo smaže
-- [x] Že první platná číslice je hned za desetinnou čárkou a exponent se upraví
-- Že se použije jen 1 bit
-- Že se číslo uloží jako text
diff --git a/content/technicke-vybaveni/1-rocnik/05-analogovy-a-digitalni-zvuk.en.md b/content/technicke-vybaveni/1-rocnik/05-analogovy-a-digitalni-zvuk.en.md
deleted file mode 100644
index 10c9f99..0000000
--- a/content/technicke-vybaveni/1-rocnik/05-analogovy-a-digitalni-zvuk.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Analog and digital sound
-popis: 2 slides about turning sound into numbers, then a quiz.
-nazev_cviceni: Quiz: analog and digital
-nadpis_kvizu: Quiz on analog and digital sound
----
diff --git a/content/technicke-vybaveni/1-rocnik/05-analogovy-a-digitalni-zvuk.md b/content/technicke-vybaveni/1-rocnik/05-analogovy-a-digitalni-zvuk.md
deleted file mode 100644
index 9598cbb..0000000
--- a/content/technicke-vybaveni/1-rocnik/05-analogovy-a-digitalni-zvuk.md
+++ /dev/null
@@ -1,58 +0,0 @@
----
-id: hardware-1-analogovy-a-digitalni-zvuk
-puvodni: hw.5
-nazev: Analogový a digitální zvuk
-popis: 2 snímky o převodu zvuku na čísla a pak kvíz.
-nazev_cviceni: Kvíz: analog a digitál
-nadpis_kvizu: Kvíz k analogovému a digitálnímu zvuku
----
-
-# Výklad
-
-## Analogový a digitální zvuk
-*Signál*
-
-- Analogový zvuk je spojitý signál – v každém okamžiku má nějakou hodnotu.
-- Digitální zvuk je posloupnost čísel, která ten signál popisují.
-- Z mikrofonu vychází analogové napětí, do reproduktoru musí zase jít spojitý signál.
-- Počítač umí uložit jen čísla, proto se zvuk digitalizuje.
-
-> Tip: analog je spojitý, digitál je posloupnost čísel.
-
-## A/D a D/A převodník
-*Převod*
-
-- A/D převodník (ADC) převede analogový signál na čísla.
-- D/A převodník (DAC) z čísel znovu složí spojitý signál.
-- Digitalizace má dva kroky: vzorkování a kvantování.
-- Vzorkování určí, kdy se hodnota změří.
-- Kvantování určí, na kolik hladin se amplituda zaokrouhlí.
-
-> Tip: ADC dovnitř, DAC ven. Digitalizace = vzorkování + kvantování.
-
-
-# Kvíz
-
-## Co je analogový zvuk?
-- Posloupnost celých čísel
-- [x] Spojitý signál s hodnotou v každém okamžiku
-- Jen název souboru WAV
-- Jen bit v paměti
-
-## Co dělá A/D převodník (ADC)?
-- [x] Převádí analogový signál na čísla
-- Zesiluje jen reproduktor
-- Maže vysoké tóny po uložení
-- Počítá jen velikost souboru
-
-## Jaké jsou dva hlavní kroky digitalizace zvuku?
-- Komprese a tisk
-- [x] Vzorkování a kvantování
-- Šifrování a záloha
-- Násobení a dělení
-
-## Co dělá D/A převodník (DAC)?
-- [x] Z čísel znovu skládá spojitý signál
-- Měří jen vzorkovací frekvenci
-- Ukládá zvuk jako text
-- Zvyšuje počet bitů v souboru
diff --git a/content/technicke-vybaveni/1-rocnik/06-vzorkovani.en.md b/content/technicke-vybaveni/1-rocnik/06-vzorkovani.en.md
deleted file mode 100644
index d5c4f7e..0000000
--- a/content/technicke-vybaveni/1-rocnik/06-vzorkovani.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Sampling
-popis: 2 slides about sample rate and Nyquist, then a quiz.
-nazev_cviceni: Quiz: sampling
-nadpis_kvizu: Quiz on sampling sound
----
diff --git a/content/technicke-vybaveni/1-rocnik/06-vzorkovani.md b/content/technicke-vybaveni/1-rocnik/06-vzorkovani.md
deleted file mode 100644
index d6e790e..0000000
--- a/content/technicke-vybaveni/1-rocnik/06-vzorkovani.md
+++ /dev/null
@@ -1,58 +0,0 @@
----
-id: hardware-1-vzorkovani
-puvodni: hw.6
-nazev: Vzorkování
-popis: 2 snímky o vzorkovací frekvenci a Nyquistovi a pak kvíz.
-nazev_cviceni: Kvíz: vzorkování
-nadpis_kvizu: Kvíz ke vzorkování zvuku
----
-
-# Výklad
-
-## Vzorkování
-*Vzorky*
-
-- Vzorkování měří velikost signálu v pravidelných okamžicích.
-- Mezi vzorky se hodnota neukládá.
-- Vzorkovací frekvence fs je počet vzorků za sekundu.
-- Jednotka je hertz (Hz). 1 kHz = 1000 vzorků za sekundu.
-- Vyšší fs zachytí rychlejší změny, tedy vyšší tóny.
-
-> Tip: vzorkovací frekvence fs = počet vzorků za sekundu (Hz).
-
-## Nyquistův–Shannonův teorém
-*Nyquist*
-
-- Pro věrnou rekonstrukci musí být fs vyšší než dvojnásobek nejvyšší frekvence ve signálu.
-- Nyquistova frekvence je fs / 2 – vyšší tón už záznam neunese.
-- Člověk slyší zhruba do 20 kHz, dvojnásobek je 40 kHz.
-- CD proto používá 44,1 kHz: o něco víc než 40 kHz, aby zbyl prostor pro filtr.
-
-> Tip: fs musí být vyšší než 2 × fmax. Nyquistova frekvence je fs / 2.
-
-
-# Kvíz
-
-## Co je vzorkovací frekvence?
-- Počet bitů v jednom vzorku
-- [x] Počet vzorků za sekundu
-- Délka skladby v minutách
-- Počet kanálů (mono nebo stereo)
-
-## V jakých jednotkách se udává vzorkovací frekvence?
-- V bytech
-- [x] V hertzech
-- Ve voltech
-- V pixelech
-
-## Co říká Nyquistův–Shannonův teorém?
-- [x] fs musí být vyšší než dvojnásobek nejvyšší frekvence
-- fs musí být vždy 8 kHz
-- Stačí jeden vzorek na celou skladbu
-- Bitová hloubka musí být 1
-
-## Proč má audio CD vzorkovací frekvenci 44,1 kHz?
-- Protože byte má 8 bitů
-- [x] Sluch sahá asi do 20 kHz a fs musí být vyšší než dvojnásobek
-- Protože stereo má dva kanály
-- Protože minutu tvoří 60 sekund
diff --git a/content/technicke-vybaveni/1-rocnik/07-kvantovani.en.md b/content/technicke-vybaveni/1-rocnik/07-kvantovani.en.md
deleted file mode 100644
index 0c7a7db..0000000
--- a/content/technicke-vybaveni/1-rocnik/07-kvantovani.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Quantization
-popis: 2 slides about bit depth and quantization error, then a quiz.
-nazev_cviceni: Quiz: quantization
-nadpis_kvizu: Quiz on quantizing sound
----
diff --git a/content/technicke-vybaveni/1-rocnik/07-kvantovani.md b/content/technicke-vybaveni/1-rocnik/07-kvantovani.md
deleted file mode 100644
index 1661378..0000000
--- a/content/technicke-vybaveni/1-rocnik/07-kvantovani.md
+++ /dev/null
@@ -1,58 +0,0 @@
----
-id: hardware-1-kvantovani
-puvodni: hw.7
-nazev: Kvantování
-popis: 2 snímky o bitové hloubce a kvantizační chybě a pak kvíz.
-nazev_cviceni: Kvíz: kvantování
-nadpis_kvizu: Kvíz ke kvantování zvuku
----
-
-# Výklad
-
-## Kvantování
-*Hladiny*
-
-- Kvantování zaokrouhlí amplitudu každého vzorku na nejbližší z konečného počtu hladin.
-- Počet hladin je 2ⁿ, kde n je bitová hloubka (počet bitů na vzorek).
-- 8 bitů dává 256 hladin, 16 bitů dává 65 536 hladin.
-- Vzorkování říká jak často, kvantování jak přesně.
-
-> Tip: n bitů → 2ⁿ hladin. 8 bitů = 256, 16 bitů = 65 536.
-
-## Kvantizační chyba
-*Chyba*
-
-- Kvantizační chyba je rozdíl mezi skutečnou hodnotou a zvolenou hladinou.
-- V záznamu se projeví jako kvantizační šum.
-- Větší bitová hloubka hladiny zjemní a šum zmenší.
-- Audio CD používá 16 bitů na vzorek, studio často 24 bitů.
-- Vyšší hloubka také zvětší datový tok.
-
-> Tip: víc bitů = jemnější hladiny, menší šum a větší soubor.
-
-
-# Kvíz
-
-## Co je kvantování?
-- Měření signálu v pravidelných okamžicích
-- [x] Zaokrouhlení amplitudy na konečný počet hladin
-- Převod čísel zpět na analog
-- Komprese souboru MP3
-
-## Kolik hladin má 8bitové kvantování?
-- 8
-- 16
-- [x] 256
-- 65 536
-
-## Kolik hladin má 16bitové kvantování?
-- 16
-- 256
-- 1 024
-- [x] 65 536
-
-## Co je kvantizační chyba?
-- [x] Rozdíl mezi skutečnou hodnotou a zvolenou hladinou
-- Příliš nízká vzorkovací frekvence
-- Počet kanálů ve stereu
-- Délka skladby v sekundách
diff --git a/content/technicke-vybaveni/1-rocnik/08-pcm-a-datovy-tok.en.md b/content/technicke-vybaveni/1-rocnik/08-pcm-a-datovy-tok.en.md
deleted file mode 100644
index 80400cb..0000000
--- a/content/technicke-vybaveni/1-rocnik/08-pcm-a-datovy-tok.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: PCM and data rate
-popis: 2 slides about PCM and recording size, then a quiz.
-nazev_cviceni: Quiz: PCM and data rate
-nadpis_kvizu: Quiz on PCM and data rate
----
diff --git a/content/technicke-vybaveni/1-rocnik/08-pcm-a-datovy-tok.md b/content/technicke-vybaveni/1-rocnik/08-pcm-a-datovy-tok.md
deleted file mode 100644
index ad1af28..0000000
--- a/content/technicke-vybaveni/1-rocnik/08-pcm-a-datovy-tok.md
+++ /dev/null
@@ -1,58 +0,0 @@
----
-id: hardware-1-pcm-a-datovy-tok
-puvodni: hw.8
-nazev: PCM a datový tok
-popis: 2 snímky o PCM a velikosti záznamu a pak kvíz.
-nazev_cviceni: Kvíz: PCM a datový tok
-nadpis_kvizu: Kvíz k PCM a datovému toku
----
-
-# Výklad
-
-## Pulzně kódová modulace
-*PCM*
-
-- PCM uloží každý vzorek jako binární číslo.
-- Je to základní nekomprimovaný záznam (WAV, audio CD).
-- Datový tok = vzorkovací frekvence × bitová hloubka × počet kanálů.
-- Mono má jeden kanál, stereo dva (levý a pravý).
-
-> Tip: tok = fs × bity na vzorek × počet kanálů.
-
-## Kolik dat zabere CD
-*Velikost*
-
-- CD stereo: 44 100 Hz × 16 bit × 2 = 1 411 200 bit/s.
-- To je 176 400 B/s.
-- Za minutu je to 10 584 000 B, tedy přes 10 MB.
-- Delší záznam, vyšší fs, více bitů nebo více kanálů soubor zvětší.
-- MP3 a AAC soubor zmenší za cenu ztráty části informace.
-
-> Tip: komprese (MP3, AAC) soubor zmenší, ale část informace zahodí.
-
-
-# Kvíz
-
-## Co je PCM?
-- [x] Každý vzorek uložený jako binární číslo
-- Jen komprimovaný formát MP3
-- Filtr před mikrofonem
-- Jednotka hlasitosti
-
-## Jak se spočítá datový tok nekomprimovaného zvuku?
-- fs + bity + kanály
-- [x] fs × bitová hloubka × počet kanálů
-- jen délka skladby v minutách
-- počet hladin děleno dvěma
-
-## Jaký je datový tok audio CD ve stereu?
-- 44 100 bit/s
-- 256 bit/s
-- [x] 1 411 200 bit/s
-- 8 bit/s
-
-## Co udělá komprese MP3 se záznamem?
-- Zvětší soubor a přidá vzorky
-- [x] Zmenší soubor a část informace zahodí
-- Změní jen název souboru
-- Převede digitál zpět na analog
diff --git a/content/technicke-vybaveni/1-rocnik/09-aliasing-a-parametry.en.md b/content/technicke-vybaveni/1-rocnik/09-aliasing-a-parametry.en.md
deleted file mode 100644
index 3799bdc..0000000
--- a/content/technicke-vybaveni/1-rocnik/09-aliasing-a-parametry.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Aliasing and formats
-popis: 2 slides about aliasing and common formats, then a quiz.
-nazev_cviceni: Quiz: aliasing and formats
-nadpis_kvizu: Quiz on aliasing and recording formats
----
diff --git a/content/technicke-vybaveni/1-rocnik/09-aliasing-a-parametry.md b/content/technicke-vybaveni/1-rocnik/09-aliasing-a-parametry.md
deleted file mode 100644
index 3bd9364..0000000
--- a/content/technicke-vybaveni/1-rocnik/09-aliasing-a-parametry.md
+++ /dev/null
@@ -1,57 +0,0 @@
----
-id: hardware-1-aliasing-a-parametry
-puvodni: hw.9
-nazev: Aliasing a parametry
-popis: 2 snímky o aliasingu a běžných formátech a pak kvíz.
-nazev_cviceni: Kvíz: aliasing a parametry
-nadpis_kvizu: Kvíz k aliasingu a parametrům záznamu
----
-
-# Výklad
-
-## Když je vzorkování moc řídké
-*Aliasing*
-
-- Aliasing vznikne, když je fs příliš nízká a poruší se Nyquistovo pravidlo.
-- Vysoká frekvence se v záznamu jeví jako nižší tón.
-- Příklad: tón 6 kHz vzorkovaný 8 kHz se jeví jako 2 kHz.
-- Antialiasingový filtr před převodníkem vysoké frekvence ořízne.
-
-> Tip: aliasing vznikne před uložením a z hotových vzorků už nejde spolehlivě odstranit.
-
-## Běžné parametry záznamu
-*Formáty*
-
-- Audio CD: 44,1 kHz, 16 bitů, stereo.
-- Klasický telefonní hovor: 8 kHz a 8 bitů – řeč se vejde do pásma do 4 kHz.
-- Studio často používá 48 nebo 96 kHz a 24 bitů.
-- Parametry se volí podle toho, co má záznam unést a jak velký smí být.
-
-> Tip: vyšší fs a víc bitů znamená věrnější záznam a větší tok.
-
-
-# Kvíz
-
-## Kdy vzniká aliasing?
-- [x] Když je vzorkovací frekvence příliš nízká
-- Když má záznam 24 bitů
-- Když je soubor ve stereu
-- Když se použije D/A převodník
-
-## K čemu je antialiasingový filtr?
-- Zvětší bitovou hloubku po uložení
-- [x] Před vzorkováním ořízne frekvence nad Nyquistovu
-- Spočítá datový tok
-- Převede mono na stereo
-
-## Jaké parametry má audio CD?
-- 8 kHz, 8 bitů, mono
-- [x] 44,1 kHz, 16 bitů, stereo
-- 96 kHz, 8 bitů, mono
-- 1 kHz, 1 bit, stereo
-
-## Proč telefonnímu hovoru stačí vzorkování 8 kHz?
-- [x] Řeč se vejde do pásma zhruba do 4 kHz
-- Telefon nahrává na audio CD
-- 8 kHz je totéž co 44,1 kHz
-- Řeč nemá žádnou frekvenci
diff --git a/content/technicke-vybaveni/1-rocnik/10-pocitacova-skrin.en.md b/content/technicke-vybaveni/1-rocnik/10-pocitacova-skrin.en.md
deleted file mode 100644
index 7ae9d76..0000000
--- a/content/technicke-vybaveni/1-rocnik/10-pocitacova-skrin.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Computer case
-popis: 2 slides about the case, airflow and types, then a quiz.
-nazev_cviceni: Quiz: computer case
-nadpis_kvizu: Quiz on the computer case
----
diff --git a/content/technicke-vybaveni/1-rocnik/10-pocitacova-skrin.md b/content/technicke-vybaveni/1-rocnik/10-pocitacova-skrin.md
deleted file mode 100644
index 916dd1c..0000000
--- a/content/technicke-vybaveni/1-rocnik/10-pocitacova-skrin.md
+++ /dev/null
@@ -1,59 +0,0 @@
----
-id: hardware-1-pocitacova-skrin
-puvodni: hw.10
-nazev: Počítačová skříň
-popis: 2 snímky o skříni, airflow a typech a pak kvíz.
-nazev_cviceni: Kvíz: počítačová skříň
-nadpis_kvizu: Kvíz k počítačové skříni
----
-
-# Výklad
-
-## Počítačová skříň
-*Účel*
-
-- Skříň zastřešuje a drží ostatní komponenty.
-- Součásti se propojují kabely nebo přímo do základní desky.
-- Při provozu vzniká teplo a skříň ho musí odvést.
-- Uzavřená skříň s mřížkami udržuje správné proudění vzduchu (airflow).
-
-> Tip: skříň má být uzavřená, ale s větracími mřížkami.
-
-## Přední a zadní stěna, desktop a tower
-*Stěny*
-
-- Přední stěna: zapnutí, restart, zelená kontrolka chodu a červená kontrolka disku.
-- Vpředu bývají i USB porty a audio vstupy a výstupy.
-- Zadní stěna zpřístupňuje porty desky a přídavných karet.
-- Volné otvory kryjí záslepky, aby se dovnitř neprášilo.
-- Desktop je skříň naležato, monitor se na ni pokládá.
-- Tower stojí nastojato vedle monitoru nebo pod stolem.
-
-> Tip: desktop leží, tower stojí.
-
-
-# Kvíz
-
-## K čemu slouží počítačová skříň?
-- Jen jako ozdoba monitoru
-- [x] Zastřešuje a uchycuje ostatní komponenty
-- Převádí 230 V na 12 V
-- Ukládá operační systém
-
-## Proč má být skříň uzavřená a mít větrací mřížky?
-- Aby se dovnitř vešel jen jeden disk
-- [x] Aby uvnitř správně proudil vzduch a odvádělo se teplo
-- Aby nešel zapnout restart
-- Aby odpadly kabely
-
-## Co typicky najdeme na přední stěně?
-- [x] Tlačítka, kontrolky, USB a audio
-- Jen patici procesoru
-- Jen sloty ISA
-- Jen plotny pevného disku
-
-## Jak se liší desktop a tower?
-- Desktop je bez zdroje, tower bez desky
-- Desktop je jen pro notebooky
-- [x] Desktop leží, tower stojí
-- Liší se jen barvou kontrolek
diff --git a/content/technicke-vybaveni/1-rocnik/11-napajeci-zdroj.en.md b/content/technicke-vybaveni/1-rocnik/11-napajeci-zdroj.en.md
deleted file mode 100644
index b294df1..0000000
--- a/content/technicke-vybaveni/1-rocnik/11-napajeci-zdroj.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Power supply
-popis: 2 slides about the ATX supply and power draw, then a quiz.
-nazev_cviceni: Quiz: power supply
-nadpis_kvizu: Quiz on the power supply
----
diff --git a/content/technicke-vybaveni/1-rocnik/11-napajeci-zdroj.md b/content/technicke-vybaveni/1-rocnik/11-napajeci-zdroj.md
deleted file mode 100644
index 1351169..0000000
--- a/content/technicke-vybaveni/1-rocnik/11-napajeci-zdroj.md
+++ /dev/null
@@ -1,58 +0,0 @@
----
-id: hardware-1-napajeci-zdroj
-puvodni: hw.11
-nazev: Napájecí zdroj
-popis: 2 snímky o zdroji ATX a spotřebě a pak kvíz.
-nazev_cviceni: Kvíz: napájecí zdroj
-nadpis_kvizu: Kvíz k napájecímu zdroji
----
-
-# Výklad
-
-## Napájecí zdroj
-*Převod*
-
-- Zdroj zásobuje všechny části počítače elektřinou.
-- Převádí střídavé napětí 230 V, 50 Hz na stejnosměrné 3,3 V, 5 V a 12 V.
-- Nejrozšířenější je standard ATX, včetně variant TFX a SFX.
-- ATX umí počítač vypnout softwarově přes základní desku.
-- Výkon se vyrábí v řadě hodnot, třeba od 350 W po 700 W i víc.
-
-> Tip: ze sítě jde střídavých 230 V / 50 Hz.
-
-## Co se napájí přímo a kolik to bere
-*Přívody*
-
-- Přímo ze zdroje: deska, pevné disky, mechaniky, aktivní chladiče a grafické karty.
-- Přes desku: rozšiřující karty, některé ventilátory, procesor, porty a LED kontrolky.
-- V klidu bere sestava zhruba 100 W.
-- V plné zátěži může spotřeba dosáhnout 500–600 W.
-
-> Tip: spotřeba není stálá.
-
-
-# Kvíz
-
-## Co zdroj udělá se síťovým napětím?
-- Nechá 230 V střídavých všude v počítači
-- [x] Převede 230 V střídavých na stejnosměrné 3,3 V, 5 V a 12 V
-- Převede všechno na 230 V stejnosměrných
-- Vyrábí jen 1,5 V pro USB
-
-## Co umožňuje standard ATX?
-- [x] Softwarové vypnutí počítače přes základní desku
-- Jen napájení disketové mechaniky
-- Zrušení větracích mřížek
-- Uložení BIOSu na pásku
-
-## Co se ze zdroje napájí přímo?
-- Jen LED na klávesnici
-- Jen porty na zadní stěně
-- [x] Deska, disky, mechaniky, aktivní chladiče a grafické karty
-- Jen operační paměť
-
-## Jak se mění spotřeba počítače?
-- Je pořád přesně 350 W
-- [x] V klidu asi 100 W, v plné zátěži 500–600 W
-- V klidu 500 W a v zátěži 100 W
-- Nezávisí na tom, co počítač dělá
diff --git a/content/technicke-vybaveni/1-rocnik/12-zakladni-deska.en.md b/content/technicke-vybaveni/1-rocnik/12-zakladni-deska.en.md
deleted file mode 100644
index 8001167..0000000
--- a/content/technicke-vybaveni/1-rocnik/12-zakladni-deska.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Motherboard
-popis: 2 slides about the board, chipset and BIOS, then a quiz.
-nazev_cviceni: Quiz: motherboard
-nadpis_kvizu: Quiz on the motherboard
----
diff --git a/content/technicke-vybaveni/1-rocnik/12-zakladni-deska.md b/content/technicke-vybaveni/1-rocnik/12-zakladni-deska.md
deleted file mode 100644
index 5971c4b..0000000
--- a/content/technicke-vybaveni/1-rocnik/12-zakladni-deska.md
+++ /dev/null
@@ -1,57 +0,0 @@
----
-id: hardware-1-zakladni-deska
-puvodni: hw.12
-nazev: Základní deska
-popis: 2 snímky o desce, čipsetu a BIOSu a pak kvíz.
-nazev_cviceni: Kvíz: základní deska
-nadpis_kvizu: Kvíz k základní desce
----
-
-# Výklad
-
-## Základní deska
-*Páteř*
-
-- Je to vícevrstvý plošný spoj, fundament a páteř počítače.
-- Propojuje procesor se všemi ostatními částmi.
-- Čipset tvoří dva čipy: severní a jižní most.
-- Sběrnice jsou měděné cestičky pro data a signály.
-
-> Tip: deska rozhoduje o kompatibilitě a o tom, co půjde upgradovat.
-
-## Patice, mosty a BIOS
-*Čipy*
-
-- Patice (socket) mechanicky drží procesor a elektricky ho propojí.
-- Severní most je systémový řadič blízko CPU a řeší rychlé přesuny: FSB, AGP a paměťovou sběrnici.
-- Jižní most připojuje pomalejší periferie, BIOS, disky, USB a porty.
-- BIOS je mezistupeň mezi hardwarem a softwarem a nese instrukce pro zavedení operačního systému.
-
-> Tip: nastavení BIOSu drží čip CMOS na baterii.
-
-
-# Kvíz
-
-## Co je základní deska?
-- [x] Vícevrstvý plošný spoj, páteř počítače
-- Jen kovový kryt zdroje
-- Jen ventilátor na procesoru
-- Externí disk
-
-## Z čeho se skládá čipset?
-- Ze tří grafických karet
-- [x] Ze severního a jižního mostu
-- Jen z baterie CMOS
-- Jen ze slotu ISA
-
-## Co řeší severní most?
-- Jen pomalé USB porty
-- Jen zvukový výstup
-- [x] Rychlé přesuny dat: FSB, AGP a paměťovou sběrnici
-- Jen tlačítko reset
-
-## Kde je uložené nastavení BIOSu?
-- [x] V čipu CMOS napájeném baterií
-- Jen na papírovém štítku skříně
-- V mechanice CD
-- V červeném vodiči zdroje
diff --git a/content/technicke-vybaveni/1-rocnik/13-konektory-a-sloty.en.md b/content/technicke-vybaveni/1-rocnik/13-konektory-a-sloty.en.md
deleted file mode 100644
index 4d83851..0000000
--- a/content/technicke-vybaveni/1-rocnik/13-konektory-a-sloty.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Connectors and slots
-popis: 2 slides about power, SATA and slots, then a quiz.
-nazev_cviceni: Quiz: connectors and slots
-nadpis_kvizu: Quiz on connectors and slots
----
diff --git a/content/technicke-vybaveni/1-rocnik/13-konektory-a-sloty.md b/content/technicke-vybaveni/1-rocnik/13-konektory-a-sloty.md
deleted file mode 100644
index 70248ec..0000000
--- a/content/technicke-vybaveni/1-rocnik/13-konektory-a-sloty.md
+++ /dev/null
@@ -1,57 +0,0 @@
----
-id: hardware-1-konektory-a-sloty
-puvodni: hw.13
-nazev: Konektory a sloty
-popis: 2 snímky o napájení, SATA a slotech a pak kvíz.
-nazev_cviceni: Kvíz: konektory a sloty
-nadpis_kvizu: Kvíz ke konektorům a slotům
----
-
-# Výklad
-
-## Napájení a datové kabely
-*Konektory*
-
-- Deska bere 24pinový napájecí konektor a procesor zvlášť 4pinový nebo 8pinový.
-- Disky a mechaniky se připojují přes SATA.
-- Starší rozhraní jsou IDE a FDD.
-- F_PANEL připojuje Power, Reset, LED diody a PC speaker.
-
-> Tip: F_PANEL je svazek od předního panelu skříně.
-
-## ISA, PCI, AGP a PCIe
-*Sloty*
-
-- ISA je zastaralý 16bitový slot.
-- PCI je nezávislé na CPU, běží na 33 MHz a má propustnost 264 MB/s.
-- AGP vzniklo pro grafické karty a má přímé spojení s RAM.
-- PCI Express je sériové, vysokorychlostní a umí Hot Plug.
-
-> Tip: dnešní rychlé rozhraní je PCI Express.
-
-
-# Kvíz
-
-## Jakými konektory se deska napájí?
-- Jen jedním USB
-- [x] 24 piny pro desku a 4 nebo 8 piny pro CPU
-- Jen konektorem FDD
-- Jen slotem ISA
-
-## Co se připojuje na F_PANEL?
-- [x] Power, Reset, LED a PC speaker
-- Jen pevný disk
-- Jen monitor
-- Jen anténa Wi-Fi
-
-## Jaké má parametry sběrnice PCI?
-- Sériová, jen pro tiskárny
-- 16bitová a dnes jediná používaná
-- [x] Nezávislá na CPU, 33 MHz, 264 MB/s
-- Jen pro procesor
-
-## K čemu vzniklo AGP?
-- Pro disketové mechaniky
-- [x] Speciálně pro grafické karty, s přímým spojením k RAM
-- Jako náhrada napájecího zdroje
-- Pro pomalé tiskárny
diff --git a/content/technicke-vybaveni/1-rocnik/14-operacni-pameti.en.md b/content/technicke-vybaveni/1-rocnik/14-operacni-pameti.en.md
deleted file mode 100644
index 9226cc6..0000000
--- a/content/technicke-vybaveni/1-rocnik/14-operacni-pameti.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Memory
-popis: 2 slides about RAM, SRAM and DRAM, then a quiz.
-nazev_cviceni: Quiz: memory
-nadpis_kvizu: Quiz on memory
----
diff --git a/content/technicke-vybaveni/1-rocnik/14-operacni-pameti.md b/content/technicke-vybaveni/1-rocnik/14-operacni-pameti.md
deleted file mode 100644
index 4dfd967..0000000
--- a/content/technicke-vybaveni/1-rocnik/14-operacni-pameti.md
+++ /dev/null
@@ -1,57 +0,0 @@
----
-id: hardware-1-operacni-pameti
-puvodni: hw.14
-nazev: Operační paměti
-popis: 2 snímky o RAM, SRAM a DRAM a pak kvíz.
-nazev_cviceni: Kvíz: paměti
-nadpis_kvizu: Kvíz k operačním pamětím
----
-
-# Výklad
-
-## Vnitřní a vnější paměť
-*Dělení*
-
-- Vnitřní paměť je operační RAM: rychlá, pro rozpracovaná data, bez napájení se smaže.
-- Vnější paměť jsou disky, USB a optická média.
-- Vnější paměť data udrží i bez napájení.
-- Je ale mnohem pomalejší než RAM.
-
-> Tip: RAM je rychlá, ale bez napájení data ztratí.
-
-## SRAM a DRAM
-*Typy*
-
-- SRAM (statická) je velmi rychlá a drahá.
-- Používá se jako cache v procesoru.
-- DRAM (dynamická) je levnější a data se musí neustále obnovovat.
-- Patří sem SDR a řada DDR až po DDR3.
-
-> Tip: SRAM je cache v procesoru, DRAM se musí obnovovat.
-
-
-# Kvíz
-
-## K čemu slouží operační RAM?
-- [x] Pro rozpracovaná data; je rychlá a potřebuje napájení
-- Pro trvalé uložení filmů bez elektřiny
-- Jen jako náhrada zdroje
-- Jen pro větrání skříně
-
-## Čím se vnější paměť liší od RAM?
-- Je rychlejší a maže se při vypnutí
-- [x] Drží data i bez napájení, ale je pomalejší
-- Je vždy jen v procesoru
-- Nepotřebuje žádný řadič
-
-## Kde se používá SRAM?
-- Jako jediný pevný disk
-- Jen v optické mechanice
-- [x] Jako rychlá a drahá cache v procesoru
-- Jen na předním panelu
-
-## Co platí o DRAM?
-- [x] Je levnější a data se musí obnovovat (SDR, DDR až DDR3)
-- Nikdy se neobnovuje a je dražší než SRAM
-- Slouží jen jako baterie CMOS
-- Je to jiný název pro zdroj ATX
diff --git a/content/technicke-vybaveni/1-rocnik/15-procesor.en.md b/content/technicke-vybaveni/1-rocnik/15-procesor.en.md
deleted file mode 100644
index 053237a..0000000
--- a/content/technicke-vybaveni/1-rocnik/15-procesor.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Processor
-popis: 3 slides about the CPU, cores and cooling, then a quiz.
-nazev_cviceni: Quiz: processor
-nadpis_kvizu: Quiz on the processor
----
diff --git a/content/technicke-vybaveni/1-rocnik/15-procesor.md b/content/technicke-vybaveni/1-rocnik/15-procesor.md
deleted file mode 100644
index d7ad482..0000000
--- a/content/technicke-vybaveni/1-rocnik/15-procesor.md
+++ /dev/null
@@ -1,68 +0,0 @@
----
-id: hardware-1-procesor
-puvodni: hw.15
-nazev: Procesor
-popis: 3 snímky o CPU, jádrech a chlazení a pak kvíz.
-nazev_cviceni: Kvíz: procesor
-nadpis_kvizu: Kvíz k procesoru
----
-
-# Výklad
-
-## Procesor
-*Mozek*
-
-- CPU je hlavní výpočetní jednotka a řídí ostatní části.
-- Je to křemíkový čip z miliard tranzistorů, které pracují jako spínače.
-- Frekvence je počet cyklů za sekundu (Hz, v praxi GHz).
-- Takt vznikne jako součin base clock (100 MHz) a násobiče.
-
-> Tip: takt = základní hodiny 100 MHz × násobič.
-
-## Více jader, vlákna a cache
-*Jádra*
-
-- Více jader jsou samostatné jednotky pro souběžnou práci.
-- Hyper-Threading / SMT nechá jedno jádro zpracovat dvě vlákna najednou.
-- Cache je vyrovnávací paměť mezi rychlým jádrem a pomalejšími součástmi.
-- Bývá ve třech úrovních: L1, L2 a L3.
-
-> Tip: cache L1, L2 a L3 zmenšují čekání na pomalejší paměť.
-
-## Chlazení procesoru
-*Teplo*
-
-- Chladič odvádí teplo z čipu.
-- Vzduchové chlazení je pasivní blok a ventilátor.
-- Je levné a spolehlivé.
-- Vodní all-in-one vede teplo kapalinou do radiátoru.
-- U procesoru tak zabere méně místa.
-
-> Tip: chlazení prodlužuje životnost a dovolí přetaktování.
-
-
-# Kvíz
-
-## Jak vzniká takt procesoru?
-- Jen z napětí 12 V
-- [x] Jako součin base clock 100 MHz a násobiče
-- Jen z počtu USB portů
-- Z rychlosti ventilátoru ve skříni
-
-## Co umí Hyper-Threading / SMT?
-- [x] Jedno jádro zpracuje dvě vlákna současně
-- Vypne chlazení
-- Zdvojnásobí napětí zdroje
-- Nahradí operační paměť diskem
-
-## K čemu je cache L1, L2 a L3?
-- K napájení grafické karty
-- K uložení BIOSu na baterii
-- [x] Vyrovnává rychlostní rozdíl mezi procesorem a ostatními částmi
-- K větrání zadní stěny
-
-## Čím se vyznačuje vzduchové chlazení CPU?
-- Jen kapalinou bez ventilátoru
-- [x] Pasivním blokem a ventilátorem; je levné a spolehlivé
-- Tím, že procesor nehřeje
-- Tím, že se montuje jen na disk
diff --git a/content/technicke-vybaveni/1-rocnik/16-pridavne-karty.en.md b/content/technicke-vybaveni/1-rocnik/16-pridavne-karty.en.md
deleted file mode 100644
index 153f921..0000000
--- a/content/technicke-vybaveni/1-rocnik/16-pridavne-karty.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Expansion cards
-popis: 2 slides about cards and PCIe slots, then a quiz.
-nazev_cviceni: Quiz: expansion cards
-nadpis_kvizu: Quiz on expansion cards
----
diff --git a/content/technicke-vybaveni/1-rocnik/16-pridavne-karty.md b/content/technicke-vybaveni/1-rocnik/16-pridavne-karty.md
deleted file mode 100644
index c5d6264..0000000
--- a/content/technicke-vybaveni/1-rocnik/16-pridavne-karty.md
+++ /dev/null
@@ -1,55 +0,0 @@
----
-id: hardware-1-pridavne-karty
-puvodni: hw.16
-nazev: Přídavné karty
-popis: 2 snímky o kartách a slotech PCIe a pak kvíz.
-nazev_cviceni: Kvíz: přídavné karty
-nadpis_kvizu: Kvíz k přídavným kartám
----
-
-# Výklad
-
-## Přídavné karty
-*Karty*
-
-- Rozšiřující karta vylepší nebo rozšíří to, co počítač umí.
-- Typické jsou grafické, zvukové, síťové a televizní karty.
-- Sloty se vyvíjely, proto karta obvykle nejde zasunout do libovolného slotu.
-
-> Tip: karta je plošný spoj, který počítači přidá schopnost.
-
-## Kam se karta dává
-*PCIe*
-
-- Grafické karty patří do PCI Express ×16.
-- Ostatní karty se dnes dávají do PCI Express ×1 až ×4.
-- Číslo za × říká, kolik linek slot kartě dá.
-
-> Tip: grafika chce nejširší slot.
-
-
-# Kvíz
-
-## K čemu je přídavná karta?
-- [x] Rozšíří nebo vylepší schopnosti počítače
-- Nahradí napájecí zdroj
-- Jen uzavře skříň
-- Ukládá BIOS
-
-## Do jakého slotu patří grafická karta?
-- Do ISA
-- [x] Do PCI Express ×16
-- Do FDD
-- Do F_PANEL
-
-## Kam se dnes dávají ostatní karty?
-- Jen do AGP
-- Jen na přední stěnu skříně
-- [x] Do PCI Express ×1 až ×4
-- Přímo do patice procesoru
-
-## Proč nejde karta zasunout do každého slotu?
-- [x] Sloty se liší a karta sedí jen do odpovídajícího
-- Protože všechny sloty jsou stejné
-- Protože karty nemají kontakty
-- Protože to zakazuje airflow
diff --git a/content/technicke-vybaveni/1-rocnik/17-graficka-karta.en.md b/content/technicke-vybaveni/1-rocnik/17-graficka-karta.en.md
deleted file mode 100644
index f4d6ea3..0000000
--- a/content/technicke-vybaveni/1-rocnik/17-graficka-karta.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Graphics card
-popis: 3 slides about the image, card types and the image path, then a quiz.
-nazev_cviceni: Quiz: graphics card
-nadpis_kvizu: Quiz on the graphics card
----
diff --git a/content/technicke-vybaveni/1-rocnik/17-graficka-karta.md b/content/technicke-vybaveni/1-rocnik/17-graficka-karta.md
deleted file mode 100644
index eded28a..0000000
--- a/content/technicke-vybaveni/1-rocnik/17-graficka-karta.md
+++ /dev/null
@@ -1,72 +0,0 @@
----
-id: hardware-1-graficka-karta
-puvodni: hw.17
-nazev: Grafická karta
-popis: 3 snímky o obrazu, typech karet a cestě obrazu a pak kvíz.
-nazev_cviceni: Kvíz: grafická karta
-nadpis_kvizu: Kvíz ke grafické kartě
----
-
-# Výklad
-
-## Z čeho se skládá obraz
-*Obraz*
-
-- Digitalizace obrazu je vzorkování, kvantování a kódování.
-- Jas může mít třeba 256 úrovní, nebo jen 2.
-- Pixel je bod v obraze.
-- Polygon je 2D mnohoúhelník; ve hrách často trojúhelník, protože ho GPU zpracuje nejjednodušeji.
-- Vertex je vrchol, edge je hrana.
-- Textura je bitmapa namapovaná na 3D model.
-- Ray tracing je metoda sledování paprsku.
-
-> Tip: hry skládají scénu hlavně z trojúhelníků.
-
-## Integrovaná a samostatná grafika
-*Typy*
-
-- Integrovaná grafika je čip přímo na základní desce.
-- Bývá v levnějších a kancelářských sestavách i v noteboocích.
-- Má nižší výkon a nižší cenu a může brzdit zbytek systému.
-- Samostatná karta se dává do rozšiřujícího slotu.
-- Míří na hráče a náročnější uživatele.
-
-> Tip: integrovaný čip šetří peníze, samostatná karta přidá výkon.
-
-## Co je na kartě a kam jde obraz
-*Cesta*
-
-- Na kartě je GPU, RAMDAC, paměti, sběrnice, porty a často konektor přídavného napájení (Molex).
-- GPU mívá aktivní chladič, okolo něj sedí paměťové čipy.
-- Datový tok začíná v procesoru počítače a jde do grafické paměti.
-- GPU data zpracuje a pošle je do převodníku signálu.
-- Odtud obraz pokračuje do monitoru (framebuffer).
-
-> Tip: obraz jde z CPU do grafické paměti, pak do GPU a dál na monitor.
-
-
-# Kvíz
-
-## Co je pixel?
-- [x] Bod v obraze
-- Hrana polygonu
-- Napájecí konektor
-- Typ pevného disku
-
-## Proč hry často používají trojúhelník?
-- Protože má víc hran než čtverec
-- [x] Pro grafickou kartu je nejjednodušší na zpracování
-- Protože nepotřebuje texturu
-- Protože nahradí monitor
-
-## Čím se vyznačuje integrovaná grafika?
-- Je vždy rychlejší než samostatná karta
-- Sedí jen v mechanice DVD
-- [x] Čip je na desce, bývá levnější a má nižší výkon
-- Nemá vliv na zbytek počítače
-
-## Jakou cestou jde obraz?
-- [x] CPU → grafická paměť → GPU → převodník → monitor
-- Monitor → disk → zdroj → skříň
-- Jen z BIOSu přímo do reproduktoru
-- Z F_PANEL rovnou na tiskárnu
diff --git a/content/technicke-vybaveni/1-rocnik/18-shadery-a-render.en.md b/content/technicke-vybaveni/1-rocnik/18-shadery-a-render.en.md
deleted file mode 100644
index 72dfda3..0000000
--- a/content/technicke-vybaveni/1-rocnik/18-shadery-a-render.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Shaders and rendering
-popis: 3 slides about shaders, rendering and CUDA, then a quiz.
-nazev_cviceni: Quiz: shaders and rendering
-nadpis_kvizu: Quiz on shaders and rendering
----
diff --git a/content/technicke-vybaveni/1-rocnik/18-shadery-a-render.md b/content/technicke-vybaveni/1-rocnik/18-shadery-a-render.md
deleted file mode 100644
index 992d18d..0000000
--- a/content/technicke-vybaveni/1-rocnik/18-shadery-a-render.md
+++ /dev/null
@@ -1,72 +0,0 @@
----
-id: hardware-1-shadery-a-render
-puvodni: hw.18
-nazev: Shadery a render
-popis: 3 snímky o shaderech, renderu a CUDA a pak kvíz.
-nazev_cviceni: Kvíz: shadery a render
-nadpis_kvizu: Kvíz k shaderům a renderování
----
-
-# Výklad
-
-## Grafický procesor
-*GPU*
-
-- GPU přijímá data od CPU a počítá obraz.
-- Je to složitý čip se stovkami milionů tranzistorů.
-- Sleduje se takt v MHz, kapacita paměti a počet shaderů.
-- Dřív se počítaly vertex a pixel shadery, dnes spíš unifikované, částečně programovatelné jednotky.
-
-> Tip: pro výkon počítače je GPU stejně důležité jako CPU.
-
-## Shader a renderování
-*Programy*
-
-- Shader je program, který řídí části grafického řetězce.
-- Renderování tvoří obraz podle počítačového modelu, nejčastěji 3D.
-- Render je vizualizace: model se přenese do 2D bitmapy.
-- Softwarový render počítá procesor. Je přesnější, ale mnohonásobně pomalejší.
-- Hardwarový render slouží pro náhledy a hry.
-- Vertex shader transformuje vrcholy a nové nevytváří.
-- Pixel shader potom mapuje texturu a přidává efekty, třeba ray tracing nebo bump mapping.
-
-> Tip: vertex shader posouvá vrcholy, ale nové nevytváří.
-
-## CUDA a architektura GPU
-*Výpočty*
-
-- CUDA je paralelní platforma NVIDIA a nechá GPU počítat i jiné úlohy než obraz.
-- Zrychluje třeba lineární algebru, obraz, video a hluboké učení.
-- Obdoba u AMD se jmenuje FireStream.
-- GPU vzniklo pro hry: miliony polygonů a velké textury.
-- Je stavěné na tisíce vláken, hodně počítání a málo podmínek a na sekvenční čtení paměti.
-- Jednoduché skalární procesory jsou sdružené do streaming multiprocesorů.
-
-> Tip: většinu čipu tvoří mnoho jednoduchých procesorů.
-
-
-# Kvíz
-
-## Co je shader?
-- [x] Program řídící části grafického řetězce
-- Napájecí konektor Molex
-- Typ pevného disku
-- Větrací mřížka
-
-## Co vertex shader nedělá?
-- Neposouvá vrcholy
-- [x] Nevytváří nové vertexy
-- Nesahá na grafickou kartu
-- Nepoužívá se ve 3D
-
-## Čím se liší softwarový render?
-- Je rychlejší, protože ho počítá jen GPU
-- Umí jen černobíle
-- [x] Počítá ho CPU, je přesnější a mnohem pomalejší
-- Nevytvoří bitmapu
-
-## Co je CUDA?
-- [x] Platforma NVIDIA pro výpočty na GPU
-- Standard napájecího zdroje
-- Starý slot ISA
-- Formát diskety
diff --git a/content/technicke-vybaveni/1-rocnik/19-pamet-a-sbernice-gpu.en.md b/content/technicke-vybaveni/1-rocnik/19-pamet-a-sbernice-gpu.en.md
deleted file mode 100644
index 2aad1f5..0000000
--- a/content/technicke-vybaveni/1-rocnik/19-pamet-a-sbernice-gpu.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: GPU memory and bus
-popis: 3 slides about RAMDAC, GDDR and PCIe, then a quiz.
-nazev_cviceni: Quiz: GPU memory and bus
-nadpis_kvizu: Quiz on GPU memory and the bus
----
diff --git a/content/technicke-vybaveni/1-rocnik/19-pamet-a-sbernice-gpu.md b/content/technicke-vybaveni/1-rocnik/19-pamet-a-sbernice-gpu.md
deleted file mode 100644
index 335dc85..0000000
--- a/content/technicke-vybaveni/1-rocnik/19-pamet-a-sbernice-gpu.md
+++ /dev/null
@@ -1,70 +0,0 @@
----
-id: hardware-1-pamet-a-sbernice-gpu
-puvodni: hw.19
-nazev: Paměť a sběrnice GPU
-popis: 3 snímky o RAMDAC, GDDR a PCIe a pak kvíz.
-nazev_cviceni: Kvíz: paměť a sběrnice GPU
-nadpis_kvizu: Kvíz k paměti a sběrnici GPU
----
-
-# Výklad
-
-## RAMDAC a grafická paměť
-*Převod*
-
-- RAMDAC převádí digitální data na analogová.
-- U dnešních karet je uvnitř GPU a má přednost při čtení paměti.
-- Okolo GPU jsou grafické paměti, často DDR nebo GDDR5.
-- Hlavní parametry jsou kapacita a frekvence.
-- CAS latency je čekání, než se po zadání adresy sloupce data objeví na pinech.
-- Čím nižší latence, tím lépe. Časy paměti se udávají v nanosekundách.
-
-> Tip: nižší CAS latency znamená kratší čekání.
-
-## Sběrnice grafické karty
-*Spojení*
-
-- Typická sběrnice karty je PCI Express ×16, dřív to bylo AGP.
-- PCIe 4.0 zdvojnásobí propustnost proti předchozí verzi při stejném počtu linek.
-- Rychlost jedné linky stoupla z 1 GB/s na 2 GB/s.
-- Slot ×16 tak teoreticky dává 32 GB/s.
-- M.2 se čtyřmi linkami dává 8 GB/s.
-
-> Tip: PCIe 4.0 má na jedné lince 2 GB/s.
-
-## Porty na kartě
-*Výstupy*
-
-- Běžné výstupy jsou D-Sub, DVI, HDMI a DisplayPort.
-- Starší karty mají i TV výstup, S-Video nebo cinch.
-- Výrobci parametry přikrášlují, proto se karty srovnávají benchmarkem.
-- Známý test je 3DMark. Podíl má i procesor, rozhoduje ale grafická karta.
-
-> Tip: stejný obraz může odejít analogově i digitálně.
-
-
-# Kvíz
-
-## Co dělá RAMDAC?
-- Převádí analogový signál na digitální
-- [x] Převádí digitální data na analogová a dnes je v GPU
-- Počítá jen cache procesoru
-- Formátuje pevný disk
-
-## Co znamená nižší CAS latency?
-- [x] Data se objeví dřív, takže je to lepší
-- Karta má míň portů
-- Sběrnice je pomalejší
-- Monitor má nižší rozlišení
-
-## Jakou teoretickou propustnost má PCIe 4.0 ×16?
-- 2 GB/s
-- 8 GB/s
-- [x] 32 GB/s
-- 264 MB/s
-
-## Co se stalo s rychlostí jedné linky u PCIe 4.0?
-- Klesla z 2 GB/s na 1 GB/s
-- [x] Stoupla z 1 GB/s na 2 GB/s
-- Zůstala 133 MB/s
-- Záleží jen na barvě slotu
diff --git a/content/technicke-vybaveni/1-rocnik/20-rozliseni-obrazu.en.md b/content/technicke-vybaveni/1-rocnik/20-rozliseni-obrazu.en.md
deleted file mode 100644
index be30d2b..0000000
--- a/content/technicke-vybaveni/1-rocnik/20-rozliseni-obrazu.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Display resolutions
-popis: 2 slides about ports, resolutions and SLI, then a quiz.
-nazev_cviceni: Quiz: display resolutions
-nadpis_kvizu: Quiz on resolutions and multi-GPU
----
diff --git a/content/technicke-vybaveni/1-rocnik/20-rozliseni-obrazu.md b/content/technicke-vybaveni/1-rocnik/20-rozliseni-obrazu.md
deleted file mode 100644
index 8100d63..0000000
--- a/content/technicke-vybaveni/1-rocnik/20-rozliseni-obrazu.md
+++ /dev/null
@@ -1,59 +0,0 @@
----
-id: hardware-1-rozliseni-obrazu
-puvodni: hw.20
-nazev: Rozlišení obrazu
-popis: 2 snímky o portech, rozlišení a SLI a pak kvíz.
-nazev_cviceni: Kvíz: rozlišení obrazu
-nadpis_kvizu: Kvíz k rozlišení a více kartám
----
-
-# Výklad
-
-## SLI, CrossFire a režimy
-*Více karet*
-
-- SLI od NVIDIA spojí dvě grafické karty do jednoho celku.
-- Vychází levněji než jedna špičková karta.
-- Obdoba u ATI se jmenuje CrossFire.
-- Režim určuje rozlišení a barevnou hloubku.
-- Dnešní karty zvládnou i 3200×2400 (QUXGA) při 32 bitech.
-
-> Tip: první režim IBM PC byl v roce 1982 MDA.
-
-## 720p, 1080p, 4K a 8K
-*Názvy*
-
-- 720p je 1280×720, tedy HD nebo HD Ready.
-- 1080p je 1920×1080, tedy Full HD.
-- 1440p je 2560×1440, tedy QHD, čtyřnásobek 720p.
-- 4K (2160p) je 3840×2160, čtyřnásobek Full HD.
-- 8K (4320p) je 7680×4320, šestnáctinásobek Full HD.
-
-> Tip: 4K má čtyřikrát víc pixelů než 1080p, 8K šestnáctkrát.
-
-
-# Kvíz
-
-## Co je 1080p?
-- [x] 1920×1080, Full HD
-- 1280×720, HD Ready
-- 7680×4320, 8K
-- 3200×2400, QUXGA
-
-## Co je 4K?
-- 1280×720
-- [x] 3840×2160, čtyřnásobek pixelů Full HD
-- 2560×1440
-- 640×480
-
-## Kolikrát víc pixelů má 8K proti 1080p?
-- Dvakrát
-- Čtyřikrát
-- [x] Šestnáctkrát
-- Stejně
-
-## Co je SLI a co je CrossFire?
-- [x] SLI spojuje karty NVIDIA, CrossFire je obdoba ATI
-- Obojí je typ paměti DDR
-- SLI je port HDMI, CrossFire je D-Sub
-- Jsou to dva názvy stejné firmy
diff --git a/content/technicke-vybaveni/1-rocnik/21-zvukova-karta.en.md b/content/technicke-vybaveni/1-rocnik/21-zvukova-karta.en.md
deleted file mode 100644
index 28419a4..0000000
--- a/content/technicke-vybaveni/1-rocnik/21-zvukova-karta.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Sound card
-popis: 3 slides about card types, the chip and recording, then a quiz.
-nazev_cviceni: Quiz: sound card
-nadpis_kvizu: Quiz on the sound card
----
diff --git a/content/technicke-vybaveni/1-rocnik/21-zvukova-karta.md b/content/technicke-vybaveni/1-rocnik/21-zvukova-karta.md
deleted file mode 100644
index 6c45806..0000000
--- a/content/technicke-vybaveni/1-rocnik/21-zvukova-karta.md
+++ /dev/null
@@ -1,69 +0,0 @@
----
-id: hardware-1-zvukova-karta
-puvodni: hw.21
-nazev: Zvuková karta
-popis: 3 snímky o typech karet, čipu a nahrávání a pak kvíz.
-nazev_cviceni: Kvíz: zvuková karta
-nadpis_kvizu: Kvíz ke zvukové kartě
----
-
-# Výklad
-
-## Zvuková karta
-*Výstup*
-
-- Karta zajistí zvukový výstup.
-- Převede digitální signál na analogový, který zahraje reproduktor.
-- Rozlišujeme integrované, interní a externí karty.
-- CD má 44,1 kHz a bitovou hloubku 16 až 24 bitů.
-
-> Tip: digitalizace zvuku je vzorkování, kvantování a kódování.
-
-## Integrovaná, interní, externí a herní
-*Provedení*
-
-- Integrovaný zvuk je na téměř každé desce a na multimédia stačí.
-- Má slabší zesílení a méně konektorů.
-- Interní karta jde do PCIe a nabízí prostorový zvuk a software pro nastavení.
-- Externí karta se hodí, když integrovaná nestačí, i do notebooku.
-- Herní karta, interní nebo externí, drží prostorový zvuk, malou odezvu a přesnou reprodukci.
-
-> Tip: externí karta se připojuje přes USB a sedí mimo skříň.
-
-## DSP a nahrávání
-*Čip*
-
-- Zvukový čip má jádra, D/A a A/D převodníky, sluchátkový zesilovač a vstupy i výstupy.
-- DSP stojí na harvardské architektuře: paměť programu je oddělená od paměti dat, na rozdíl od von Neumannova modelu.
-- Počet vstupů má odpovídat počtu nástrojů nahrávaných najednou.
-- Na zpěv stačí jeden analogový vstup.
-- Některé nástroje, třeba keyboard, chtějí vysokoimpedanční vstup Hi-Z.
-
-> Tip: kondenzátorový mikrofon chce fantomové napájení 48 V.
-
-
-# Kvíz
-
-## Co zvuková karta dělá se signálem?
-- [x] Převede digitální signál na analogový pro reproduktor
-- Převede jen obraz na texturu
-- Nahradí procesor
-- Ukládá data na pásku
-
-## Jak se připojuje interní zvuková karta?
-- Jen přes VGA
-- [x] Přes slot PCIe
-- Jen přes FDD
-- Přes baterii CMOS
-
-## Jak se připojuje externí zvuková karta?
-- Do patice procesoru
-- Do slotu AGP
-- [x] Přes USB, mimo skříň
-- Jen na přední LED
-
-## K čemu je fantomové napájení 48 V?
-- [x] Pro kondenzátorové mikrofony
-- Pro vypnutí zdroje ATX
-- Pro chlazení procesoru
-- Pro slot ISA
diff --git a/content/technicke-vybaveni/1-rocnik/22-sitova-a-tv-karta.en.md b/content/technicke-vybaveni/1-rocnik/22-sitova-a-tv-karta.en.md
deleted file mode 100644
index 1c71188..0000000
--- a/content/technicke-vybaveni/1-rocnik/22-sitova-a-tv-karta.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Network and TV cards
-popis: 2 slides about network and TV cards, then a quiz.
-nazev_cviceni: Quiz: network and TV cards
-nadpis_kvizu: Quiz on network and TV cards
----
diff --git a/content/technicke-vybaveni/1-rocnik/22-sitova-a-tv-karta.md b/content/technicke-vybaveni/1-rocnik/22-sitova-a-tv-karta.md
deleted file mode 100644
index 515cd77..0000000
--- a/content/technicke-vybaveni/1-rocnik/22-sitova-a-tv-karta.md
+++ /dev/null
@@ -1,60 +0,0 @@
----
-id: hardware-1-sitova-a-tv-karta
-puvodni: hw.22
-nazev: Síťová a TV karta
-popis: 2 snímky o síťové a televizní kartě a pak kvíz.
-nazev_cviceni: Kvíz: síťová a TV karta
-nadpis_kvizu: Kvíz k síťové a televizní kartě
----
-
-# Výklad
-
-## Síťová karta
-*Síť*
-
-- Integrovaná karta má ethernetový konektor přímo na desce.
-- Interní karta kombinuje kabel a Wi-Fi a jde do PCIe.
-- Externí karta se strčí do USB.
-- Wake on LAN umí počítač zapnout po datové síti.
-- Externí anténa zvětší dosah.
-- MIMO použije víc antén a zvedne propustnost.
-- WPS zjednoduší nastavení bezdrátového spojení.
-
-> Tip: dnes nemusí jít jen o kabel, umí i Wi-Fi.
-
-## Televizní karta
-*Televize*
-
-- Televizní tuner na desce integrovaný není nikdy.
-- Přidá počítači příjem televizního signálu.
-- Umí signál i zaznamenat.
-- Je to další rozšiřující karta, ne součást čipsetu.
-
-> Tip: tuner na základní desce integrovaný nebývá.
-
-
-# Kvíz
-
-## Kde je integrovaná síťová karta?
-- [x] Ethernetový konektor přímo na desce
-- Jen v optické mechanice
-- Jen jako externí disk
-- V patici procesoru
-
-## Co je Wake on LAN?
-- Vypnutí monitoru při nečinnosti
-- [x] Zapnutí počítače přes datovou síť
-- Zrychlení pevného disku
-- Jiný název pro BIOS
-
-## Co přináší MIMO?
-- Jednu anténu a nižší dosah
-- Jen kabelový konektor
-- [x] Více antén a vyšší propustnost
-- Zrušení Wi-Fi
-
-## Jak je to s televizní kartou na desce?
-- [x] Není integrovaná nikdy
-- Je na každé desce ATX
-- Nahrazuje zvukovou kartu
-- Je součást severního mostu
diff --git a/content/technicke-vybaveni/1-rocnik/23-pevny-disk.en.md b/content/technicke-vybaveni/1-rocnik/23-pevny-disk.en.md
deleted file mode 100644
index d65288b..0000000
--- a/content/technicke-vybaveni/1-rocnik/23-pevny-disk.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Hard drive
-popis: 3 slides about HDDs, parameters and data layout, then a quiz.
-nazev_cviceni: Quiz: hard drive
-nadpis_kvizu: Quiz on the hard drive
----
diff --git a/content/technicke-vybaveni/1-rocnik/23-pevny-disk.md b/content/technicke-vybaveni/1-rocnik/23-pevny-disk.md
deleted file mode 100644
index 77ebda7..0000000
--- a/content/technicke-vybaveni/1-rocnik/23-pevny-disk.md
+++ /dev/null
@@ -1,69 +0,0 @@
----
-id: hardware-1-pevny-disk
-puvodni: hw.23
-nazev: Pevný disk
-popis: 3 snímky o HDD, parametrech a struktuře dat a pak kvíz.
-nazev_cviceni: Kvíz: pevný disk
-nadpis_kvizu: Kvíz k pevnému disku
----
-
-# Výklad
-
-## Vnější paměti a HDD
-*Trvalá data*
-
-- Vnější paměť drží data dlouhodobě, má kapacitu v terabajtech a je energeticky nezávislá.
-- Patří sem HDD, SSD a USB flash disky.
-- Pevný disk zaznamenává data magneticky.
-- Notebookové disky mají 2,5 palce, stolní 3,5 palce.
-
-> Tip: HDD je mechanický, otřes ho může poškodit.
-
-## Parametry pevného disku
-*Čísla*
-
-- Kapacita se udává v terabajtech.
-- Přístupová doba je v milisekundách.
-- Přenosová rychlost je ve stovkách MB/s.
-- Cache bývá třeba 256 MB.
-
-> Tip: běžné otáčky jsou 5400 a 7200 za minutu.
-
-## Zápis, čtení a struktura
-*Stopy*
-
-- Zápis dělá cívka magnetickým polem.
-- Čtení používá elektromagnetickou indukci.
-- Disk je hermeticky uzavřený.
-- Formátování vytvoří stopy a sektory.
-- Cylindr jsou stejné stopy na všech plotnách.
-- Defragmentace složí rozházená data k sobě, aby se četla rychleji.
-
-> Tip: hlavy se ploten nedotýkají, vznášejí se na vzduchu.
-
-
-# Kvíz
-
-## Na jakém principu pracuje HDD?
-- [x] Na magnetickém záznamu a je mechanický
-- Jen na optickém laseru
-- Nemá pohyblivé části
-- Drží data jen při zapnutém napájení
-
-## Jaké rozměry mají pevné disky?
-- Jen 8 palců
-- [x] 2,5" do notebooku a 3,5" do stolního počítače
-- 12 cm jako CD
-- Jen formát microSD
-
-## Jaké otáčky se u HDD běžně uvádějí?
-- 100 a 200 ot/min
-- 1000 ot/min
-- [x] 5400 a 7200 ot/min
-- 48 000 ot/min
-
-## Jak disk zapisuje a čte?
-- [x] Zápis cívkou, čtení elektromagnetickou indukcí
-- Zápis laserem, čtení ventilátorem
-- Obojí jen přes BIOS
-- Zápis do CMOS, čtení z baterie
diff --git a/content/technicke-vybaveni/1-rocnik/24-ssd-a-flash.en.md b/content/technicke-vybaveni/1-rocnik/24-ssd-a-flash.en.md
deleted file mode 100644
index 4ee9748..0000000
--- a/content/technicke-vybaveni/1-rocnik/24-ssd-a-flash.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: SSD and flash
-popis: 3 slides about SSD, TRIM, SSHD and USB, then a quiz.
-nazev_cviceni: Quiz: SSD and flash
-nadpis_kvizu: Quiz on SSD and flash
----
diff --git a/content/technicke-vybaveni/1-rocnik/24-ssd-a-flash.md b/content/technicke-vybaveni/1-rocnik/24-ssd-a-flash.md
deleted file mode 100644
index c4c5fcd..0000000
--- a/content/technicke-vybaveni/1-rocnik/24-ssd-a-flash.md
+++ /dev/null
@@ -1,69 +0,0 @@
----
-id: hardware-1-ssd-a-flash
-puvodni: hw.24
-nazev: SSD a flash
-popis: 3 snímky o SSD, TRIM, SSHD a USB a pak kvíz.
-nazev_cviceni: Kvíz: SSD a flash
-nadpis_kvizu: Kvíz k SSD a flash paměti
----
-
-# Výklad
-
-## SSD
-*Bez pohybu*
-
-- SSD je polovodičový disk bez pohyblivých částí.
-- Je odolnější, tiché, úspornější a rychlejší než HDD.
-- Buňky se opotřebovávají, počet zápisů je omezený.
-- Bývá dražší než plotnový disk stejné kapacity.
-
-> Tip: fragmentace u SSD problém není.
-
-## FTL, TRIM a záchrana
-*Buňky*
-
-- FTL rovnoměrně rozkládá zápisy, aby se buňky sjížděly stejně.
-- TRIM řekne disku, které bloky už nikdo nepoužívá, a zápis se tím zrychlí.
-- Poškození může být hardwarové (mechanika, elektronika, oheň, voda) nebo softwarové (smazání, formát, virus).
-- Smazání maže metadata, obsah na HDD často zůstane a dá se obnovit.
-- U SSD je obnova kvůli TRIM obtížnější.
-
-> Tip: smazání smaže metadata. U SSD TRIM záchranu ztěžuje.
-
-## SSHD a USB flash
-*Přenos*
-
-- SSHD je pevný disk s malou SSD částí, zhruba 8 GB.
-- Často používané soubory si přesune na rychlou část.
-- Dává smysl, když chcete kapacitu i rychlost za rozumnou cenu.
-- USB flash je malé médium na přenos dat po sběrnici USB.
-- Životnost buněk záleží na typu SLC, MLC nebo TLC a na kvalitě hardwaru.
-
-> Tip: konektor USB flash disku je počítaný asi na 1500 cyklů.
-
-
-# Kvíz
-
-## Co je hlavní výhoda SSD proti HDD?
-- Má plotny a je hlučnější
-- [x] Nemá pohyblivé části, je tiché a rychlejší
-- Data ztratí při vypnutí
-- Nejde ho použít jako systémový disk
-
-## Jaká je nevýhoda SSD?
-- [x] Buňky se opotřebovávají a disk bývá dražší
-- Musí se defragmentovat každý den
-- Nepřežije přesun po stole
-- Nemá TRIM ani FTL
-
-## Co dělá příkaz TRIM?
-- Zvýší otáčky ploten
-- Smaže BIOS
-- [x] Oznámí SSD, které bloky už nejsou použité
-- Zapne fantomové napájení
-
-## Co je SSHD?
-- Jen jiný název pro disketu
-- [x] HDD doplněné o malou SSD část, asi 8 GB
-- Optický disk s modrým laserem
-- Paměťová karta SD
diff --git a/content/technicke-vybaveni/1-rocnik/25-pasky-a-diskety.en.md b/content/technicke-vybaveni/1-rocnik/25-pasky-a-diskety.en.md
deleted file mode 100644
index 1300daf..0000000
--- a/content/technicke-vybaveni/1-rocnik/25-pasky-a-diskety.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Tapes and floppies
-popis: 3 slides about removable media, tapes and floppies, then a quiz.
-nazev_cviceni: Quiz: tapes and floppies
-nadpis_kvizu: Quiz on tapes and floppies
----
diff --git a/content/technicke-vybaveni/1-rocnik/25-pasky-a-diskety.md b/content/technicke-vybaveni/1-rocnik/25-pasky-a-diskety.md
deleted file mode 100644
index 4b25db4..0000000
--- a/content/technicke-vybaveni/1-rocnik/25-pasky-a-diskety.md
+++ /dev/null
@@ -1,72 +0,0 @@
----
-id: hardware-1-pasky-a-diskety
-puvodni: hw.25
-nazev: Pásky a diskety
-popis: 3 snímky o výměnných médiích, páskách a disketách a pak kvíz.
-nazev_cviceni: Kvíz: pásky a diskety
-nadpis_kvizu: Kvíz k páskám a disketám
----
-
-# Výklad
-
-## Výměnná média
-*Přehled*
-
-- Mechanická média: děrné štítky a děrné pásky.
-- Magnetická: pásky a diskety.
-- Optická: CD, DVD a Blu-ray.
-- Elektrická: paměťové karty a USB flash.
-- K souboru na pásce nejde skočit, páska se na místo přetočí.
-
-> Tip: páska se musí převinout, proto se hodí na archiv, ne na denní práci.
-
-## Magnetická páska
-*Pásky*
-
-- Jedna raná páska unesla data asi deseti tisíc děrných štítků.
-- Kapacita začínala u stovek kilobajtů, třeba 850 kB.
-- Dlouho měla vyšší kapacitu než pevné disky; zlom přišel kolem roku 2010 s terabajtovými disky.
-- Podle IBM ji v USA používá přímo nebo nepřímo až 82 % velkých firem.
-- Odhad je stovky miliard gigabajtů dat na páskách po světě.
-- Sklad CSCS v Curychu má 27 PB na 17 000 páskách LTO-5 po 1500 GB.
-
-> Tip: páska pořád slouží velkým firmám na dlouhodobou zálohu.
-
-## Z pásky na kotouč
-*Diskety*
-
-- Stopa se přesunula na otáčející se mylarový kotouč.
-- Soustředné stopy umožní náhodný přístup, ne jen převíjení.
-- 8" disketa (1971) měla 100 kB a zápis z jedné strany.
-- 5,25" (1976) se dala poslat poštou; kapacita 80 až 1200 kB.
-- 3,5" zavedla Sony v roce 1981.
-- Standard 1990–1995 je 1,44 MB, pevný obal a dvířka, vloží se jen správně.
-
-> Tip: ikona ukládání je 3,5" disketa.
-
-
-# Kvíz
-
-## Proč se páska hodí na archiv a ne na běžnou práci?
-- [x] K souboru se musí převinout, přístup není náhodný
-- Nemá žádnou kapacitu
-- Nejde ji vyjmout z mechaniky
-- Maže se při každém čtení
-
-## Jaká byla raná 8" disketa?
-- Z roku 1981 a měla 1,44 MB
-- [x] Z roku 1971 a měla 100 kB
-- Z roku 2010 a měla 1 TB
-- Byla optická
-
-## Co platí o 3,5" disketě?
-- Zavedla ji IBM v roce 1960 a měla 850 kB
-- Neměla pevný obal
-- [x] Sony, 1981, standardně 1,44 MB
-- Šla vložit oběma směry
-
-## Čím disketa zrychlila přístup proti pásce?
-- [x] Soustředné stopy dovolí náhodný přístup
-- Má modrý laser
-- Nemá magnetickou vrstvu
-- Čte se jen od konce
diff --git a/content/technicke-vybaveni/1-rocnik/26-opticka-media.en.md b/content/technicke-vybaveni/1-rocnik/26-opticka-media.en.md
deleted file mode 100644
index 1f08106..0000000
--- a/content/technicke-vybaveni/1-rocnik/26-opticka-media.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Optical media
-popis: 3 slides about CD, DVD, Blu-ray and memory cards, then a quiz.
-nazev_cviceni: Quiz: optical media
-nadpis_kvizu: Quiz on optical and flash media
----
diff --git a/content/technicke-vybaveni/1-rocnik/26-opticka-media.md b/content/technicke-vybaveni/1-rocnik/26-opticka-media.md
deleted file mode 100644
index 800b8d5..0000000
--- a/content/technicke-vybaveni/1-rocnik/26-opticka-media.md
+++ /dev/null
@@ -1,68 +0,0 @@
----
-id: hardware-1-opticka-media
-puvodni: hw.26
-nazev: Optická média
-popis: 3 snímky o CD, DVD, Blu-ray a kartách a pak kvíz.
-nazev_cviceni: Kvíz: optická média
-nadpis_kvizu: Kvíz k optickým a flash médiím
----
-
-# Výklad
-
-## Jak se čte optický disk
-*Laser*
-
-- Kotouč má průměr 12 cm a tloušťku 1,2 mm.
-- Data leží ve spirále od středu ke kraji.
-- U 700MB CD je stopa dlouhá 6,244 km.
-- Spirála střídá pity (prohlubně) a landy (plochy).
-
-> Tip: land odrazí paprsek (1), pit ho rozptýlí (0).
-
-## CD, DVD a Blu-ray
-*Generace*
-
-- CD má 650–870 MB. CD-R vypálí organické barvivo asi při 290 °C a zápis je nevratný.
-- CD-RW jde přepsat tisíckrát až stotisíckrát. Slitina AgInSbTe mění krystaly zahřátím a chlazením.
-- DVD má 1,4 až 15,82 GB, stopu 11,84 km a umí dvě vrstvy i obě strany.
-- Blu-ray má 25–128 GB. Kratší vlnová délka znamená hustší záznam.
-- Optika ustoupila kvůli kapacitě, pomalému zápisu, spolehlivosti a tomu, že obsah je online.
-
-> Tip: Blu-ray používá modrý laser 405 nm, DVD červený 650 nm.
-
-## Flash karty
-*Karty*
-
-- Flash je statická vnější paměť: malá, s velkou kapacitou, odolná vůči vibracím a magnetickému poli.
-- Buňky mají plovoucí hradlo: SLC, MLC, TLC nebo QLC.
-- Karty: SD (i mini a micro), MMC, Memory Stick a CompactFlash.
-- Čtečka na USB mívá víc slotů, aby vzala víc formátů.
-
-> Tip: NAND paměť vytlačila optické mechaniky.
-
-
-# Kvíz
-
-## Co se stane, když laser trefí land a když trefí pit?
-- Obojí je signál 0
-- [x] Land se odrazí jako 1, pit se rozptýlí jako 0
-- Land je 0 a pit je 1
-- Laser se na disku nepoužívá
-
-## Jak se zapisuje CD-R?
-- [x] Vypálením barviva, zápis je nevratný
-- Magnetickou cívkou jako HDD
-- Libovolně mnohokrát jako RAM
-- Jen změnou názvu souboru
-
-## Čím se laser Blu-ray liší od DVD?
-- Má červený laser 650 nm a menší kapacitu
-- Nemá spirálu
-- [x] Má modrý laser 405 nm a kapacitu 25–128 GB
-- Má stejný laser jako CD
-
-## Které karty patří mezi flash média?
-- Jen děrné štítky
-- [x] SD, MMC, Memory Stick a CompactFlash
-- Jen 8" diskety
-- Jen pásky LTO
diff --git a/content/technicke-vybaveni/1-rocnik/27-klavesnice.en.md b/content/technicke-vybaveni/1-rocnik/27-klavesnice.en.md
deleted file mode 100644
index 4472bdf..0000000
--- a/content/technicke-vybaveni/1-rocnik/27-klavesnice.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Keyboard
-popis: 2 slides about keyboard parts and types, then a quiz.
-nazev_cviceni: Quiz: keyboard
-nadpis_kvizu: Quiz on the keyboard
----
diff --git a/content/technicke-vybaveni/1-rocnik/27-klavesnice.md b/content/technicke-vybaveni/1-rocnik/27-klavesnice.md
deleted file mode 100644
index 0b0d05d..0000000
--- a/content/technicke-vybaveni/1-rocnik/27-klavesnice.md
+++ /dev/null
@@ -1,58 +0,0 @@
----
-id: hardware-1-klavesnice
-puvodni: hw.27
-nazev: Klávesnice
-popis: 2 snímky o částech a typech klávesnic a pak kvíz.
-nazev_cviceni: Kvíz: klávesnice
-nadpis_kvizu: Kvíz ke klávesnici
----
-
-# Výklad
-
-## Polohovací zařízení a klávesnice
-*Vstup*
-
-- Polohovací zařízení hýbe kurzorem a bývá drátové i bezdrátové.
-- Hlavní písmenková část.
-- Speciální a české znaky.
-- Ovládací tlačítka a posunovací tlačítka.
-- Numerická klávesnice a funkční klávesy.
-
-> Tip: klávesnice má šest částí.
-
-## Membrána, guma, nůžky a laser
-*Typy*
-
-- Membránová je nejčastější u stolních počítačů. Stisk propojí membránu a je velmi tichá.
-- U vodivé gumy se guma prohne ke kontaktu a sepne obvod. Pod ní je izolační destička a pozlacený kontakt.
-- Scissor-switch nemá gumovou membránu. Klávesa se propadne po nůžkovém mechanismu.
-- Laserová klávesnice promítá rozložení na podložku a senzor snímá stisk. Hodí se k mobilu.
-
-> Tip: scissor-switch je notebookový, mělký a tichý.
-
-
-# Kvíz
-
-## Z kolika částí se skládá běžná klávesnice?
-- [x] Ze šesti
-- Ze dvou
-- Z dvanácti
-- Jen z numerického bloku
-
-## Jaká je membránová klávesnice?
-- Nejhlučnější a jen pro notebooky
-- [x] Nejpoužívanější u stolních počítačů a velmi tichá
-- Promítá se laserem
-- Nemá žádný kontakt
-
-## Co je scissor-switch?
-- Gumová membrána ve stolním PC
-- Laser na podložce
-- [x] Notebookový nůžkový mechanismus, mělký a tichý stisk
-- Jen funkční klávesy
-
-## Jak pracuje laserová klávesnice?
-- [x] Promítne rozložení a senzor snímá virtuální stisk
-- Používá vodivou gumu
-- Je jen mechanická s nejhlubším stiskem
-- Čte jen numerický blok
diff --git a/content/technicke-vybaveni/1-rocnik/28-polohovaci-zarizeni.en.md b/content/technicke-vybaveni/1-rocnik/28-polohovaci-zarizeni.en.md
deleted file mode 100644
index 8e6e1c1..0000000
--- a/content/technicke-vybaveni/1-rocnik/28-polohovaci-zarizeni.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Pointing devices
-popis: 3 slides about the mouse, tablet and touchpad, then a quiz.
-nazev_cviceni: Quiz: pointing devices
-nadpis_kvizu: Quiz on pointing devices
----
diff --git a/content/technicke-vybaveni/1-rocnik/28-polohovaci-zarizeni.md b/content/technicke-vybaveni/1-rocnik/28-polohovaci-zarizeni.md
deleted file mode 100644
index de16781..0000000
--- a/content/technicke-vybaveni/1-rocnik/28-polohovaci-zarizeni.md
+++ /dev/null
@@ -1,68 +0,0 @@
----
-id: hardware-1-polohovaci-zarizeni
-puvodni: hw.28
-nazev: Polohovací zařízení
-popis: 3 snímky o myši, tabletu a touchpadu a pak kvíz.
-nazev_cviceni: Kvíz: polohovací zařízení
-nadpis_kvizu: Kvíz k polohovacím zařízením
----
-
-# Výklad
-
-## Kuličková myš
-*Kulička*
-
-- Posuv otáčí kuličkou.
-- Kulička točí válečky pro osy x a y.
-- Kotoučky na válečcích mají otvory.
-- Infračervené diody otvory prosvěcují.
-- Senzory záblesky převedou na polohu.
-
-> Tip: pohyb se měří prosvícením otvorů v kotoučcích.
-
-## Spaceball, trackball, tablet a digitizér
-*Prostor*
-
-- Spaceball má pohyb a rotaci po třech osách. V CAD a grafice zvedá efektivitu zhruba o 30–50 %.
-- Trackball je jako obrácená kuličková myš. Na rychlý a přesný pohyb se nehodí.
-- Tablet má síť vodičů, která reaguje na pero. Hodí se do CAD a grafiky, kreslit jde i na LCD.
-- Digitizér je speciální myš na přesnou polohu a chce speciální podložku. Měří odpor mezi ukazovátkem a podložkou.
-
-> Tip: tablet pozná tlak, až ve stovkách až 1024 úrovních.
-
-## Trackpoint a touchpad
-*Notebook*
-
-- Trackpoint je malý joystick a šetří místo.
-- Touchpad je nejčastější polohovací zařízení notebooku.
-- Snímá elektrickou kapacitu prstu.
-- Hotspoty na ploše mají zvláštní účel, třeba posuvník.
-
-> Tip: trackpoint sedí mezi klávesami G, H a B.
-
-
-# Kvíz
-
-## Jak kuličková myš pozná pohyb?
-- Laserem na podložce bez kuličky
-- [x] Kulička točí válečky a infračervené diody prosvěcují otvory
-- Jen kapacitou prstu
-- Měřením 48 V
-
-## Kde je trackpoint?
-- [x] Mezi klávesami G, H a B
-- Na zadní stěně skříně
-- V optické mechanice
-- Jen u stolní myši
-
-## Jak touchpad snímá prst?
-- Kuličkou a válečky
-- Magnetickou páskou
-- [x] Elektrickou kapacitou prstu
-- Fantomovým napájením
-
-## Co umí grafický tablet?
-- Jen zapnout počítač
-- [x] Reaguje na pero a tlak, až 1024 úrovní
-- Nahradí napájecí zdroj
-- Čte jen děrné štítky
diff --git a/content/technicke-vybaveni/1-rocnik/29-herni-ovladace-a-dotyk.en.md b/content/technicke-vybaveni/1-rocnik/29-herni-ovladace-a-dotyk.en.md
deleted file mode 100644
index 043747b..0000000
--- a/content/technicke-vybaveni/1-rocnik/29-herni-ovladace-a-dotyk.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Game controls and touch
-popis: 3 slides about gamepads, Kinect and touch, then a quiz.
-nazev_cviceni: Quiz: game controls and touch
-nadpis_kvizu: Quiz on game controls and touch
----
diff --git a/content/technicke-vybaveni/1-rocnik/29-herni-ovladace-a-dotyk.md b/content/technicke-vybaveni/1-rocnik/29-herni-ovladace-a-dotyk.md
deleted file mode 100644
index 1c6a4d0..0000000
--- a/content/technicke-vybaveni/1-rocnik/29-herni-ovladace-a-dotyk.md
+++ /dev/null
@@ -1,69 +0,0 @@
----
-id: hardware-1-herni-ovladace-a-dotyk
-puvodni: hw.29
-nazev: Herní ovladače a dotyk
-popis: 3 snímky o gamepadu, Kinectu a dotyku a pak kvíz.
-nazev_cviceni: Kvíz: herní ovladače a dotyk
-nadpis_kvizu: Kvíz k herním ovladačům a dotyku
----
-
-# Výklad
-
-## Gamepad, joystick a volant
-*Hry*
-
-- Gamepad ovládá hry a mívá programovatelná tlačítka.
-- DualShock přidává vibrace.
-- Joystick je páka na základně. Dřív byl analogový, dnes digitální.
-- Používá se i v letadlech nebo bagrech.
-- Volant napodobuje řízení: pedály, řadící páka a programovatelná tlačítka.
-
-> Tip: force feedback přidá vibrace a nárazy.
-
-## Kinect
-*Tělo*
-
-- Kombinuje RGB kameru, hloubkové čidlo, mikrofony a vlastní procesor.
-- Sleduje pohyb celého těla v prostoru.
-- Reaguje na pokyny a pozná i změnu zabarvení hlasu a emoce.
-
-> Tip: sleduje celé tělo ve 3D, ne jen ovladač v ruce.
-
-## Odporová a kapacitní vrstva
-*Displej*
-
-- Odporová vrstva má tvrzenou a vodivou vrstvu, odporové vrstvy, vymezovací bodovou síť a sklo.
-- Dotyk vrstvy přitlačí k sobě a obvod se sepne.
-- Kapacitní displej má dvě vodivé vrstvy pod napětím, oddělené mezerou.
-- Chovají se jako kondenzátor a kolem sebe mají slabé pole.
-- Změna kapacity prozradí místo dotyku.
-- Funguje to s vodivým předmětem, ne s libovolným plastem.
-
-> Tip: kapacitního displeje se musíte dotknout vodivě, třeba prstem.
-
-
-# Kvíz
-
-## Co přidává DualShock?
-- [x] Vibrace gamepadu
-- Optickou mechaniku
-- Slot PCIe
-- Magnetickou pásku
-
-## Co je force feedback?
-- Jen jiné rozlišení monitoru
-- [x] Vibrace a nárazy u joysticku a volantu
-- Typ paměti DDR3
-- Způsob formátování disku
-
-## Co Kinect sleduje?
-- Jen stisk jedné klávesy
-- Jen otáčky pevného disku
-- [x] Pohyb celého těla ve 3D, pomocí kamery, hloubky a mikrofonů
-- Jen napětí zdroje
-
-## Čím se musíte dotknout kapacitního displeje?
-- [x] Vodivým předmětem, například prstem
-- Jen nevodivým plastem
-- Jen speciálním perem bez elektřiny
-- Děrnou páskou
diff --git a/content/technicke-vybaveni/2-rocnik/01-monitory.en.md b/content/technicke-vybaveni/2-rocnik/01-monitory.en.md
deleted file mode 100644
index 49ab9c1..0000000
--- a/content/technicke-vybaveni/2-rocnik/01-monitory.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Displays
-popis: The panel, the resolution, and why 60 Hz is not the only number.
-nazev_cviceni: Quiz: Displays
-nadpis_kvizu: Quiz on Displays
----
diff --git a/content/technicke-vybaveni/2-rocnik/01-monitory.md b/content/technicke-vybaveni/2-rocnik/01-monitory.md
deleted file mode 100644
index 94998df..0000000
--- a/content/technicke-vybaveni/2-rocnik/01-monitory.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-2-monitory
-puvodni: hw2.1
-nazev: Monitory
-popis: Panel, rozlišení a proč 60 Hz není jediné číslo.
-nazev_cviceni: Kvíz: Monitory
-nadpis_kvizu: Kvíz k tématu Monitory
----
-
-# Výklad
-
-## LCD a OLED
-*Panel*
-
-- LCD svítí podsvícením a panel světlo cloní.
-- OLED svítí samotnými body a umí opravdovou černou.
-- Lesklý panel má vyšší kontrast a víc odlesků.
-- Uhlopříčka v palcích není totéž co pracovní plocha v bodech.
-
-> Tip: rozbité podsvícení LCD zhasne obraz, i když panel pořád kreslí.
-
-## Rozlišení a frekvence
-*Obraz*
-
-- Nižší rozlišení interpoluje a obraz měkne.
-- Obnovovací frekvence říká, kolikrát za sekundu se obraz překreslí.
-- Odezva panelu není totéž co frekvence.
-- HDR chce vyšší jas, ne jen zaškrtnutou volbu.
-
-> Tip: nativní rozlišení panelu je to, na které je mřížka bodů.
-
-## Obraz musí stihnout pásmo
-*Kabel*
-
-- DisplayPort a HDMI mají verze s různým datovým tokem.
-- VGA je analogové a na dlouhém kabelu měkne.
-- USB-C obraz poveze jen tehdy, když port umí alternativní režim.
-- Dokovací stanice má vlastní limit, ne limit notebooku.
-
-> Tip: starý kabel nebo slabá verze HDMI oseká frekvenci i rozlišení.
-
-
-# Kvíz
-
-## Čím se OLED liší od běžného LCD?
-- [x] Body samy svítí a černá je opravdu zhasnutá
-- Nemá rozlišení
-- Jede jen po VGA
-- Nepotřebuje grafickou kartu
-> LCD obraz skládá cloněním podsvícení.
-
-## Co je nativní rozlišení?
-- [x] Počet bodů, na který je panel vyroben
-- Nejvyšší číslo v herním menu bez ohledu na panel
-- Rychlost procesoru
-- Počet USB portů
-> Jiné rozlišení se musí přepočítat.
-
-## Proč 4K při 144 Hz někdy nejde?
-- [x] Kabel nebo verze portu nestihne datový tok
-- OLED zakazuje frekvenci nad 60 Hz
-- IPv4 omezuje monitory
-- BIOS maže rozlišení
-> Pásmo portu je strop, ne přání v menu.
-
-## Kdy USB-C nepřenese obraz?
-- [x] Když port umí jen data a nabíjení
-- Když je kabel černý
-- Když má notebook SSD
-- Když je v síti VLAN
-> Konektor stejného tvaru ještě neznamená DisplayPort v něm.
diff --git a/content/technicke-vybaveni/2-rocnik/02-tisk-a-sken.en.md b/content/technicke-vybaveni/2-rocnik/02-tisk-a-sken.en.md
deleted file mode 100644
index 8963385..0000000
--- a/content/technicke-vybaveni/2-rocnik/02-tisk-a-sken.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Print and scan
-popis: Laser, inkjet, and a network queue.
-nazev_cviceni: Quiz: Print and scan
-nadpis_kvizu: Quiz on Print and scan
----
diff --git a/content/technicke-vybaveni/2-rocnik/02-tisk-a-sken.md b/content/technicke-vybaveni/2-rocnik/02-tisk-a-sken.md
deleted file mode 100644
index 04b0a7d..0000000
--- a/content/technicke-vybaveni/2-rocnik/02-tisk-a-sken.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-2-tisk-a-sken
-puvodni: hw2.2
-nazev: Tisk a sken
-popis: Laser, inkoust a síťová fronta.
-nazev_cviceni: Kvíz: Tisk a sken
-nadpis_kvizu: Kvíz k tématu Tisk a sken
----
-
-# Výklad
-
-## Toner a kapky
-*Princip*
-
-- Laser nabije válec a toner se chytí podle obrazu.
-- Inkoust stříká kapky a hodí se na fotky i občasný tisk.
-- Zaschnutá hlava je častější než porucha desky.
-- Termotransfer na štítky není totéž co kancelářský laser.
-
-> Tip: laser netaví toner na papír sám, dělá to zapékací válec.
-
-## Jedna tiskárna pro učebnu
-*Síť*
-
-- Sdílená tiskárna na počítači žije, jen když je ten počítač zapnutý.
-- Síťová tiskárna poslouchá sama.
-- Fronta na serveru drží ovladač na jednom místě.
-- Špatný ovladač tiskne znaky místo stránky.
-
-> Tip: tiskárna s vlastní adresou má rezervaci v DHCP.
-
-## Obraz zpět do počítače
-*Sken*
-
-- Plochý skener i podavač mají jiný limit gramáže.
-- ADF nezvládne sešité listy.
-- Síťový sken může poslat soubor do složky nebo na poštu.
-- Sklo a podavač se mažou, jinak jsou na stránce pruhy.
-
-> Tip: rozlišení 600 dpi na text stačí, plakát chce víc.
-
-
-# Kvíz
-
-## Co v laserové tiskárně připevní toner k papíru?
-- [x] Zapékací válec
-- Inkoustová hlava
-- Ventilátor zdroje
-- VGA kabel
-> Teplo a tlak toner zataví.
-
-## Kdy sdílená tiskárna na učitelském počítači netiskne?
-- [x] Když je ten počítač vypnutý
-- Když má tiskárna vlastní IP a server
-- Když je v síti DHCP
-- Když je papír v zásobníku
-> Sdílení přes počítač z toho počítače dělá server.
-
-## Proč dát tiskárně rezervaci v DHCP?
-- [x] Aby ovladače mířily pořád na stejnou adresu
-- Aby se zvýšilo rozlišení
-- Aby toner vydržel dýl
-- Aby nešla skenovat
-> Nová adresa po restartu rozbije frontu.
-
-## Co podavač skeneru nezvládne?
-- [x] Sešitou sadu listů
-- Jednotlivé běžné listy
-- Černobílý text
-- Odeslání do složky, pokud to umí firmware
-> Sešit patří na sklo, ne do ADF.
diff --git a/content/technicke-vybaveni/2-rocnik/03-sbernice-a-disky.en.md b/content/technicke-vybaveni/2-rocnik/03-sbernice-a-disky.en.md
deleted file mode 100644
index 35914d7..0000000
--- a/content/technicke-vybaveni/2-rocnik/03-sbernice-a-disky.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Buses and drives
-popis: USB, SATA and NVMe are not interchangeable just because of the plug.
-nazev_cviceni: Quiz: Buses and drives
-nadpis_kvizu: Quiz on Buses and drives
----
diff --git a/content/technicke-vybaveni/2-rocnik/03-sbernice-a-disky.md b/content/technicke-vybaveni/2-rocnik/03-sbernice-a-disky.md
deleted file mode 100644
index 07a4a9a..0000000
--- a/content/technicke-vybaveni/2-rocnik/03-sbernice-a-disky.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-2-sbernice-a-disky
-puvodni: hw2.3
-nazev: Sběrnice a disky
-popis: USB, SATA a NVMe nejsou zaměnitelné jen konektorem.
-nazev_cviceni: Kvíz: Sběrnice a disky
-nadpis_kvizu: Kvíz k tématu Sběrnice a disky
----
-
-# Výklad
-
-## Versie a napájení
-*USB*
-
-- USB 2.0, 3.x a 4 se liší tokem dat.
-- USB-C je tvar, ne záruka rychlosti.
-- Thunderbolt po stejném tvaru umí obraz, síť i disk.
-- Hub bez napájení nestačí pro víc disků.
-
-> Tip: port USB-A 2.0 fyzicky vezme i rychlý flash disk a stáhne ho na svou rychlost.
-
-## Klasické disky
-*SATA*
-
-- HDD i starší SSD sedí na SATA.
-- Rychlost rozhraní je strop, disk může být pomalejší.
-- Kabel bez západky vypadne a disk zmizí z BIOSu.
-- Externí box schová SATA za USB.
-
-> Tip: napájení SATA a datový kabel jsou dva různé vodiče.
-
-## SSD přímo na PCIe
-*NVMe*
-
-- NVMe jede po PCIe a obchází starý řadič SATA.
-- Slot M.2 na desce může umět jen jednu z variant.
-- Dlouhá destička do krátkého slotu nepatří.
-- Bez chlazení rychlé SSD ve špičce zpomalí.
-
-> Tip: M.2 je rozměr destičky. NVMe a SATA na M.2 nejsou totéž.
-
-
-# Kvíz
-
-## Co říká samotný tvar USB-C?
-- [x] Jen tvar konektoru, ne rychlost ani obraz
-- Vždy Thunderbolt
-- Vždy 40 Gb/s
-- Vždy nabíjení 100 W a DisplayPort
-> Schopnosti určuje řadič a kabel, ne obrys.
-
-## Čím se NVMe liší od SATA SSD?
-- [x] Jede po PCIe, ne po starém SATA
-- Nemá paměťové buňky
-- Jde zapojit jen do VGA
-- Nepotřebuje desku
-> Protokol a sběrnice jsou rychlejší cesta.
-
-## Co může umět slot M.2?
-- [x] Jen SATA, jen NVMe, nebo obojí, podle desky
-- Vždy obojí
-- Jen grafickou kartu
-- Jen operační paměť
-> Manuál desky je závaznější než tvar zářezu.
-
-## Proč rychlé SSD ve špičce zpomalí?
-- [x] Přehřeje se a stáhne výkon
-- SATA mu sebere VLAN
-- BIOS maže buňky
-- USB-C ho přepne na 2.0 vždy
-> Řadič hlídá teplotu.
diff --git a/content/technicke-vybaveni/2-rocnik/04-chlazeni.en.md b/content/technicke-vybaveni/2-rocnik/04-chlazeni.en.md
deleted file mode 100644
index a3f19e1..0000000
--- a/content/technicke-vybaveni/2-rocnik/04-chlazeni.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Cooling
-popis: Air needs a way in and a way out.
-nazev_cviceni: Quiz: Cooling
-nadpis_kvizu: Quiz on Cooling
----
diff --git a/content/technicke-vybaveni/2-rocnik/04-chlazeni.md b/content/technicke-vybaveni/2-rocnik/04-chlazeni.md
deleted file mode 100644
index 7c3203d..0000000
--- a/content/technicke-vybaveni/2-rocnik/04-chlazeni.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-2-chlazeni
-puvodni: hw2.4
-nazev: Chlazení
-popis: Vzduch musí mít kudy dovnitř a ven.
-nazev_cviceni: Kvíz: Chlazení
-nadpis_kvizu: Kvíz k tématu Chlazení
----
-
-# Výklad
-
-## Podtlak a přetlak
-*Tok*
-
-- Vpředu a dole se vzduch nasává, vzadu a nahoře odchází.
-- Filtr patří na vstup, ne na výfuk.
-- Zavřená přední stěna designové skříně škrtí přívod.
-- Kabel přes ventilátor je ucpaný filtr.
-
-> Tip: všechny ventilátory foukající dovnitř bez odtahu jen víří prach.
-
-## Chladič musí sednout
-*CPU*
-
-- Vzduchový chladič potřebuje místo k bočnici.
-- Voda odvede teplo do radiátoru, který pořád potřebuje vzduch.
-- Špatně dotažený chladič pustí teplotu hned po startu.
-- Limit teploty v BIOSu je pojistka, ne pracovní režim.
-
-> Tip: pasta není vrstva másla. Má vyplnit nerovnosti.
-
-## Tichý vrah zdroje
-*Prach*
-
-- Zdroj s filtrem se čistí, nezalepuje.
-- Notebookový výfuk do měkké deky se udusí.
-- Po roce bez čištění rostou otáčky a teplota.
-- Čištění stlačeným vzduchem nedrží ventilátor v roztočeném stavu naslepo.
-
-> Tip: vrstva prachu na žebru je izolace.
-
-
-# Kvíz
-
-## Kam patří prachový filtr?
-- [x] Na vstup vzduchu
-- Jen na výfuk vzadu
-- Dovnitř zdroje bez mřížky
-- Na displej
-> Filtruje se to, co teprve vstupuje.
-
-## Co dělá příliš silná vrstva pasty?
-- [x] Zhorší přestup tepla
-- Zvýší takt samo
-- Nahradí chladič
-- Opraví špatný socket
-> Pasta vede hůř než kov chladiče.
-
-## Proč vodní chlazení pořád potřebuje vzduch?
-- [x] Radiátor musí teplo předat do místnosti
-- Voda chladí jen při otevřeném okně skříně
-- Čerpadlo jede na VGA
-- Bez ventilátoru se voda vaří vždy do minuty
-> Okruh teplo přesune, radiátor ho odvede.
-
-## Co udělá notebook na dece?
-- [x] Zakryje sání a přehřeje se
-- Zvýší jas panelu
-- Přepne disk na NVMe
-- Opraví ventilátor
-> Sání bývá zespodu.
diff --git a/content/technicke-vybaveni/2-rocnik/05-sestaveni.en.md b/content/technicke-vybaveni/2-rocnik/05-sestaveni.en.md
deleted file mode 100644
index 710e54f..0000000
--- a/content/technicke-vybaveni/2-rocnik/05-sestaveni.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Building a PC
-popis: An order that does not break the board.
-nazev_cviceni: Quiz: Building a PC
-nadpis_kvizu: Quiz on Building a PC
----
diff --git a/content/technicke-vybaveni/2-rocnik/05-sestaveni.md b/content/technicke-vybaveni/2-rocnik/05-sestaveni.md
deleted file mode 100644
index edca649..0000000
--- a/content/technicke-vybaveni/2-rocnik/05-sestaveni.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-2-sestaveni
-puvodni: hw2.5
-nazev: Sestavení
-popis: Pořadí, které nerozbije desku.
-nazev_cviceni: Kvíz: Sestavení
-nadpis_kvizu: Kvíz k tématu Sestavení
----
-
-# Výklad
-
-## Nejdřív mimo skříň
-*Příprava*
-
-- Deska, procesor, chladič a jeden modul paměti.
-- Zdroj mimo skříň a obraz na základní grafiku.
-- Až potom disk, karty a zbytek paměti.
-- Antistatická podložka a klidné ruce porazí koberec.
-
-> Tip: sestavu nejdřív oživte na stole. Ve skříni se špatně hledá nedosedlá paměť.
-
-## Procesor nemá kam spadnout
-*Socket*
-
-- Trojúhelník na procesoru sedí se značkou na socketu.
-- Páka se zavírá až když procesor leží rovně.
-- Pasta až po usazení, ne do socketu.
-- Chladič se dotahuje křížem, ne jednou stranou nadoraz.
-
-> Tip: ohnutý pin na desce LGA nevrátíte pinzetou naslepo.
-
-## Kabely jsou součást airflow
-*Skříň*
-
-- Nejdřív zdroj a distanční sloupky, potom deska.
-- Napájení desky, procesoru a karty jsou tři různé konektory.
-- Přední USB a tlačítko mají malé konektory podle manuálu.
-- První start se sleduje: ventilátory, obraz, pípnutí nebo LED.
-
-> Tip: distanční sloupky pod deskou musí sedět s otvory, jinak deska zkratuje.
-
-
-# Kvíz
-
-## Proč oživit sestavu nejdřív mimo skříň?
-- [x] Nedosedlá paměť se ve skříni hledá hůř
-- Ve skříni nejde zapnout zdroj
-- BIOS ve skříni neběží
-- Pasta venku schne
-> Minimum dílů na stole ukáže, jestli jádro žije.
-
-## Co se nesmí stát pinům na socketu LGA?
-- [x] Ohnout je
-- Nechat je v socketu
-- Srovnat procesor podle značky
-- Zavřít páku až po usazení
-> Pin na desce je křehký.
-
-## K čemu jsou distanční sloupky?
-- [x] Drží desku dál od plechu skříně
-- Napájí ventilátor
-- Jsou to šrouby do disku
-- Uzemňují jen přední LED
-> Bez nich nebo se špatnými sloupky deska zkratuje.
-
-## Který konektor nepatří do napájení procesoru?
-- [x] PCIe konektor grafické karty místo EPS
-- Hlavní 24pin na desku
-- EPS konektor k socketu
-- SATA napájení disku
-> Konektory si jsou podobné a sedí špatně záměrně.
diff --git a/content/technicke-vybaveni/2-rocnik/06-uefi-a-boot.en.md b/content/technicke-vybaveni/2-rocnik/06-uefi-a-boot.en.md
deleted file mode 100644
index cc6bd07..0000000
--- a/content/technicke-vybaveni/2-rocnik/06-uefi-a-boot.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: UEFI and boot
-popis: What happens before the operating system starts.
-nazev_cviceni: Quiz: UEFI and boot
-nadpis_kvizu: Quiz on UEFI and boot
----
diff --git a/content/technicke-vybaveni/2-rocnik/06-uefi-a-boot.md b/content/technicke-vybaveni/2-rocnik/06-uefi-a-boot.md
deleted file mode 100644
index 51f7fd8..0000000
--- a/content/technicke-vybaveni/2-rocnik/06-uefi-a-boot.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-2-uefi-a-boot
-puvodni: hw2.6
-nazev: UEFI a boot
-popis: Co se děje, než naběhne systém.
-nazev_cviceni: Kvíz: UEFI a boot
-nadpis_kvizu: Kvíz k tématu UEFI a boot
----
-
-# Výklad
-
-## BIOS a UEFI
-*Firmware*
-
-- Firmware otestuje železo a najde disk se systémem.
-- UEFI používá tabulku GPT, starý BIOS často MBR.
-- Secure Boot pustí jen podepsaný zavaděč.
-- Vypnutí Secure Bootu je výjimka, ne výchozí rada.
-
-> Tip: UEFI není jen barevné menu. Umí jiné dělení disku a Secure Boot.
-
-## Z čeho se startuje
-*Pořadí*
-
-- Nabídka bootu umí jednorázově vybrat zařízení.
-- Disk bez zavaděče se v seznamu nechytí.
-- Více systémů potřebuje bootovací nabídku, ne přepisování pořád dokola.
-- Síťový boot PXE hledá obraz na serveru.
-
-> Tip: flash disk v pořadí před systémem nabootuje omylem instalaci.
-
-## Menu není pro celou třídu
-*Heslo*
-
-- Heslo na setup brání přehazování bootu.
-- Heslo na disk je silnější a bez něj jsou data nedostupná.
-- Clear CMOS heslo setupu často smaže a vezme i nastavení.
-- Po změně profilu paměti ověřte, že systém naběhne dvakrát.
-
-> Tip: heslo BIOSu bez záznamu je past, až se bude měnit disk.
-
-
-# Kvíz
-
-## Co Secure Boot hlídá?
-- [x] Aby se spustil jen důvěryhodný zavaděč
-- Aby měl disk aspoň 1 TB
-- Aby ventilátor točil na maximum
-- Aby nešel USB flash disk vůbec zapojit
-> Nepodepsaný zavaděč se nespustí.
-
-## Čím se liší GPT od MBR?
-- [x] GPT je tabulka oddílů pro UEFI a větší disky
-- GPT je typ pasty
-- MBR je jen pro NVMe
-- Jsou to totéž
-> Starý MBR má omezení, na která velký disk naráží.
-
-## Proč nenechat flash disk trvale první v bootu?
-- [x] Omylem z něj naběhne cizí systém
-- Flash disk vypne Secure Boot navždy
-- UEFI flash disky zakazuje
-- Smaže TPM
-> Jednorázová nabídka bootu je bezpečnější.
-
-## Co často udělá vymazání CMOS?
-- [x] Smaže heslo setupu i ostatní volby
-- Smaže jen soubory na ploše
-- Přepne Windows na jiný jazyk
-- Opraví ohnutý pin
-> Je to návrat firmware k výchozímu stavu.
diff --git a/content/technicke-vybaveni/2-rocnik/07-diagnostika.en.md b/content/technicke-vybaveni/2-rocnik/07-diagnostika.en.md
deleted file mode 100644
index 2976cca..0000000
--- a/content/technicke-vybaveni/2-rocnik/07-diagnostika.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Diagnostics
-popis: POST, beep codes, and the smallest build that still starts.
-nazev_cviceni: Quiz: Diagnostics
-nadpis_kvizu: Quiz on Diagnostics
----
diff --git a/content/technicke-vybaveni/2-rocnik/07-diagnostika.md b/content/technicke-vybaveni/2-rocnik/07-diagnostika.md
deleted file mode 100644
index 62ab16e..0000000
--- a/content/technicke-vybaveni/2-rocnik/07-diagnostika.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-2-diagnostika
-puvodni: hw2.7
-nazev: Diagnostika
-popis: POST, pípání a minimum, které ještě naběhne.
-nazev_cviceni: Kvíz: Diagnostika
-nadpis_kvizu: Kvíz k tématu Diagnostika
----
-
-# Výklad
-
-## Test hned po zapnutí
-*POST*
-
-- Firmware zkouší procesor, paměť a grafiku.
-- Kódy na displeji desky jsou přesnější než staré pípání.
-- Reproduktor skříně musí být zapojený, jinak ticho nic neřekne.
-- Manuál desky překládá kódy. Nejsou u všech stejné.
-
-> Tip: žádný obraz a žádné pípnutí neznamená automaticky mrtvý procesor.
-
-## Uberte, co může lhát
-*Minimum*
-
-- Ven grafika, pokud má procesor vlastní obraz.
-- Ven disky a karty.
-- Zkuste jiný kabel obrazu a jiný port.
-- Až jádro naběhne, vracejte díly po jednom.
-
-> Tip: jeden modul paměti v doporučeném slotu porazí čtyři najednou.
-
-## Náhodné pády
-*Paměť a disk*
-
-- Test paměti běží déle než jedno pípnutí.
-- Chyby čtení disku se projeví v jeho vlastních statistikách.
-- Přehřátí shodí stroj až při zátěži, ne v menu BIOSu.
-- Vyměněný díl ověřte stejnou zkouškou, která předtím selhala.
-
-> Tip: pád až ve Windows může být paměť, ne jen ovladač.
-
-
-# Kvíz
-
-## Co POST dělá?
-- [x] Po zapnutí zkouší základní díly
-- Instaluje systém
-- Formátuje SSD
-- Stahuje ovladače z internetu
-> Je to první test firmware, ne operační systém.
-
-## Proč začít s jedním modulem paměti?
-- [x] Špatný modul nebo slot se snáz odhalí
-- Deska víc modulů zakazuje
-- BIOS umí jen 4 GB
-- Druhý modul maže UEFI
-> Minimum dílů zužuje chybu.
-
-## Kdy se přehřátí často neprojeví?
-- [x] V menu BIOSu bez zátěže
-- Při dlouhém testu procesoru
-- Ve hře
-- Při kódování videa
-> Teplota roste až s odběrem.
-
-## Co ověří výměnu dílu?
-- [x] Stejná zkouška, která předtím selhala
-- Jen to, že ventilátor se točí
-- Nová tapeta
-- Jiná barva skříně
-> Bez opakování testu nevíte, jestli to byla příčina.
diff --git a/content/technicke-vybaveni/2-rocnik/08-porty-a-dokovani.en.md b/content/technicke-vybaveni/2-rocnik/08-porty-a-dokovani.en.md
deleted file mode 100644
index 6825093..0000000
--- a/content/technicke-vybaveni/2-rocnik/08-porty-a-dokovani.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Ports and docks
-popis: What a laptop can actually drive.
-nazev_cviceni: Quiz: Ports and docks
-nadpis_kvizu: Quiz on Ports and docks
----
diff --git a/content/technicke-vybaveni/2-rocnik/08-porty-a-dokovani.md b/content/technicke-vybaveni/2-rocnik/08-porty-a-dokovani.md
deleted file mode 100644
index 2acd091..0000000
--- a/content/technicke-vybaveni/2-rocnik/08-porty-a-dokovani.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-2-porty-a-dokovani
-puvodni: hw2.8
-nazev: Porty a dokování
-popis: Co z notebooku opravdu vyleze.
-nazev_cviceni: Kvíz: Porty a dokování
-nadpis_kvizu: Kvíz k tématu Porty a dokování
----
-
-# Výklad
-
-## Jeden kabel, mnoho omezení
-*Dok*
-
-- Dok může nést obraz, síť i napájení.
-- Limit je nejslabší článek: port, kabel nebo dok.
-- Dva externí monitory chtějí port, který to umí, ne přání uživatele.
-- Napájení přes dok nemusí stačit hernímu notebooku.
-
-> Tip: levný USB hub není dok. Nepřenese obraz, který port neumí.
-
-## Kabel z notebooku
-*Síť*
-
-- USB adaptér je síťová karta mimo šasi.
-- Rychlost adaptéru bývá 1 Gb/s, ne rychlost školní páteře.
-- Dok s vlastní síťovkou má vlastní MAC adresu.
-- Rezervace DHCP na MAC notebooku pak neplatí pro dok.
-
-> Tip: mnoho tenkých notebooků už ethernet nemá a potřebuje adaptér.
-
-## Na co se ptát
-*Výběr*
-
-- Kolik obrazů, jaké rozlišení a jestli se notebook nabíjí.
-- Jestli síť jede z doku, nebo se čeká Wi-Fi.
-- Jestli čtečka karet a USB sedí verzi, kterou škola používá.
-- Manuál portu je důležitější než fotka konektoru.
-
-> Tip: stejný USB-C na dvou noteboocích nemá stejné schopnosti.
-
-
-# Kvíz
-
-## Kdy USB-C dok nepřenese dva monitory?
-- [x] Když port notebooku umí jen data
-- Když je dok černý
-- Když je v učebně VLAN
-- Když má disk NVMe
-> Dok neumí vyrobit schopnost, kterou port nemá.
-
-## Proč DHCP rezervace na notebook neplatí přes dok?
-- [x] Dok má vlastní MAC adresu
-- DHCP doky zakazuje
-- VLAN maže rezervace
-- Ethernet nemá MAC
-> Rezervace sedí na adresu, kterou server vidí.
-
-## Co je nejslabší článek řetězu obrazu?
-- [x] Port, kabel nebo dok, podle toho, co umí nejmíň
-- Vždy monitor
-- Vždy procesor
-- Vždy tapeta
-> Datový tok musí projít všemi.
-
-## Čím se liší USB hub od doku s obrazem?
-- [x] Hub sám o sobě obraz z datového portu nevyrobí
-- Hub je vždy rychlejší
-- Dok neumí napájení
-- Jsou to totéž
-> Obraz chce alternativní režim nebo DisplayPort v kabelu.
diff --git a/content/technicke-vybaveni/3-rocnik/01-raid.en.md b/content/technicke-vybaveni/3-rocnik/01-raid.en.md
deleted file mode 100644
index 20e2144..0000000
--- a/content/technicke-vybaveni/3-rocnik/01-raid.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: RAID
-popis: Several drives as one volume, not as a backup.
-nazev_cviceni: Quiz: RAID
-nadpis_kvizu: Quiz on RAID
----
diff --git a/content/technicke-vybaveni/3-rocnik/01-raid.md b/content/technicke-vybaveni/3-rocnik/01-raid.md
deleted file mode 100644
index 1bd05df..0000000
--- a/content/technicke-vybaveni/3-rocnik/01-raid.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-3-raid
-puvodni: hw3.1
-nazev: RAID
-popis: Více disků jako jeden svazek, ne jako záloha.
-nazev_cviceni: Kvíz: RAID
-nadpis_kvizu: Kvíz k tématu RAID
----
-
-# Výklad
-
-## RAID 1
-*Zrcadlo*
-
-- Stejná data jsou na dvou discích.
-- Kapacita je jako jeden disk.
-- Čtení může být rychlejší, zápis ne dvojnásobný.
-- Po výměně disku se pole musí obnovit.
-
-> Tip: zrcadlo přežije pád jednoho disku, ne smazání souboru.
-
-## RAID 0 a 5
-*Prokládání*
-
-- RAID 0 skládá rychlost a kapacitu a nemá ochranu.
-- RAID 5 snese pád jednoho disku z aspoň tří.
-- Parita zabere kapacitu jednoho disku.
-- Obnova velkého pole trvá dlouho a druhý pád v té době je zkáza.
-
-> Tip: RAID 0 při pádu jednoho disku ztratí všechno.
-
-## Hardware a software
-*Řadič*
-
-- Hardwarový řadič má vlastní cache a baterii.
-- Softwarové pole řeší systém a je přenosnější.
-- Cache bez ochrany při výpadku proudu rozbije zápis.
-- RAID nenahrazuje zálohu na jiné místo.
-
-> Tip: pole z jiného řadiče v novém serveru nemusí naběhnout.
-
-
-# Kvíz
-
-## Co RAID 1 přežije?
-- [x] Pád jednoho z dvojice disků
-- Smazání souboru uživatelem
-- Požár místnosti
-- Ransomware na obou discích naráz
-> Oba disky mají stejná data, včetně chyby, kterou tam zapíšete.
-
-## Co je RAID 0?
-- [x] Prokládání bez ochrany při pádu disku
-- Zrcadlo
-- Záloha na pásku
-- Šifrování disku
-> Rychlost a kapacita, nulová odolnost.
-
-## Proč RAID není záloha?
-- [x] Neochrání před smazáním, malwarem ani požárem skříně
-- Protože neumí víc než dva disky
-- Protože Windows RAID zakazuje
-- Protože parita maže soubory
-> Záloha je další kopie jinde a v jiném čase.
-
-## Co hrozí velkému poli při obnově?
-- [x] Druhý disk spadne dřív, než se obnova dopočítá
-- Pole se samo přepne na RAID 0
-- BIOS smaže paritu
-- NIC se při obnově nesmí točit
-> Obnova je dlouhá a disky jsou stejně staré.
diff --git a/content/technicke-vybaveni/3-rocnik/02-nas-a-site-disku.en.md b/content/technicke-vybaveni/3-rocnik/02-nas-a-site-disku.en.md
deleted file mode 100644
index dc00365..0000000
--- a/content/technicke-vybaveni/3-rocnik/02-nas-a-site-disku.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: NAS and storage networks
-popis: Drives that are not inside one computer.
-nazev_cviceni: Quiz: NAS and storage networks
-nadpis_kvizu: Quiz on NAS and storage networks
----
diff --git a/content/technicke-vybaveni/3-rocnik/02-nas-a-site-disku.md b/content/technicke-vybaveni/3-rocnik/02-nas-a-site-disku.md
deleted file mode 100644
index d2515bb..0000000
--- a/content/technicke-vybaveni/3-rocnik/02-nas-a-site-disku.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-3-nas-a-site-disku
-puvodni: hw3.2
-nazev: NAS a sítě disků
-popis: Disky, které nejsou uvnitř jednoho počítače.
-nazev_cviceni: Kvíz: NAS a sítě disků
-nadpis_kvizu: Kvíz k tématu NAS a sítě disků
----
-
-# Výklad
-
-## Souborový server v krabici
-*NAS*
-
-- Nabízí složky po síti.
-- Má vlastní účty nebo se ptá školního adresáře.
-- Dva porty se dají spojit, ale jen když to umí i switch.
-- Hlučný diskový provoz nepatří do tiché učebny.
-
-> Tip: NAS v učebně bez zálohy je jen jeden košík na všechna data.
-
-## Disk jako místní
-*SAN*
-
-- Server vidí disk, i když leží jinde.
-- Síť pro SAN se nemíchá s provozem žáků.
-- Špatné zónování vystaví disk cizímu serveru.
-- Výpadek této sítě shodí systémy, které na disku jedou.
-
-> Tip: SAN nedává složku, dává blokové zařízení.
-
-## Práva a snímky
-*Soubor*
-
-- Práva na složce mají sedět se skupinami, ne s jedním účtem učitele.
-- Snímek umí vrátit soubor smazaný dopoledne.
-- Plný svazek zastaví i zdánlivě nesouvisející službu.
-- Kapacita se hlídá dřív, než svítí červeně.
-
-> Tip: snímek disku není záloha, dokud leží na stejném poli.
-
-
-# Kvíz
-
-## Co NAS obvykle nabízí?
-- [x] Složky po síti
-- Blokový disk celé škole bez oprávnění
-- Náhradu procesoru v noteboocích
-- Obraz monitoru
-> Je to souborová služba.
-
-## Čím se SAN liší?
-- [x] Server dostane disk jako bloky, ne jako složku
-- Je to jen jiné jméno pro USB flash disk
-- Jede jen po Wi-Fi
-- Nemá disky
-> Souborový systém si na těch blocích dělá až server.
-
-## Proč snímek na stejném poli není záloha?
-- [x] Pád pole vezme i snímek
-- Snímek nejde vytvořit
-- Snímek maže RAID
-- SMB snímky zakazuje
-> Záloha musí přežít ztrátu původního úložiště.
-
-## Kam nepatří hlučné diskové pole?
-- [x] Do tiché učebny vedle žáků
-- Do racku s odtahem
-- Do místnosti se zámek
-- Do sítě oddělené od hostů
-> Hluk a přístup jsou součást návrhu.
diff --git a/content/technicke-vybaveni/3-rocnik/03-servery-a-rack.en.md b/content/technicke-vybaveni/3-rocnik/03-servery-a-rack.en.md
deleted file mode 100644
index 0542f14..0000000
--- a/content/technicke-vybaveni/3-rocnik/03-servery-a-rack.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Servers and racks
-popis: A cabinet that is not a gaming PC on its side.
-nazev_cviceni: Quiz: Servers and racks
-nadpis_kvizu: Quiz on Servers and racks
----
diff --git a/content/technicke-vybaveni/3-rocnik/03-servery-a-rack.md b/content/technicke-vybaveni/3-rocnik/03-servery-a-rack.md
deleted file mode 100644
index 5450904..0000000
--- a/content/technicke-vybaveni/3-rocnik/03-servery-a-rack.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-3-servery-a-rack
-puvodni: hw3.3
-nazev: Servery a rack
-popis: Skříň, do které se nedává herní počítač naležato.
-nazev_cviceni: Kvíz: Servery a rack
-nadpis_kvizu: Kvíz k tématu Servery a rack
----
-
-# Výklad
-
-## Racková jednotka
-*Tvar*
-
-- Server v racku se měří v jednotkách U.
-- Kolejnice musí unést hloubku skříně.
-- Napájecí lišta není prodlužovačka z kuchyně.
-- Proud a teplo se počítají dopředu, ne až když jistič spadne.
-
-> Tip: 1U je výška, ne výkon.
-
-## Studená ulička
-*Vzduch*
-
-- Studený vzduch jde před servery, teplý za ně.
-- Zaslepení prázdných pozic nutí vzduch přes stroje.
-- Kabelová armáda před sáním je ucpávka.
-- Teplota v místnosti není teplota uvnitř 1U serveru.
-
-> Tip: servery v racku sají vepředu a foukají dozadu. Otočený kus kazí ostatním.
-
-## Když nejste ve škole
-*Vzdáleně*
-
-- IPMI nebo iDRAC žije i když systém ne.
-- Patří do správní sítě, ne do učebny.
-- Konzole v racku je záloha, když síť správy spadne.
-- Štítek na serveru říká, co na něm jede.
-
-> Tip: vzdálená správa má vlastní port a vlastní heslo.
-
-
-# Kvíz
-
-## Co znamená 1U?
-- [x] Výšku zařízení v racku
-- Výkon jednoho jádra
-- Rychlost disku
-- Počet zdrojů
-> Je to rozměr, ne třída výkonu.
-
-## Kudy má server v racku sát vzduch?
-- [x] Vepředu a foukat dozadu
-- Zezadu dopředu
-- Jen shora bez mřížky
-- Z podlahy dírami v desce
-> Otočený server ohřívá sousedům sání.
-
-## K čemu je vzdálená správa serveru?
-- [x] Ovládáte ho, i když systém neběží
-- Nahrazuje zálohu dat
-- Je to Wi-Fi pro žáky
-- Maže RAID
-> Vidíte obraz a umíte stroj zapnout po síti.
-
-## Kam nepatří port vzdálené správy?
-- [x] Do žákovské VLAN
-- Do správní sítě
-- Za firewall
-- Na heslo jiné než admin na lístečku
-> Je to klíče od serveru.
diff --git a/content/technicke-vybaveni/3-rocnik/04-virtualizace.en.md b/content/technicke-vybaveni/3-rocnik/04-virtualizace.en.md
deleted file mode 100644
index 44f539c..0000000
--- a/content/technicke-vybaveni/3-rocnik/04-virtualizace.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Virtualisation
-popis: Several systems on one piece of hardware.
-nazev_cviceni: Quiz: Virtualisation
-nadpis_kvizu: Quiz on Virtualisation
----
diff --git a/content/technicke-vybaveni/3-rocnik/04-virtualizace.md b/content/technicke-vybaveni/3-rocnik/04-virtualizace.md
deleted file mode 100644
index 35666a2..0000000
--- a/content/technicke-vybaveni/3-rocnik/04-virtualizace.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-3-virtualizace
-puvodni: hw3.4
-nazev: Virtualizace
-popis: Více systémů na jednom železe.
-nazev_cviceni: Kvíz: Virtualizace
-nadpis_kvizu: Kvíz k tématu Virtualizace
----
-
-# Výklad
-
-## Kdo rozděluje železo
-*Hypervizor*
-
-- Hypervizor pouští více systémů nad jedním hardwarem.
-- Každý systém má vlastní diskový soubor a virtuální síťovku.
-- Paměť, kterou slíbíte navíc, se ve špičce potká.
-- Pád fyzického serveru shodí všechny hosty naráz.
-
-> Tip: virtuální počítač nemá vlastní zdroj. Dělí se o ten fyzický.
-
-## Virtuální switch
-*Síť*
-
-- Virtuální switch propojí hosty mezi sebou.
-- Most je pustí ven fyzickým portem.
-- Izolovaná síť jen mezi virtuály ven nevede.
-- Špatný trunk vystaví správní virtuál do učebny.
-
-> Tip: VLAN ve virtuálu musí sedět s trunkem na fyzické kartě.
-
-## Soubor není kouzlo
-*Disk*
-
-- Tenké provizionování slíbí víc, než je na začátku obsazeno.
-- Snímek před změnou umí vrátit systém.
-- Zapomenuté snímky zpomalí disk a zaplní úložiště.
-- Záloha virtuálu patří mimo ten samý disk.
-
-> Tip: tenký disk roste a může zaplnit svazek, na kterém leží.
-
-
-# Kvíz
-
-## Co se stane, když spadne fyzický server s pěti virtuály?
-- [x] Spadnou všechny, pokud nemají kam utéct
-- Přežije aspoň správní virtuál
-- Hypervizor je přenese na notebook
-- Nic, virtuál zdroj nepotřebuje
-> Sdílejí jedno železo.
-
-## K čemu je most virtuálního switche?
-- [x] Pustí virtuál do fyzické sítě
-- Smaže VLAN
-- Nahradí RAID
-- Zapne Secure Boot hostitele
-> Bez mostu virtuál ven neodejde.
-
-## Čím je tenký disk nebezpečný?
-- [x] Doroste a zaplní úložiště pod sebou
-- Nejde zálohovat
-- Nemá souborový systém
-- Vypne hypervizor
-> Slíbená kapacita není volné místo.
-
-## Co udělá zapomenutý snímek?
-- [x] Roste a zpomaluje disk
-- Smaže heslo BIOSu
-- Opraví RAID 0
-- Přidá paměť
-> Snímek je dočasná pojistka, ne trvalý režim.
diff --git a/content/technicke-vybaveni/3-rocnik/05-zalohovani.en.md b/content/technicke-vybaveni/3-rocnik/05-zalohovani.en.md
deleted file mode 100644
index 29c47a1..0000000
--- a/content/technicke-vybaveni/3-rocnik/05-zalohovani.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Backups
-popis: A copy you can actually restore.
-nazev_cviceni: Quiz: Backups
-nadpis_kvizu: Quiz on Backups
----
diff --git a/content/technicke-vybaveni/3-rocnik/05-zalohovani.md b/content/technicke-vybaveni/3-rocnik/05-zalohovani.md
deleted file mode 100644
index 8cd8db8..0000000
--- a/content/technicke-vybaveni/3-rocnik/05-zalohovani.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-3-zalohovani
-puvodni: hw3.5
-nazev: Zálohování
-popis: Kopie, kterou umíte vrátit.
-nazev_cviceni: Kvíz: Zálohování
-nadpis_kvizu: Kvíz k tématu Zálohování
----
-
-# Výklad
-
-## Tři místa
-*Pravidlo*
-
-- Jedna kopie je málo, když leží u originálu.
-- Druhá má být mimo stroj a třetí mimo budovu, když na tom záleží.
-- Verze v čase vrátí soubor z pátku, ne jen ze včerejška.
-- Šifrovaná záloha bez klíče je hromada nic.
-
-> Tip: kopie na tom samém disku přežije jen omyl v koši, ne pád disku.
-
-## Ne jen plochu
-*Co zálohovat*
-
-- Data, účty a konfigurace sítě jsou různé věci.
-- Poštovní schránka v cloudu má vlastní nastavení retence.
-- Databáze se nezálohuje kopírováním otevřeného souboru naslepo.
-- Seznam, co se nezálohuje, má být vědomý.
-
-> Tip: obraz systému bez dat školy je pěkná, ale prázdná záchrana.
-
-## Obnova je součást zálohy
-*Zkouška*
-
-- Jednou za čas obnovte soubor i celý systém nanečisto.
-- Měřte, jak dlouho obnova trvá.
-- Zápis říká, kdo má klíče a kde leží média.
-- Po ransomware rozhoduje stáří poslední čisté kopie.
-
-> Tip: záloha, kterou jste nikdy neobnovili, je hypotéza.
-
-
-# Kvíz
-
-## Proč nestačí kopie na stejném disku?
-- [x] Pád disku vezme originál i kopii
-- Systém druhou složku zakazuje
-- RAID ji sám smaže
-- NTFS umí jen jeden soubor
-> Chyba média je společná.
-
-## Co není pořádná zkouška zálohy?
-- [x] Jen to, že noční úloha svítí zeleně
-- Obnova jednoho souboru
-- Změřený čas obnovy systému
-- Ověření, že klíč k šifře existuje
-> Zelená fajfka neznamená, že data jdou otevřít.
-
-## Co rozhoduje po ransomware?
-- [x] Jestli existuje čistá kopie starší než nákaza
-- Barva skříně
-- Počet ventilátorů
-- Verze spořiče obrazovky
-> Šifrovaná záloha z doby, kdy už malware běžel, je taky šifrovaná.
-
-## Proč nekopírovat otevřenou databázi jako obyčejný soubor?
-- [x] Rozpracovaný zápis bude nekonzistentní
-- Databáze nemá soubor
-- SMB databáze zakazuje
-- Kopie by smazala RAID
-> Běžící databáze se zálohuje nástrojem, který umí konzistentní snímek.
diff --git a/content/technicke-vybaveni/3-rocnik/06-ups.en.md b/content/technicke-vybaveni/3-rocnik/06-ups.en.md
deleted file mode 100644
index 824ef90..0000000
--- a/content/technicke-vybaveni/3-rocnik/06-ups.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: UPS
-popis: Minutes to shut down cleanly, not a spare power station.
-nazev_cviceni: Quiz: UPS
-nadpis_kvizu: Quiz on UPS
----
diff --git a/content/technicke-vybaveni/3-rocnik/06-ups.md b/content/technicke-vybaveni/3-rocnik/06-ups.md
deleted file mode 100644
index 8460d6a..0000000
--- a/content/technicke-vybaveni/3-rocnik/06-ups.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-3-ups
-puvodni: hw3.6
-nazev: UPS
-popis: Minuty na uložení, ne náhradní elektrárna.
-nazev_cviceni: Kvíz: UPS
-nadpis_kvizu: Kvíz k tématu UPS
----
-
-# Výklad
-
-## Překlenout výpadek
-*Úkol*
-
-- Při výpadku přejde na baterii.
-- Čas běhu závisí na odběru, ne na nálepce bez zátěže.
-- Server dostane signál a vypne se po pořádku.
-- Tiskárna a monitor na UPS servery okrádají o minuty.
-
-> Tip: malá UPS neutáhne učebnu. Utáhne server, než se uloží.
-
-## Ne každá UPS je pro zdroj serveru
-*Tvar*
-
-- Sinusový výstup je jistější pro aktivní PFC zdroje.
-- Výkon ve voltampérech není totéž co watty na štítku zdroje.
-- Přetížená UPS spadne právě ve výpadku.
-- Zkouška pod zátěží ukáže skutečné minuty.
-
-> Tip: levná UPS s obdélníkovým průběhem modernímu zdroji vadí.
-
-## Spotřební díl
-*Baterie*
-
-- Test baterie nečeká na první bouřku.
-- Výměna se dělá dřív, než článek nabobtná.
-- Teplá místnost baterii ničí rychleji.
-- UPS není náhrada za zálohu dat.
-
-> Tip: baterie po třech letech v teple už slíbené minuty nedá.
-
-
-# Kvíz
-
-## K čemu malá UPS u serveru je?
-- [x] Aby se systém stihl uložit a vypnout
-- Aby učebna jela celé odpoledne
-- Aby nahradila zálohu disků
-- Aby chladila rack
-> Jsou to minuty, ne směna.
-
-## Proč na UPS nepatří laserová tiskárna?
-- [x] Má velký odběr a sebere čas serverům
-- Tiskárna UPS zničí vždy okamžitě
-- USB tiskárny UPS zakazuje
-- Toner potřebuje jinou frekvenci
-> Špička zapékání baterii vybíjí zbytečně.
-
-## Co ukáže jen nálepka bez zátěže?
-- [x] Optimistický čas, který v provozu neplatí
-- Přesné minuty při plném serveru
-- Stav RAID
-- Teplotu procesoru
-> Měří se s tím, co na UPS opravdu visí.
-
-## Jak často baterie přestane držet slib?
-- [x] Po letech, dřív v teple
-- Nikdy, je součástí zdroje navždy
-- Až po deseti minutách prvního provozu
-- Jen když je UPS černá
-> Je to spotřební díl a má se zkoušet.
diff --git a/content/technicke-vybaveni/3-rocnik/07-firmware.en.md b/content/technicke-vybaveni/3-rocnik/07-firmware.en.md
deleted file mode 100644
index e5d8139..0000000
--- a/content/technicke-vybaveni/3-rocnik/07-firmware.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Firmware
-popis: An update that is not applied in the middle of a lesson.
-nazev_cviceni: Quiz: Firmware
-nadpis_kvizu: Quiz on Firmware
----
diff --git a/content/technicke-vybaveni/3-rocnik/07-firmware.md b/content/technicke-vybaveni/3-rocnik/07-firmware.md
deleted file mode 100644
index 1c2535b..0000000
--- a/content/technicke-vybaveni/3-rocnik/07-firmware.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-3-firmware
-puvodni: hw3.7
-nazev: Firmware
-popis: Aktualizace, která nejde udělat uprostřed hodiny.
-nazev_cviceni: Kvíz: Firmware
-nadpis_kvizu: Kvíz k tématu Firmware
----
-
-# Výklad
-
-## Program v železe
-*Co to je*
-
-- Deska, disk, switch i BIOS mají vlastní verzi.
-- Chyba ve firmware shodí zařízení bez ohledu na systém.
-- Výrobce vydává opravy i roky po prodeji.
-- Neznámá verze se nejdřív zjistí, potom se teprve stahuje.
-
-> Tip: firmware není ovladač ve Windows. Žije v zařízení i bez systému.
-
-## Jedno zařízení, zdroj, záloha
-*Postup*
-
-- Přečtěte, co verze opravuje a co rozbije.
-- Záloha konfigurace switche jde bokem.
-- Napájení nesmí vypadnout v půlce zápisu.
-- Po dokončení ověřte, že naběhla i služba, ne jen dioda.
-
-> Tip: aktualizace při bouřce bez UPS je způsob, jak dostat cihlu.
-
-## Ne všechno v pátek odpoledne
-*Řada*
-
-- Nejdřív méně důležité zařízení.
-- Okno, kdy smí síť ležet, je domluvené.
-- Když nová verze zlobí, musíte umět předchozí.
-- Zápis verze patří k zařízení, ne do hlavy jednoho člověka.
-
-> Tip: neaktualizujte jádro sítě a jediný server ve stejné hodině.
-
-
-# Kvíz
-
-## Čím se firmware liší od ovladače?
-- [x] Je v zařízení a běží i bez operačního systému
-- Je to jen soubor na ploše
-- Maže se s přeinstalací vždy
-- Umí ho jen tiskárna
-> BIOS a firmware disku přežijí nový systém.
-
-## Co před aktualizací switche uložíte?
-- [x] Konfiguraci a zajistíte napájení
-- Jen tapetu na ploše
-- Heslo Wi-Fi na tabuli
-- Nic, aktualizace si pamatuje sama
-> Přerušený zápis a chybějící záloha jsou dvě pasti.
-
-## Proč neaktualizovat jádro a jediný server naráz?
-- [x] Když obojí selže, nemáte z čeho učit ani spravovat
-- Výrobce to technicky zakazuje vždy
-- Firmware umí jen jedno zařízení na budovu
-- UPS to nesežere
-> Nechte si cestu zpět.
-
-## Co po aktualizaci ověříte?
-- [x] Že služba opravdu naběhla, ne jen že svítí dioda
-- Že je krabice teplejší
-- Že se změnila barva menu
-- Nic, firmware se neověřuje
-> Start zařízení není totéž co fungující síť.
diff --git a/content/technicke-vybaveni/3-rocnik/08-mereni-vykonu.en.md b/content/technicke-vybaveni/3-rocnik/08-mereni-vykonu.en.md
deleted file mode 100644
index bc59680..0000000
--- a/content/technicke-vybaveni/3-rocnik/08-mereni-vykonu.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Measuring performance
-popis: A number without the conditions means nothing.
-nazev_cviceni: Quiz: Measuring performance
-nadpis_kvizu: Quiz on Measuring performance
----
diff --git a/content/technicke-vybaveni/3-rocnik/08-mereni-vykonu.md b/content/technicke-vybaveni/3-rocnik/08-mereni-vykonu.md
deleted file mode 100644
index dc8d55b..0000000
--- a/content/technicke-vybaveni/3-rocnik/08-mereni-vykonu.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-3-mereni-vykonu
-puvodni: hw3.8
-nazev: Měření výkonu
-popis: Číslo bez podmínek nic neznamená.
-nazev_cviceni: Kvíz: Měření výkonu
-nadpis_kvizu: Kvíz k tématu Měření výkonu
----
-
-# Výklad
-
-## Úzké hrdlo
-*Co měřit*
-
-- Sledujte procesor, paměť, disk a síť zvlášť.
-- Vytížení 100 % chvíli není chyba, trvale ano.
-- Fronta na disku řekne víc než otáčky ventilátoru.
-- Jedno číslo z herního testu není návrh školního serveru.
-
-> Tip: rychlý procesor nezachrání pomalý disk, na kterém systém čeká.
-
-## Stejná zkouška
-*Podmínky*
-
-- Studený start a druhý běh z cache nejsou totéž.
-- Notebook na baterii schválně zpomalí.
-- Antivirus v testu umí sežrat výsledek.
-- Zapište verzi systému i to, co na pozadí jelo.
-
-> Tip: srovnávejte stejný test, stejná data a stejný čas běhu.
-
-## Kdy dokoupit
-*Rozhodnutí*
-
-- Málo paměti žene systém na disk a všechno zpomalí.
-- Plný a pomalý disk kazí i silný procesor.
-- Slabé Wi-Fi nezrychlíte výměnou grafické karty.
-- Nákup bez měření je odhad.
-
-> Tip: nejdřív paměť nebo disk, ne automaticky nejnovější procesor.
-
-
-# Kvíz
-
-## Co neznamená krátká špička procesoru na 100 %?
-- [x] Že je procesor nutně slabý
-- Že je všechno v pořádku navždy
-- Že disk neexistuje
-- Že se má hned vyměnit deska
-> Záleží, jestli špička trvá a jestli na ní někdo čeká.
-
-## Proč druhý běh testu bývá rychlejší?
-- [x] Data už jsou v cache
-- Procesor se sám přetaktuje natrvalo
-- Disk se zmenší
-- BIOS test podruhé vynechá
-> Cache není výkon železa v prvním čtení.
-
-## Co často zpomalí celý systém dřív než procesor?
-- [x] Málo paměti a pomalý disk
-- Příliš mnoho USB myš
-- Barva skříně
-- Počet ikon na ploše
-> Systém pak pořád čeká na stránkování.
-
-## Co k výsledku testu patří zapsat?
-- [x] Verze, napájení a co jelo na pozadí
-- Jen nejvyšší číslo
-- Jméno učitele
-- Heslo k BIOSu
-> Bez podmínek číslo nejde zopakovat.
diff --git a/content/technicke-vybaveni/4-rocnik/01-udrzba.en.md b/content/technicke-vybaveni/4-rocnik/01-udrzba.en.md
deleted file mode 100644
index dbc5959..0000000
--- a/content/technicke-vybaveni/4-rocnik/01-udrzba.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Maintenance
-popis: Cleaning and checks before something burns.
-nazev_cviceni: Quiz: Maintenance
-nadpis_kvizu: Quiz on Maintenance
----
diff --git a/content/technicke-vybaveni/4-rocnik/01-udrzba.md b/content/technicke-vybaveni/4-rocnik/01-udrzba.md
deleted file mode 100644
index 843f225..0000000
--- a/content/technicke-vybaveni/4-rocnik/01-udrzba.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-4-udrzba
-puvodni: hw4.1
-nazev: Údržba
-popis: Čištění a revize dřív, než něco shoří.
-nazev_cviceni: Kvíz: Údržba
-nadpis_kvizu: Kvíz k tématu Údržba
----
-
-# Výklad
-
-## Řád, ne až když hučí
-*Prašnost*
-
-- Filtry se perou dřív, než zčernají.
-- Zdroj se neotevírá. Čistí se zvenku.
-- Klávesnice a myš učebny jsou spotřební a špinavé.
-- Zápis, kdy se učebna dělala, příště určí interval.
-
-> Tip: otevřená skříň vysátá naslepo umí nasát prach dovnitř zdroje.
-
-## Uvolněný kabel vypadá jako mrtvá deska
-*Kontakty*
-
-- Oxidy na stolních konektorech kazí obraz i USB.
-- Ohnutý konektor HDMI se do portu necpe silou.
-- Napájecí kabel s poškozenou izolací se vyřadí, nelepí páskou.
-- Prodlužovačka pod kobercem je požár, ne úspora místa.
-
-> Tip: než objednáte desku, domáčkněte napájení a vyzkoušejte jiný kabel obrazu.
-
-## Co ve třídě vůbec je
-*Evidence*
-
-- Štítek, umístění a rok pořízení.
-- Záruka a doklad nejsou v šuplíku jednoho člověka.
-- Odepsaný stroj se nevrací do sítě s původním účtem.
-- Pravidelná prohlídka je levnější než hodina bez učebny.
-
-> Tip: bez inventárního čísla nepoznáte, který zdroj se vrací do záruky.
-
-
-# Kvíz
-
-## Co se při údržbě se zdrojem nedělá?
-- [x] Neotevírá se
-- Omývá se mu mřížka zvenku
-- Kontroluje se kabel
-- Píše se, kdy byl čištěný
-> Uvnitř je napětí i po odpojení.
-
-## Co uděláte s kabelem s porušenou izolací?
-- [x] Vyřadíte ho
-- Zalepíte izolepou a vrátíte pod koberec
-- Schováte ho za skříň
-- Dáte ho k žákovskému počítači
-> Poškozená izolace je požár a úraz.
-
-## K čemu je inventární číslo?
-- [x] Aby šlo dohledat záruku a umístění
-- Aby zrychlilo procesor
-- Nahrazuje heslo BIOSu
-- Je to sériové číslo Windows
-> Bez něj je reklamace hádanka.
-
-## Kam nepatří odepsaný počítač?
-- [x] Zpět do sítě s původními účty
-- Do evidence odpadu
-- K smazání disků
-- Mimo žákovskou VLAN
-> Starý účet na mrtvém stroji je pořád díra, když ho někdo zapne.
diff --git a/content/technicke-vybaveni/4-rocnik/02-tpm-a-secure-boot.en.md b/content/technicke-vybaveni/4-rocnik/02-tpm-a-secure-boot.en.md
deleted file mode 100644
index 39ba2c3..0000000
--- a/content/technicke-vybaveni/4-rocnik/02-tpm-a-secure-boot.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: TPM and Secure Boot
-popis: Hardware that watches the system start.
-nazev_cviceni: Quiz: TPM and Secure Boot
-nadpis_kvizu: Quiz on TPM and Secure Boot
----
diff --git a/content/technicke-vybaveni/4-rocnik/02-tpm-a-secure-boot.md b/content/technicke-vybaveni/4-rocnik/02-tpm-a-secure-boot.md
deleted file mode 100644
index 769729c..0000000
--- a/content/technicke-vybaveni/4-rocnik/02-tpm-a-secure-boot.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-4-tpm-a-secure-boot
-puvodni: hw4.2
-nazev: TPM a Secure Boot
-popis: Železo, které hlídá start systému.
-nazev_cviceni: Kvíz: TPM a Secure Boot
-nadpis_kvizu: Kvíz k tématu TPM a Secure Boot
----
-
-# Výklad
-
-## Klíče mimo disk
-*TPM*
-
-- Čip drží klíče použité při startu a šifrování disku.
-- Bez něj některé systémy šifrování disku odmítnou.
-- Vymazání TPM bez zálohy klíčů znamená ztrátu přístupu k disku.
-- Na virtuálu bývá TPM také virtuální a patří k tomu stroji.
-
-> Tip: TPM není heslo, které uživatel píše při každém startu.
-
-## Řetěz důvěry
-*Start*
-
-- Firmware ověří zavaděč.
-- Zavaděč ověří jádro.
-- Cizí systém z flash disku bez podpisu nenaběhne.
-- V laboratoři se výjimka zapíše, nenechá vypnutá navždy.
-
-> Tip: Secure Boot a TPM řeší start, ne to, co uživatel spustí odpoledne.
-
-## Kensington není šifra
-*Zámek*
-
-- Zámek skříně zpomalí toho, kdo chce odnést RAM.
-- Heslo BIOSu zpomalí toho, kdo chce přeházet boot.
-- Šifra disku zastaví toho, kdo disk odnese.
-- Všechny tři vrstvy řeší jiného zloděje.
-
-> Tip: lankový zámek zdrží odnesení, data chrání šifrování disku.
-
-
-# Kvíz
-
-## Co TPM drží?
-- [x] Klíče pro start a šifrování
-- Zálohu všech dokumentů školy
-- Obraz monitoru
-- Seznam VLAN
-> Není to úložiště souborů.
-
-## Co se stane po vymazání TPM bez zálohy klíčů?
-- [x] Šifrovaný disk nemusí jít otevřít
-- Zrychlí se start
-- Smaže se jen spořič
-- Nic, TPM klíče nepoužívá
-> Klíč v čipu bez kopie je pryč.
-
-## Co Secure Boot nezastaví?
-- [x] Program, který uživatel spustí v už naběhlém systému
-- Nepodepsaný zavaděč
-- Cizí systém z flashe, pokud podpis chybí
-- Změnu řetězu startu
-> Hlídá start, ne každé pozdější kliknutí.
-
-## Co ochrání data na odcizeném disku?
-- [x] Šifrování disku
-- Jen lankový zámek skříně
-- Heslo spořiče
-- Inventární štítek
-> Zámek zdrží odnesení celé skříně, ne čtení disku na jiném stroji.
diff --git a/content/technicke-vybaveni/4-rocnik/03-nakup-sestavy.en.md b/content/technicke-vybaveni/4-rocnik/03-nakup-sestavy.en.md
deleted file mode 100644
index daa49e7..0000000
--- a/content/technicke-vybaveni/4-rocnik/03-nakup-sestavy.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Buying a computer
-popis: The work comes first, then the box.
-nazev_cviceni: Quiz: Buying a computer
-nadpis_kvizu: Quiz on Buying a computer
----
diff --git a/content/technicke-vybaveni/4-rocnik/03-nakup-sestavy.md b/content/technicke-vybaveni/4-rocnik/03-nakup-sestavy.md
deleted file mode 100644
index ecb82c1..0000000
--- a/content/technicke-vybaveni/4-rocnik/03-nakup-sestavy.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-4-nakup-sestavy
-puvodni: hw4.3
-nazev: Nákup sestavy
-popis: Nejdřív práce, potom krabice.
-nazev_cviceni: Kvíz: Nákup sestavy
-nadpis_kvizu: Kvíz k tématu Nákup sestavy
----
-
-# Výklad
-
-## Učebna není střih videa
-*Účel*
-
-- Kancelář chce spolehlivost, ticho a dost paměti na prohlížeč.
-- Střih a CAD chtějí procesor, paměť a často grafiku.
-- Server chce ECC nebo aspoň diskové pole a správu, ne podsvícení.
-- Počet portů a dokování je součást ceny, ne doplněk potom.
-
-> Tip: stejná sestava pro psaní a pro CAD je buď drahá, nebo pomalá.
-
-## Co srovnávat
-*Čísla*
-
-- Paměť berte s rezervou na čtyři roky, ne na dnešní minimum.
-- SSD jako systémový disk je základ. Velký sklad může být pomalejší.
-- Zdroj má rezervu nad součtem štítků, ne jen přesný součet.
-- Záruka na místě je u učebny levnější než odvoz dvaceti strojů.
-
-> Tip: více jader nepomůže programu, který umí jen jedno.
-
-## Jedna image
-*Stejnost*
-
-- Stejný model v učebně zjednoduší image i náhradní díl.
-- Dokoupit za dva roky stejnou desku často nejde. Pište, co je uvnitř.
-- Nejlevnější kus bez portů, které učebna používá, levný není.
-- Příkon a hluk patří do zadání, ne až do reklamace.
-
-> Tip: dvacet různých desek znamená dvacet ovladačů.
-
-
-# Kvíz
-
-## Podle čeho se volí sestava?
-- [x] Podle práce, kterou na ní škola dělá
-- Podle barvy krabice
-- Podle nejvyššího čísla v reklamě
-- Podle počtu RGB ventilátorů
-> CAD a psaní dopisu nechtějí stejné železo.
-
-## Proč v učebně stejný model?
-- [x] Jedna image a jeden náhradní díl
-- Aby se lišily ovladače
-- Aby každý stroj bootoval jinak
-- Zákon to vyžaduje kvůli VLAN
-> Různé desky znamenají různou údržbu.
-
-## Co u zdroje nestačí?
-- [x] Součet štítků bez rezervy
-- Rezerva nad typický odběr
-- Správný formát do skříně
-- Pojistka, která sedí k rozvodu
-> Špička a stárnutí se do přesného součtu nevejdou.
-
-## Co zdraží zdánlivě levný notebook?
-- [x] Chybějící porty a dok, který musíte dokoupit
-- To, že má SSD
-- To, že má záruku
-- To, že je tichý
-> Učebna bez obrazových výstupů potřebuje doky.
diff --git a/content/technicke-vybaveni/4-rocnik/04-mobilni-zarizeni.en.md b/content/technicke-vybaveni/4-rocnik/04-mobilni-zarizeni.en.md
deleted file mode 100644
index 7496177..0000000
--- a/content/technicke-vybaveni/4-rocnik/04-mobilni-zarizeni.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Mobile devices
-popis: Tablets and phones on the school network.
-nazev_cviceni: Quiz: Mobile devices
-nadpis_kvizu: Quiz on Mobile devices
----
diff --git a/content/technicke-vybaveni/4-rocnik/04-mobilni-zarizeni.md b/content/technicke-vybaveni/4-rocnik/04-mobilni-zarizeni.md
deleted file mode 100644
index 6fef017..0000000
--- a/content/technicke-vybaveni/4-rocnik/04-mobilni-zarizeni.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-4-mobilni-zarizeni
-puvodni: hw4.4
-nazev: Mobilní zařízení
-popis: Tablet a telefon ve školní síti.
-nazev_cviceni: Kvíz: Mobilní zařízení
-nadpis_kvizu: Kvíz k tématu Mobilní zařízení
----
-
-# Výklad
-
-## Cizí kapsy
-*Správa*
-
-- Školní tablet se dá zapsat do správy a na dálku smazat.
-- Soukromý telefon do správy školy nepatří.
-- Účet školy na soukromém zařízení má jít odebrat.
-- Ztracený školní tablet bez správy je cizí počítač s vašimi daty.
-
-> Tip: telefon žáka není školní majetek a nepatří mu školní certifikát natrvalo.
-
-## Kam ho pustit
-*Síť*
-
-- Hostovská nebo mobilní síť je oddělená.
-- Školní tablet může dostat jinou síť než návštěva.
-- Sdílené tablety se mezi žáky mažou nebo přepínají profil.
-- Nabíjení ve třídě je otázka pojistek, ne jen kabelů na stole.
-
-> Tip: telefon učitelů ve stejné VLAN jako servery je zbytečné riziko.
-
-## Baterie a pád
-*Život*
-
-- Obal a popruh jsou součást nákupu, ne charita.
-- Aktualizace systému se dělají hromadně, ne podle nálady.
-- Rozbitý displej se nevrací do oběhu s trčícím sklem.
-- Evidence IMEI nebo sériového čísla patří k inventuře.
-
-> Tip: cyklování baterie v nabíjecím kufru ji po dvou letech unaví.
-
-
-# Kvíz
-
-## Čím se liší školní tablet od soukromého telefonu?
-- [x] Školní jde zapsat do správy a na dálku vymazat
-- Soukromý musí mít školní certifikát navždy
-- Jsou to stejné režimy
-- Tablet nepatří do inventury
-> Majetek školy se dá spravovat, kapsa žáka ne.
-
-## Kam nepatří učitelský telefon?
-- [x] Do VLAN se servery
-- Do oddělené mobilní sítě
-- Na účet, který jde odebrat
-- Mimo žákovská data, pokud to jde
-> Ztracený telefon nemá být klíč k serverům.
-
-## Co sdílený tablet mezi žáky potřebuje?
-- [x] Mazání profilu nebo oddělené účty
-- Jedno heslo napsané na obalu
-- Trvalou složku předchozího žáka
-- Vypnuté aktualizace
-> Cizí účet na dalším žákovi je únik.
-
-## Co patří do evidence tabletu?
-- [x] Sériové číslo a umístění
-- Jen barva obalu
-- Heslo žáka
-- Soukromé fotky
-> Bez čísla ho nepoznáte v kupě stejných.
diff --git a/content/technicke-vybaveni/4-rocnik/05-periferie-v-provozu.en.md b/content/technicke-vybaveni/4-rocnik/05-periferie-v-provozu.en.md
deleted file mode 100644
index 5113620..0000000
--- a/content/technicke-vybaveni/4-rocnik/05-periferie-v-provozu.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Peripherals in daily use
-popis: Printers, projectors, and what actually breaks.
-nazev_cviceni: Quiz: Peripherals in daily use
-nadpis_kvizu: Quiz on Peripherals in daily use
----
diff --git a/content/technicke-vybaveni/4-rocnik/05-periferie-v-provozu.md b/content/technicke-vybaveni/4-rocnik/05-periferie-v-provozu.md
deleted file mode 100644
index 2b82c27..0000000
--- a/content/technicke-vybaveni/4-rocnik/05-periferie-v-provozu.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-4-periferie-v-provozu
-puvodni: hw4.5
-nazev: Periferie v provozu
-popis: Tiskárny, projektory a co se na nich láme.
-nazev_cviceni: Kvíz: Periferie v provozu
-nadpis_kvizu: Kvíz k tématu Periferie v provozu
----
-
-# Výklad
-
-## Lampa a síť
-*Projektor*
-
-- Zdroj obrazu a kabel jsou častější chyba než deska.
-- Filtr projektoru se ucpe prachem a přístroj se vypne.
-- Stropní držák se dotahuje, nevěší se na něj.
-- Náhradní kabel ve třídě je levnější než přestávka.
-
-> Tip: lampa má hodiny. Nečekejte, až praskne uprostřed hodiny.
-
-## Fronta a toner
-*Tisk*
-
-- Sklad toneru má být suchý, ne na topení.
-- Účet tisku omezí náhodné desítky stran.
-- Síťová tiskárna se po výměně adresy musí najít znovu.
-- Servisní interval bubnu není totéž co výměna toneru.
-
-> Tip: prázdný toner a zaseknutý papír jsou dvě různé hlášky.
-
-## Myš, sluchátka, kamera
-*Drobnosti*
-
-- Sluchátka ve třídě mají být omyvatelná.
-- Kamera na dálkovou výuku chce světlo před obličejem, ne za zády.
-- USB přijímač myši ztracený v portu projektoru se špatně hledá.
-- Spotřební věci se objednávají dřív, než dojde poslední kus.
-
-> Tip: bezdrátová myš bez náhradní baterie je v hodině mrtvá.
-
-
-# Kvíz
-
-## Co u projektoru kontrolovat dřív než desku?
-- [x] Zdroj obrazu, kabel a filtr
-- Jen typ žárovky ve stropě učebny
-- Heslo Wi-Fi
-- Verzi BIOSu učitelského telefonu
-> Obraz často končí na kabelu.
-
-## Čím se liší toner a buben?
-- [x] Toner je prach s barvou, buben je díl, který obraz přenáší
-- Jsou to totéž
-- Buben se mění každý den
-- Toner je jen u inkoustové tiskárny
-> Prázdný toner a unavený buben vypadají na stránce jinak.
-
-## Proč účty tisku?
-- [x] Omezí náhodné velké tisky
-- Zvýší rozlišení
-- Nahradí papír
-- Vypnou skener
-> Fronta bez limitu tiskne omylem celé knihy.
-
-## Co ve třídě dochází bez náhrady jako první?
-- [x] Baterie, kabely a spotřební díly
-- Procesory
-- Rackové lišty
-- TPM čipy
-> Drobnost zastaví hodinu stejně jako velká porucha.
diff --git a/content/technicke-vybaveni/4-rocnik/06-ekologie-a-data.en.md b/content/technicke-vybaveni/4-rocnik/06-ekologie-a-data.en.md
deleted file mode 100644
index 6e57cc0..0000000
--- a/content/technicke-vybaveni/4-rocnik/06-ekologie-a-data.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Disposal and data
-popis: A discarded drive still contains the school.
-nazev_cviceni: Quiz: Disposal and data
-nadpis_kvizu: Quiz on Disposal and data
----
diff --git a/content/technicke-vybaveni/4-rocnik/06-ekologie-a-data.md b/content/technicke-vybaveni/4-rocnik/06-ekologie-a-data.md
deleted file mode 100644
index 3cf69fb..0000000
--- a/content/technicke-vybaveni/4-rocnik/06-ekologie-a-data.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-4-ekologie-a-data
-puvodni: hw4.6
-nazev: Ekologie a data
-popis: Vyhozený disk pořád obsahuje školu.
-nazev_cviceni: Kvíz: Ekologie a data
-nadpis_kvizu: Kvíz k tématu Ekologie a data
----
-
-# Výklad
-
-## Smazat neznamená vyhodit
-*Disk*
-
-- Před vyřazením se disk smaže tak, aby nešel běžně obnovit.
-- Nefunkční disk se fyzicky zničí, ne jen odloží.
-- SSD se maže příkazem, který buňky opravdu uvolní.
-- Potvrzení o likvidaci patří k inventuře.
-
-> Tip: rychlé formátování nechá data čitelná.
-
-## Není to směsný odpad
-*Železo*
-
-- Elektroodpad se odevzdává na určené místo.
-- Baterie zvlášť, a nabobtnané ještě opatrněji.
-- Toner je taky odpad s režimem, ne koš ve sborovně.
-- Štítek s jménem žáka se před odvozem sundá.
-
-> Tip: monitor a deska nepatří do popelnice za školou.
-
-## Starý stroj ven ze školy
-*Darování*
-
-- Nejdřív data, potom majetek.
-- Licence vázaná na školu se nesmí odstěhovat s počítačem.
-- Zápis, komu stroj připadl, chrání školu.
-- Nefunkční dar je jen přesun odpadu.
-
-> Tip: darovaný počítač bez smazání účtů je dar i s přístupy.
-
-
-# Kvíz
-
-## Proč nestačí rychlé formátování?
-- [x] Data jdou často obnovit
-- Formát disk fyzicky rozbije
-- SSD formát zakazuje
-- Smaže i TPM v jiném počítači
-> Zmizí odkaz, ne obsah buněk.
-
-## Kam nepatří stará deska?
-- [x] Do směsného koše
-- Do elektroodpadu
-- K evidované likvidaci
-- Po smazání dat, pokud se vyřazuje
-> Elektroodpad má vlastní režim.
-
-## Co před darem počítače uděláte?
-- [x] Smažete data a ověříte licence
-- Necháte účty, ať se nový majitel přihlásí
-- Jen umyjete skříň
-- Přilepíte heslo na víko
-> Dar není předání školní sítě.
-
-## Čím se maže SSD líp než jedním přepisem naslepo?
-- [x] Příkazem, který buňky opravdu uvolní
-- Smazáním koše
-- Změnou tapety
-- Vypnutím Secure Boot
-> Řadič SSD přepis naslepo nemusí uložit tam, kde čekáte.
diff --git a/content/technicke-vybaveni/4-rocnik/07-metodika-zavady.en.md b/content/technicke-vybaveni/4-rocnik/07-metodika-zavady.en.md
deleted file mode 100644
index b2bd544..0000000
--- a/content/technicke-vybaveni/4-rocnik/07-metodika-zavady.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: A fault method
-popis: One sentence that holds the whole procedure.
-nazev_cviceni: Quiz: A fault method
-nadpis_kvizu: Quiz on A fault method
----
diff --git a/content/technicke-vybaveni/4-rocnik/07-metodika-zavady.md b/content/technicke-vybaveni/4-rocnik/07-metodika-zavady.md
deleted file mode 100644
index 2b6a7ad..0000000
--- a/content/technicke-vybaveni/4-rocnik/07-metodika-zavady.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-4-metodika-zavady
-puvodni: hw4.7
-nazev: Metodika závady
-popis: Jedna věta, která drží celý postup.
-nazev_cviceni: Kvíz: Metodika závady
-nadpis_kvizu: Kvíz k tématu Metodika závady
----
-
-# Výklad
-
-## Co přesně nejde
-*Příznak*
-
-- Zeptejte se, co uživatel čekal a co vidí.
-- Od kdy a jestli to jde jinde.
-- Jedna učebna, nebo jen jeden stůl.
-- Bez příznaku se mění díly naslepo.
-
-> Tip: ‚nejde počítač‘ může být černý obraz, špatné heslo i spadlá síť.
-
-## Nejužší místo
-*Izolace*
-
-- Vyměňte kabel, port, účet nebo zařízení, vždy jen jedno.
-- Když náhradní kus funguje, původní je vinný nebo špatně nastavený.
-- Když nefunguje ani náhradní, chyba je ve společné věci.
-- Pište si mezivýsledky. Paměť po obědě lže.
-
-> Tip: fungující kabel a fungující port rozdělí chybu na dvě hromady.
-
-## Oprava a prevence
-*Závěr*
-
-- Opravte příčinu, ne jen příznak na jednom stole.
-- Řekněte uživateli, co se změnilo.
-- Díly do opravy nepatří zpátky do skříně potichu.
-- Opakovaná závada chce úpravu nákupu nebo zapojení.
-
-> Tip: vyměněný kabel bez zápisu se příští měsíc tváří jako záhada.
-
-
-# Kvíz
-
-## Co je první otázka?
-- [x] Co přesně nejde a od kdy
-- Jakou má deska barvu
-- Jestli je ve škole VLAN 20
-- Kolik stojí nový notebook
-> Příznak zužuje vrstvu, kterou budete zkoušet.
-
-## Proč měnit jen jeden díl?
-- [x] Ať víte, který rozdíl pomohl
-- Sklad má jen jeden kabel
-- BIOS víc výměn zakazuje
-- Uživatel jinak zapomene heslo
-> Dvě výměny naráz nic nedokážou.
-
-## Náhradní kabel taky nejde. Co z toho plyne?
-- [x] Chyba je spíš ve portu, účtu nebo zařízení
-- Oba kabely jsou vždy špatné
-- Síť nemá smysl
-- Je rozbitý TPM
-> Společné selhání ukazuje na společnou část.
-
-## Co patří do zápisu opravy?
-- [x] Příznak, příčina a co se vyměnilo
-- Jen ‚hotovo‘
-- Heslo uživatele
-- Soukromý názor na učitele
-> Příště podle zápisu nezačínáte od nuly.
diff --git a/content/technicke-vybaveni/4-rocnik/08-opakovani-k-maturite.en.md b/content/technicke-vybaveni/4-rocnik/08-opakovani-k-maturite.en.md
deleted file mode 100644
index 9ffb293..0000000
--- a/content/technicke-vybaveni/4-rocnik/08-opakovani-k-maturite.en.md
+++ /dev/null
@@ -1,6 +0,0 @@
----
-nazev: Exam review
-popis: Hardware you can name and justify.
-nazev_cviceni: Quiz: Exam review
-nadpis_kvizu: Quiz on Exam review
----
diff --git a/content/technicke-vybaveni/4-rocnik/08-opakovani-k-maturite.md b/content/technicke-vybaveni/4-rocnik/08-opakovani-k-maturite.md
deleted file mode 100644
index 7073ae1..0000000
--- a/content/technicke-vybaveni/4-rocnik/08-opakovani-k-maturite.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-id: hardware-4-opakovani-k-maturite
-puvodni: hw4.8
-nazev: Opakování k maturitě
-popis: Železo, které umíte pojmenovat a zdůvodnit.
-nazev_cviceni: Kvíz: Opakování k maturitě
-nadpis_kvizu: Kvíz k tématu Opakování k maturitě
----
-
-# Výklad
-
-## Od kliknutí k disku
-*Tok dat*
-
-- Vstup, paměť, procesor, sběrnice, disk a výstup.
-- RAM je pracovní, disk je trvalý. Není to totéž.
-- Grafika může mít vlastní paměť a pořád jede po sběrnici.
-- Úzké hrdlo je nejpomalejší článek této cesty.
-
-> Tip: maturita chce cestu, ne seznam zkratek.
-
-## Co drží učebnu
-*Spolehlivost*
-
-- RAID drží provoz při pádu disku.
-- Záloha vrátí data z jiného místa a času.
-- UPS dá čas na vypnutí.
-- Firmware a čištění jsou prevence, ne oprava po požáru.
-
-> Tip: záloha, RAID a UPS řeší tři různé nehody.
-
-## Proč právě tahle krabice
-*Rozhodnutí*
-
-- Kancelář, střih a server mají jiné priority.
-- Stejný model ve třídě šetří image.
-- Vyřazení začíná smazáním dat.
-- Závadu vedete od příznaku po jedné změně.
-
-> Tip: umíte obhájit paměť, disk a porty pro konkrétní učebnu.
-
-
-# Kvíz
-
-## Čím se liší RAM a disk?
-- [x] RAM je pracovní paměť, disk drží data i bez napájení
-- Jsou to totéž
-- Disk je vždy rychlejší
-- RAM se zálohuje na pásku
-> Vypnutí smaže obsah RAM, ne uložený soubor na disku.
-
-## Co vrátí soubor smazaný před týdnem?
-- [x] Záloha z dřívějšího dne, ne RAID
-- RAID 1 sám
-- UPS
-- Secure Boot
-> RAID má pořád aktuální, tedy i smazaný stav.
-
-## Co dá UPS?
-- [x] Čas na pořádné vypnutí
-- Kopii dat v jiné budově
-- Rychlejší procesor
-- Nový disk
-> Baterie není archiv.
-
-## Čím začíná vyřazení počítače?
-- [x] Smazáním dat
-- Sundáním obalu
-- Darem bez kontroly
-- Zápisem do VLAN
-> Nejdřív obsah, potom plech.
diff --git a/data/share/applications/org.maturita.Maturita.desktop b/data/share/applications/org.maturita.Maturita.desktop
deleted file mode 100644
index 0530844..0000000
--- a/data/share/applications/org.maturita.Maturita.desktop
+++ /dev/null
@@ -1,14 +0,0 @@
-[Desktop Entry]
-Type=Application
-Version=1.0
-Name=Graduately
-GenericName=Graduately study app
-GenericName[cs]=Příprava na maturitu
-Comment=Practice app for the Czech maturita exam
-Comment[cs]=Procvičování k maturitě
-Exec=graduately
-Icon=maturita
-Terminal=false
-Categories=Education;
-StartupNotify=true
-StartupWMClass=org-maturita-maturita-desktop-MainKt
diff --git a/data/share/icons/hicolor/128x128/apps/maturita.png b/data/share/icons/hicolor/128x128/apps/maturita.png
deleted file mode 100644
index 48d2e36..0000000
Binary files a/data/share/icons/hicolor/128x128/apps/maturita.png and /dev/null differ
diff --git a/data/share/icons/hicolor/16x16/apps/maturita.png b/data/share/icons/hicolor/16x16/apps/maturita.png
deleted file mode 100644
index 7d992c8..0000000
Binary files a/data/share/icons/hicolor/16x16/apps/maturita.png and /dev/null differ
diff --git a/data/share/icons/hicolor/256x256/apps/maturita.png b/data/share/icons/hicolor/256x256/apps/maturita.png
deleted file mode 100644
index 8fb4fcc..0000000
Binary files a/data/share/icons/hicolor/256x256/apps/maturita.png and /dev/null differ
diff --git a/data/share/icons/hicolor/32x32/apps/maturita.png b/data/share/icons/hicolor/32x32/apps/maturita.png
deleted file mode 100644
index 9f3b1d5..0000000
Binary files a/data/share/icons/hicolor/32x32/apps/maturita.png and /dev/null differ
diff --git a/data/share/icons/hicolor/48x48/apps/maturita.png b/data/share/icons/hicolor/48x48/apps/maturita.png
deleted file mode 100644
index 747ab3f..0000000
Binary files a/data/share/icons/hicolor/48x48/apps/maturita.png and /dev/null differ
diff --git a/data/share/icons/hicolor/512x512/apps/maturita.png b/data/share/icons/hicolor/512x512/apps/maturita.png
deleted file mode 100644
index 4cad9e6..0000000
Binary files a/data/share/icons/hicolor/512x512/apps/maturita.png and /dev/null differ
diff --git a/data/share/icons/hicolor/64x64/apps/maturita.png b/data/share/icons/hicolor/64x64/apps/maturita.png
deleted file mode 100644
index 4f7fd30..0000000
Binary files a/data/share/icons/hicolor/64x64/apps/maturita.png and /dev/null differ
diff --git a/graduately-android.apk b/graduately-android.apk
new file mode 100644
index 0000000..b571264
Binary files /dev/null and b/graduately-android.apk differ
diff --git a/installers/graduately-installer-android.zip b/graduately-installer-android.zip
similarity index 75%
rename from installers/graduately-installer-android.zip
rename to graduately-installer-android.zip
index 0e90dfa..f9ddd7f 100644
Binary files a/installers/graduately-installer-android.zip and b/graduately-installer-android.zip differ
diff --git a/installers/graduately-installer-ios.zip b/graduately-installer-ios.zip
similarity index 78%
rename from installers/graduately-installer-ios.zip
rename to graduately-installer-ios.zip
index 01d8c69..cdafa4d 100644
Binary files a/installers/graduately-installer-ios.zip and b/graduately-installer-ios.zip differ
diff --git a/installers/graduately-installer-linux.sh b/graduately-installer-linux.sh
similarity index 100%
rename from installers/graduately-installer-linux.sh
rename to graduately-installer-linux.sh
diff --git a/installers/graduately-installer-linux.zip b/graduately-installer-linux.zip
similarity index 83%
rename from installers/graduately-installer-linux.zip
rename to graduately-installer-linux.zip
index 1c2e54a..e8682c2 100644
Binary files a/installers/graduately-installer-linux.zip and b/graduately-installer-linux.zip differ
diff --git a/installers/graduately-installer-macos.zip b/graduately-installer-macos.zip
similarity index 67%
rename from installers/graduately-installer-macos.zip
rename to graduately-installer-macos.zip
index ca8e5fd..b298a3f 100644
Binary files a/installers/graduately-installer-macos.zip and b/graduately-installer-macos.zip differ
diff --git a/installers/graduately-installer-windows.cmd b/graduately-installer-windows.cmd
similarity index 96%
rename from installers/graduately-installer-windows.cmd
rename to graduately-installer-windows.cmd
index 334c32a..057574b 100644
--- a/installers/graduately-installer-windows.cmd
+++ b/graduately-installer-windows.cmd
@@ -1,84 +1,84 @@
-@echo off
-rem Graduately installer for Windows.
-rem
-rem Carries no application of its own. Fastly caches branch-named URLs on
-rem raw.githubusercontent.com, so this script looks up the tip of `builds`
-rem and downloads that commit — not a stale zip from the last five minutes.
-rem
-rem Double-click the file to run it. The app updates itself from then on.
-
-setlocal EnableDelayedExpansion
-set "REPO=pepaskopec-blip/Graduately"
-set "BRANCH=builds"
-set "ASSET=graduately-windows-x64.zip"
-set "DEST=%LOCALAPPDATA%\Programs\Graduately"
-
-echo Installing Graduately
-echo ---------------------
-
-where curl >nul 2>&1 || goto :no_tools
-where tar >nul 2>&1 || goto :no_tools
-
-set "TMP_DIR=%TEMP%\graduately-installer"
-rmdir /s /q "%TMP_DIR%" 2>nul
-mkdir "%TMP_DIR%" || goto :fail_tmp
-
-echo Looking up the latest build...
-set "SHA="
-for /f "usebackq delims=" %%S in (`powershell -NoProfile -ExecutionPolicy Bypass -Command "$r = & curl.exe -fsSL --max-time 20 'https://github.com/%REPO%/commits/%BRANCH%.atom'; if ($r -match 'Commit/([0-9a-f]{40})') { $Matches[1] }"`) do set "SHA=%%S"
-if not defined SHA goto :fail_download
-set "URL=https://raw.githubusercontent.com/%REPO%/%SHA%/%ASSET%"
-set "LEGACY_URL=https://raw.githubusercontent.com/%REPO%/%SHA%/maturita-windows-x64.zip"
-
-echo Downloading build %SHA:~0,7%...
-curl -fL --progress-bar --max-time 1800 -o "%TMP_DIR%\%ASSET%" "%URL%"
-if errorlevel 1 curl -fL --progress-bar --max-time 1800 -o "%TMP_DIR%\%ASSET%" "%LEGACY_URL%" || goto :fail_download
-
-echo Installing to %DEST%
-if not exist "%DEST%" mkdir "%DEST%" || goto :fail_dest
-
-tar -xf "%TMP_DIR%\%ASSET%" -C "%DEST%" || goto :fail_extract
-
-set "LNK=%APPDATA%\Microsoft\Windows\Start Menu\Programs\Graduately.lnk"
-powershell -NoProfile -ExecutionPolicy Bypass -Command ^
- "$s=(New-Object -ComObject WScript.Shell).CreateShortcut('%LNK%');" ^
- "$s.TargetPath='%DEST%\graduately.exe';$s.WorkingDirectory='%DEST%';$s.Save()" >nul 2>&1
-
-rmdir /s /q "%TMP_DIR%" 2>nul
-
-echo.
-echo Done. Starting the app...
-start "" "%DEST%\graduately.exe"
-exit /b 0
-
-:no_tools
-echo.
-echo Error: this installer needs curl and tar, which come with Windows 10
-echo version 1803 and later. Please update Windows, or download the zip
-echo manually from https://github.com/%REPO%/tree/%BRANCH%
-pause
-exit /b 1
-
-:fail_tmp
-echo.
-echo Error: could not create a temporary folder in %TEMP%.
-pause
-exit /b 1
-
-:fail_download
-echo.
-echo Error: the download failed. Check your internet connection.
-pause
-exit /b 1
-
-:fail_dest
-echo.
-echo Error: could not create %DEST%.
-pause
-exit /b 1
-
-:fail_extract
-echo.
-echo Error: the archive could not be unpacked.
-pause
-exit /b 1
+@echo off
+rem Graduately installer for Windows.
+rem
+rem Carries no application of its own. Fastly caches branch-named URLs on
+rem raw.githubusercontent.com, so this script looks up the tip of `builds`
+rem and downloads that commit — not a stale zip from the last five minutes.
+rem
+rem Double-click the file to run it. The app updates itself from then on.
+
+setlocal EnableDelayedExpansion
+set "REPO=pepaskopec-blip/Graduately"
+set "BRANCH=builds"
+set "ASSET=graduately-windows-x64.zip"
+set "DEST=%LOCALAPPDATA%\Programs\Graduately"
+
+echo Installing Graduately
+echo ---------------------
+
+where curl >nul 2>&1 || goto :no_tools
+where tar >nul 2>&1 || goto :no_tools
+
+set "TMP_DIR=%TEMP%\graduately-installer"
+rmdir /s /q "%TMP_DIR%" 2>nul
+mkdir "%TMP_DIR%" || goto :fail_tmp
+
+echo Looking up the latest build...
+set "SHA="
+for /f "usebackq delims=" %%S in (`powershell -NoProfile -ExecutionPolicy Bypass -Command "$r = & curl.exe -fsSL --max-time 20 'https://github.com/%REPO%/commits/%BRANCH%.atom'; if ($r -match 'Commit/([0-9a-f]{40})') { $Matches[1] }"`) do set "SHA=%%S"
+if not defined SHA goto :fail_download
+set "URL=https://raw.githubusercontent.com/%REPO%/%SHA%/%ASSET%"
+set "LEGACY_URL=https://raw.githubusercontent.com/%REPO%/%SHA%/maturita-windows-x64.zip"
+
+echo Downloading build %SHA:~0,7%...
+curl -fL --progress-bar --max-time 1800 -o "%TMP_DIR%\%ASSET%" "%URL%"
+if errorlevel 1 curl -fL --progress-bar --max-time 1800 -o "%TMP_DIR%\%ASSET%" "%LEGACY_URL%" || goto :fail_download
+
+echo Installing to %DEST%
+if not exist "%DEST%" mkdir "%DEST%" || goto :fail_dest
+
+tar -xf "%TMP_DIR%\%ASSET%" -C "%DEST%" || goto :fail_extract
+
+set "LNK=%APPDATA%\Microsoft\Windows\Start Menu\Programs\Graduately.lnk"
+powershell -NoProfile -ExecutionPolicy Bypass -Command ^
+ "$s=(New-Object -ComObject WScript.Shell).CreateShortcut('%LNK%');" ^
+ "$s.TargetPath='%DEST%\graduately.exe';$s.WorkingDirectory='%DEST%';$s.Save()" >nul 2>&1
+
+rmdir /s /q "%TMP_DIR%" 2>nul
+
+echo.
+echo Done. Starting the app...
+start "" "%DEST%\graduately.exe"
+exit /b 0
+
+:no_tools
+echo.
+echo Error: this installer needs curl and tar, which come with Windows 10
+echo version 1803 and later. Please update Windows, or download the zip
+echo manually from https://github.com/%REPO%/tree/%BRANCH%
+pause
+exit /b 1
+
+:fail_tmp
+echo.
+echo Error: could not create a temporary folder in %TEMP%.
+pause
+exit /b 1
+
+:fail_download
+echo.
+echo Error: the download failed. Check your internet connection.
+pause
+exit /b 1
+
+:fail_dest
+echo.
+echo Error: could not create %DEST%.
+pause
+exit /b 1
+
+:fail_extract
+echo.
+echo Error: the archive could not be unpacked.
+pause
+exit /b 1
diff --git a/graduately-installer-windows.zip b/graduately-installer-windows.zip
new file mode 100644
index 0000000..6ae9ad3
Binary files /dev/null and b/graduately-installer-windows.zip differ
diff --git a/graduately-ios.ipa b/graduately-ios.ipa
new file mode 100644
index 0000000..573f605
Binary files /dev/null and b/graduately-ios.ipa differ
diff --git a/graduately-linux-x86_64.AppImage b/graduately-linux-x86_64.AppImage
new file mode 100644
index 0000000..707d47e
Binary files /dev/null and b/graduately-linux-x86_64.AppImage differ
diff --git a/graduately-macos-arm64.zip b/graduately-macos-arm64.zip
new file mode 100644
index 0000000..8244109
Binary files /dev/null and b/graduately-macos-arm64.zip differ
diff --git a/graduately-windows-x64.zip b/graduately-windows-x64.zip
new file mode 100644
index 0000000..ef50bd1
Binary files /dev/null and b/graduately-windows-x64.zip differ
diff --git a/installers/graduately-android.html b/installers/graduately-android.html
deleted file mode 100644
index ccf32eb..0000000
--- a/installers/graduately-android.html
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
-
-
- Stáhnout Graduately
-
-
-
- Graduately pro Android
- Klepni na tlačítko. Až se APK stáhne, otevři ho a povol instalaci z neznámých zdrojů.
-
-
- Stáhnout APK
-
-
-
- Když odkaz hlásí 404, otevři
- větev builds
- a stáhni graduately-android.apk odtud.
-
-
-
-
diff --git a/installers/graduately-installer-macos.command b/installers/graduately-installer-macos.command
deleted file mode 100755
index d0e7d84..0000000
--- a/installers/graduately-installer-macos.command
+++ /dev/null
@@ -1,155 +0,0 @@
-#!/bin/sh
-# Graduately installer for macOS.
-#
-# Carries no application of its own. Fastly caches branch-named URLs on
-# raw.githubusercontent.com, so this script looks up the tip of `builds`
-# and downloads that commit — not a stale zip from the last five minutes.
-#
-# Double-click the bundled .app (or this file). No Terminal commands needed.
-
-set -eu
-
-REPO="pepaskopec-blip/Graduately"
-BRANCH="builds"
-ASSET="graduately-macos-arm64.zip"
-
-# An .app has no TTY; show dialogs so a regular user never needs Terminal.
-GUI=0
-if [ ! -t 1 ] && command -v osascript >/dev/null 2>&1; then
- GUI=1
-fi
-
-say() { printf '%s\n' "$*"; }
-
-gui_dialog() {
- osascript - "$1" <<'APPLESCRIPT'
-on run argv
- display dialog (item 1 of argv) with title "Graduately" buttons {"OK"} default button 1
-end run
-APPLESCRIPT
-}
-
-gui_alert() {
- osascript - "$1" <<'APPLESCRIPT'
-on run argv
- display dialog (item 1 of argv) with title "Graduately" buttons {"OK"} default button 1 with icon stop
-end run
-APPLESCRIPT
-}
-
-die() {
- if [ "$GUI" = 1 ]; then
- gui_alert "$*" >/dev/null || true
- else
- printf '\nChyba: %s\n' "$*" >&2
- printf 'Stiskněte Return. '
- read -r _ || true
- fi
- exit 1
-}
-
-builds_sha() {
- curl -fsSL --max-time 20 \
- "https://github.com/$REPO/commits/$BRANCH.atom" |
- sed -n 's/.*Commit\/\([0-9a-f]\{40\}\).*/\1/p' |
- head -n 1
-}
-
-if [ "$GUI" = 1 ]; then
- if ! osascript <<'APPLESCRIPT'
-display dialog "Nainstalovat Graduately do složky Aplikace? Stáhne se aktuální verze." with title "Graduately" buttons {"Zrušit", "Instalovat"} default button "Instalovat"
-APPLESCRIPT
- then
- exit 0
- fi
-else
- say "Installing Graduately"
- say "---------------------"
-fi
-
-arch=$(uname -m)
-if [ "$arch" != "arm64" ]; then
- die "Tento instalátor je pro Mac s čipem Apple. Tento počítač hlásí '$arch'."
-fi
-
-command -v curl >/dev/null 2>&1 || die "Na Macu chybí curl, bez něj nejde nic stáhnout."
-
-# ~/Applications is writable from the GUI updater later. /Applications
-# often is not, even when this installer can drop a .app there once.
-dest_dir="$HOME/Applications"
-mkdir -p "$dest_dir"
-# Remove legacy Maturita.app and any prior Graduately install in /Applications.
-for legacy in Maturita.app Graduately.app; do
- if [ -e "/Applications/$legacy" ] && [ -w "/Applications/$legacy" ]; then
- rm -rf "/Applications/$legacy"
- fi
-done
-
-tmp=$(mktemp -d)
-trap 'rm -rf "$tmp"' EXIT INT TERM
-
-if [ "$GUI" = 1 ]; then
- osascript <<'APPLESCRIPT' >/dev/null || true
-display notification "Stahuji aktuální verzi…" with title "Graduately"
-APPLESCRIPT
-fi
-
-sha=$(builds_sha)
-[ ${#sha} -eq 40 ] || die "Nepodařilo se najít aktuální verzi na GitHubu."
-URL="https://raw.githubusercontent.com/$REPO/$sha/$ASSET"
-LEGACY_ASSET="maturita-macos-arm64.zip"
-LEGACY_URL="https://raw.githubusercontent.com/$REPO/$sha/$LEGACY_ASSET"
-
-download_ok=0
-if [ "$GUI" = 1 ]; then
- if curl -fsSL --max-time 1800 -o "$tmp/$ASSET" "$URL"; then
- download_ok=1
- elif curl -fsSL --max-time 1800 -o "$tmp/$ASSET" "$LEGACY_URL"; then
- download_ok=1
- fi
- [ "$download_ok" = 1 ] ||
- die "Stažení se nezdařilo. Zkontrolujte internet a zkuste to znovu."
-else
- say "Downloading build $(printf '%.7s' "$sha")..."
- if curl -fL --progress-bar --max-time 1800 -o "$tmp/$ASSET" "$URL"; then
- download_ok=1
- elif curl -fL --progress-bar --max-time 1800 -o "$tmp/$ASSET" "$LEGACY_URL"; then
- say "(legacy package name)"
- download_ok=1
- fi
- [ "$download_ok" = 1 ] ||
- die "Stažení se nezdařilo. Zkontrolujte internet a zkuste to znovu."
- say "Unpacking..."
-fi
-
-mkdir -p "$tmp/new"
-ditto -x -k "$tmp/$ASSET" "$tmp/new" || die "Archiv se nepodařilo rozbalit."
-
-app=$(find "$tmp/new" -maxdepth 1 -name '*.app' -print -quit)
-[ -n "$app" ] || die "V archivu není žádná aplikace."
-name=$(basename "$app")
-
-rm -rf "$dest_dir/$name.old"
-if [ -e "$dest_dir/$name" ]; then
- mv "$dest_dir/$name" "$dest_dir/$name.old"
-fi
-# Drop the old product name if we are installing Graduately.app.
-if [ "$name" = "Graduately.app" ] && [ -e "$dest_dir/Maturita.app" ]; then
- rm -rf "$dest_dir/Maturita.app"
-fi
-if mv "$app" "$dest_dir/$name"; then
- rm -rf "$dest_dir/$name.old"
-else
- [ -e "$dest_dir/$name.old" ] && mv "$dest_dir/$name.old" "$dest_dir/$name"
- die "Do $dest_dir se nepodařilo zapsat."
-fi
-
-xattr -cr "$dest_dir/$name" 2>/dev/null || true
-
-if [ "$GUI" = 1 ]; then
- gui_dialog "Hotovo. Aplikace se teď otevře." >/dev/null || true
-else
- say ""
- say "Done. Starting the app..."
-fi
-open "$dest_dir/$name"
diff --git a/installers/graduately-installer-windows.zip b/installers/graduately-installer-windows.zip
deleted file mode 100644
index 1ac432e..0000000
Binary files a/installers/graduately-installer-windows.zip and /dev/null differ
diff --git a/installers/graduately-ios.html b/installers/graduately-ios.html
deleted file mode 100644
index 425163d..0000000
--- a/installers/graduately-ios.html
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-
-
-
- Graduately pro iOS
-
-
-
- Graduately pro iPhone (iOS 26)
-
- Nativní aplikace pro iOS 26+ s Liquid Glass. Není 1:1 kopie desktopu.
- Apple nepovolí instalaci jako Android APK. Nejjednodušší cesta je Mac
- s Xcode a vlastní iPhone (bezplatný Apple účet stačí na 7 dní).
-
-
- - Naklonuj Graduately
- - Otevři
Maturita.xcworkspace v kořeni repozitáře
- - V Signing vyber svůj Team
- - Připoj iPhone a stiskni Run
-
-
- IPA z větve builds jde nahrát i přes AltStore nebo Sideloadly
- (unsigned balíček, potřebuješ nástroj na sideload).
-
-
-
- Stáhnout IPA
-
-
-
- Když odkaz hlásí 404, otevři
- větev builds
- a stáhni graduately-ios.ipa odtud.
-
-
-
-
diff --git a/installers/macos/Info.plist b/installers/macos/Info.plist
deleted file mode 100644
index be9a582..0000000
--- a/installers/macos/Info.plist
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- cs
- CFBundleExecutable
- installer
- CFBundleIdentifier
- org.maturita.installer
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- Nainstalovat Graduately
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- 1.0
- LSMinimumSystemVersion
- 12.0
- LSUIElement
-
- NSHighResolutionCapable
-
-
-
diff --git a/installers/pack-installer-zips.sh b/installers/pack-installer-zips.sh
deleted file mode 100755
index 1310c99..0000000
--- a/installers/pack-installer-zips.sh
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/bin/sh
-# Rebuild the installer zips that README links to. Browsers download a
-# zip; they render .command / .sh / .cmd / .html as a text tab.
-
-set -eu
-
-cd "$(dirname "$0")"
-here=$PWD
-
-chmod +x graduately-installer-macos.command graduately-installer-linux.sh
-
-tmp=$(mktemp -d)
-trap 'rm -rf "$tmp"' EXIT INT TERM
-
-app="$tmp/Nainstalovat Graduately.app"
-mkdir -p "$app/Contents/MacOS"
-cp macos/Info.plist "$app/Contents/Info.plist"
-cp graduately-installer-macos.command "$app/Contents/MacOS/installer"
-chmod +x "$app/Contents/MacOS/installer"
-
-rm -f "$here/graduately-installer-macos.zip"
-(cd "$tmp" && zip -r -q "$here/graduately-installer-macos.zip" \
- "Nainstalovat Graduately.app")
-
-zip -q -FS graduately-installer-windows.zip graduately-installer-windows.cmd
-zip -q -FS graduately-installer-linux.zip graduately-installer-linux.sh
-
-# Browsers download a zip; they render .html as a text tab on GitHub.
-# The page looks up the tip of `builds` so Fastly cannot serve a cached 404
-# for the branch-named APK URL.
-cp graduately-android.html "$tmp/Stahnout Graduately.html"
-rm -f "$here/graduately-installer-android.zip"
-(cd "$tmp" && zip -q -FS "$here/graduately-installer-android.zip" \
- "Stahnout Graduately.html")
-
-cp graduately-ios.html "$tmp/Jak nainstalovat Graduately.html"
-rm -f "$here/graduately-installer-ios.zip"
-(cd "$tmp" && zip -q -FS "$here/graduately-installer-ios.zip" \
- "Jak nainstalovat Graduately.html")
diff --git a/ios/Graduately.xcodeproj/project.pbxproj b/ios/Graduately.xcodeproj/project.pbxproj
deleted file mode 100644
index f7dfc49..0000000
--- a/ios/Graduately.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,437 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 56;
- objects = {
-
-/* Begin PBXBuildFile section */
- A101000000000000000001 /* GraduatelyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A201000000000000000001 /* GraduatelyApp.swift */; };
- A101000000000000000002 /* AppModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A201000000000000000002 /* AppModel.swift */; };
- A101000000000000000003 /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = A201000000000000000003 /* Route.swift */; };
- A101000000000000000004 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = A201000000000000000004 /* JSON.swift */; };
- A101000000000000000005 /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = A201000000000000000005 /* Content.swift */; };
- A101000000000000000006 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = A201000000000000000006 /* Theme.swift */; };
- A101000000000000000007 /* Answers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A201000000000000000007 /* Answers.swift */; };
- A101000000000000000008 /* ProgressStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A201000000000000000008 /* ProgressStore.swift */; };
- A101000000000000000009 /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = A201000000000000000009 /* Updater.swift */; };
- A101000000000000000010 /* Components.swift in Sources */ = {isa = PBXBuildFile; fileRef = A201000000000000000010 /* Components.swift */; };
- A101000000000000000011 /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = A201000000000000000011 /* Icons.swift */; };
- A101000000000000000012 /* PathMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A201000000000000000012 /* PathMap.swift */; };
- A101000000000000000013 /* Screens.swift in Sources */ = {isa = PBXBuildFile; fileRef = A201000000000000000013 /* Screens.swift */; };
- A101000000000000000014 /* Exercises.swift in Sources */ = {isa = PBXBuildFile; fileRef = A201000000000000000014 /* Exercises.swift */; };
- A101000000000000000015 /* ExercisesMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A201000000000000000015 /* ExercisesMore.swift */; };
- A101000000000000000016 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A201000000000000000016 /* Assets.xcassets */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXFileReference section */
- A201000000000000000001 /* GraduatelyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraduatelyApp.swift; sourceTree = ""; };
- A201000000000000000002 /* AppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModel.swift; sourceTree = ""; };
- A201000000000000000003 /* Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Route.swift; sourceTree = ""; };
- A201000000000000000004 /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; };
- A201000000000000000005 /* Content.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Content.swift; sourceTree = ""; };
- A201000000000000000006 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; };
- A201000000000000000007 /* Answers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Answers.swift; sourceTree = ""; };
- A201000000000000000008 /* ProgressStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressStore.swift; sourceTree = ""; };
- A201000000000000000009 /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = ""; };
- A201000000000000000010 /* Components.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Components.swift; sourceTree = ""; };
- A201000000000000000011 /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icons.swift; sourceTree = ""; };
- A201000000000000000012 /* PathMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathMap.swift; sourceTree = ""; };
- A201000000000000000013 /* Screens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screens.swift; sourceTree = ""; };
- A201000000000000000014 /* Exercises.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Exercises.swift; sourceTree = ""; };
- A201000000000000000015 /* ExercisesMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExercisesMore.swift; sourceTree = ""; };
- A201000000000000000016 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- A201000000000000000018 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- A301000000000000000001 /* Graduately.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Graduately.app; sourceTree = BUILT_PRODUCTS_DIR; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
- A401000000000000000001 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
- A501000000000000000001 = {
- isa = PBXGroup;
- children = (
- A501000000000000000002 /* Graduately */,
- A501000000000000000006 /* Products */,
- );
- sourceTree = "";
- };
- A501000000000000000002 /* Graduately */ = {
- isa = PBXGroup;
- children = (
- A201000000000000000001 /* GraduatelyApp.swift */,
- A201000000000000000002 /* AppModel.swift */,
- A201000000000000000003 /* Route.swift */,
- A501000000000000000003 /* Data */,
- A501000000000000000004 /* Update */,
- A501000000000000000005 /* UI */,
- A201000000000000000016 /* Assets.xcassets */,
- A201000000000000000018 /* Info.plist */,
- );
- path = Graduately;
- sourceTree = "";
- };
- A501000000000000000003 /* Data */ = {
- isa = PBXGroup;
- children = (
- A201000000000000000004 /* JSON.swift */,
- A201000000000000000005 /* Content.swift */,
- A201000000000000000006 /* Theme.swift */,
- A201000000000000000007 /* Answers.swift */,
- A201000000000000000008 /* ProgressStore.swift */,
- );
- path = Data;
- sourceTree = "";
- };
- A501000000000000000004 /* Update */ = {
- isa = PBXGroup;
- children = (
- A201000000000000000009 /* Updater.swift */,
- );
- path = Update;
- sourceTree = "";
- };
- A501000000000000000005 /* UI */ = {
- isa = PBXGroup;
- children = (
- A201000000000000000010 /* Components.swift */,
- A201000000000000000011 /* Icons.swift */,
- A201000000000000000012 /* PathMap.swift */,
- A201000000000000000013 /* Screens.swift */,
- A201000000000000000014 /* Exercises.swift */,
- A201000000000000000015 /* ExercisesMore.swift */,
- );
- path = UI;
- sourceTree = "";
- };
- A501000000000000000006 /* Products */ = {
- isa = PBXGroup;
- children = (
- A301000000000000000001 /* Graduately.app */,
- );
- name = Products;
- sourceTree = "";
- };
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
- A601000000000000000001 /* Graduately */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = A801000000000000000002 /* Build configuration list for PBXNativeTarget "Graduately" */;
- buildPhases = (
- A701000000000000000001 /* Sources */,
- A401000000000000000001 /* Frameworks */,
- A701000000000000000002 /* Resources */,
- A701000000000000000004 /* Build content */,
- );
- buildRules = (
- );
- dependencies = (
- );
- name = Graduately;
- productName = Graduately;
- productReference = A301000000000000000001 /* Graduately.app */;
- productType = "com.apple.product-type.application";
- };
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
- A001000000000000000001 /* Project object */ = {
- isa = PBXProject;
- attributes = {
- BuildIndependentTargetsInParallel = 1;
- LastSwiftUpdateCheck = 1600;
- LastUpgradeCheck = 2700;
- TargetAttributes = {
- A601000000000000000001 = {
- CreatedOnToolsVersion = 16.0;
- };
- };
- };
- buildConfigurationList = A801000000000000000001 /* Build configuration list for PBXProject "Graduately" */;
- compatibilityVersion = "Xcode 14.0";
- developmentRegion = cs;
- hasScannedForEncodings = 0;
- knownRegions = (
- en,
- cs,
- Base,
- );
- mainGroup = A501000000000000000001;
- productRefGroup = A501000000000000000006 /* Products */;
- projectDirPath = "";
- projectRoot = "";
- targets = (
- A601000000000000000001 /* Graduately */,
- );
- };
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
- A701000000000000000002 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- A101000000000000000016 /* Assets.xcassets in Resources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXShellScriptBuildPhase section */
- A701000000000000000004 /* Build content */ = {
- isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- );
- inputPaths = (
- );
- name = "Build content";
- outputFileListPaths = (
- );
- outputPaths = (
- "$(DERIVED_FILE_DIR)/content.json",
- "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/content.json",
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "# Build content.json from the Markdown in content/ and drop it into the app\n# bundle. Nothing generated is kept in the repository, so a fresh clone opens\n# and runs in Xcode without extra steps.\nset -eu\nextract=\"${SRCROOT}/../scripts/build-content.py\"\nif [ ! -f \"$extract\" ]; then\n echo \"error: ${extract} not found. Keep ios/ inside the Graduately checkout.\" >&2\n exit 1\nfi\nout=\"${DERIVED_FILE_DIR}/content.json\"\nmkdir -p \"${DERIVED_FILE_DIR}\"\npython3 \"$extract\" --out \"$out\"\nmkdir -p \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}\"\ncp \"$out\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/content.json\"\n";
- };
-/* End PBXShellScriptBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- A701000000000000000001 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- A101000000000000000001 /* GraduatelyApp.swift in Sources */,
- A101000000000000000002 /* AppModel.swift in Sources */,
- A101000000000000000003 /* Route.swift in Sources */,
- A101000000000000000004 /* JSON.swift in Sources */,
- A101000000000000000005 /* Content.swift in Sources */,
- A101000000000000000006 /* Theme.swift in Sources */,
- A101000000000000000007 /* Answers.swift in Sources */,
- A101000000000000000008 /* ProgressStore.swift in Sources */,
- A101000000000000000009 /* Updater.swift in Sources */,
- A101000000000000000010 /* Components.swift in Sources */,
- A101000000000000000011 /* Icons.swift in Sources */,
- A101000000000000000012 /* PathMap.swift in Sources */,
- A101000000000000000013 /* Screens.swift in Sources */,
- A101000000000000000014 /* Exercises.swift in Sources */,
- A101000000000000000015 /* ExercisesMore.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXSourcesBuildPhase section */
-
-/* Begin XCBuildConfiguration section */
- A901000000000000000001 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = dwarf;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = YES;
- GCC_DYNAMIC_NO_PIC = NO;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 26.0;
- MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
- ONLY_ACTIVE_ARCH = YES;
- SDKROOT = iphoneos;
- STRING_CATALOG_GENERATE_SYMBOLS = YES;
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- SWIFT_VERSION = 5.0;
- };
- name = Debug;
- };
- A901000000000000000002 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = YES;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 26.0;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = iphoneos;
- STRING_CATALOG_GENERATE_SYMBOLS = YES;
- SWIFT_COMPILATION_MODE = wholemodule;
- SWIFT_OPTIMIZATION_LEVEL = "-O";
- SWIFT_VERSION = 5.0;
- VALIDATE_PRODUCT = YES;
- };
- name = Release;
- };
- A901000000000000000003 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- "CODE_SIGN_IDENTITY[sdk=iphonesimulator*]" = "-";
- CODE_SIGN_STYLE = Automatic;
- COMMIT = dev;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = C2U9S337H8;
- ENABLE_PREVIEWS = YES;
- GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_FILE = Graduately/Info.plist;
- INFOPLIST_KEY_CFBundleDisplayName = Graduately;
- INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
- INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
- INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
- INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
- INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- IPHONEOS_DEPLOYMENT_TARGET = 26.0;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- MARKETING_VERSION = 0.6;
- PRODUCT_BUNDLE_IDENTIFIER = com.bukovinafilip.maturita;
- PRODUCT_NAME = Graduately;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
- SUPPORTS_MACCATALYST = NO;
- SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
- SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
- SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Debug;
- };
- A901000000000000000004 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- "CODE_SIGN_IDENTITY[sdk=iphonesimulator*]" = "-";
- CODE_SIGN_STYLE = Automatic;
- COMMIT = dev;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = C2U9S337H8;
- ENABLE_PREVIEWS = YES;
- GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_FILE = Graduately/Info.plist;
- INFOPLIST_KEY_CFBundleDisplayName = Graduately;
- INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
- INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
- INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
- INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
- INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- IPHONEOS_DEPLOYMENT_TARGET = 26.0;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- MARKETING_VERSION = 0.6;
- PRODUCT_BUNDLE_IDENTIFIER = com.bukovinafilip.maturita;
- PRODUCT_NAME = Graduately;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
- SUPPORTS_MACCATALYST = NO;
- SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
- SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
- SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Release;
- };
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
- A801000000000000000001 /* Build configuration list for PBXProject "Graduately" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- A901000000000000000001 /* Debug */,
- A901000000000000000002 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- A801000000000000000002 /* Build configuration list for PBXNativeTarget "Graduately" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- A901000000000000000003 /* Debug */,
- A901000000000000000004 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
-/* End XCConfigurationList section */
- };
- rootObject = A001000000000000000001 /* Project object */;
-}
diff --git a/ios/Graduately.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Graduately.xcodeproj/project.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 919434a..0000000
--- a/ios/Graduately.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/ios/Graduately.xcodeproj/xcshareddata/xcschemes/Graduately.xcscheme b/ios/Graduately.xcodeproj/xcshareddata/xcschemes/Graduately.xcscheme
deleted file mode 100644
index 9cd60f4..0000000
--- a/ios/Graduately.xcodeproj/xcshareddata/xcschemes/Graduately.xcscheme
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/ios/Graduately/AppModel.swift b/ios/Graduately/AppModel.swift
deleted file mode 100644
index 9d324ec..0000000
--- a/ios/Graduately/AppModel.swift
+++ /dev/null
@@ -1,380 +0,0 @@
-import Foundation
-import SwiftUI
-
-final class AppModel: ObservableObject {
- let content: Content
- let progress: ProgressStore
- let updater = Updater()
-
- @Published var themeId: ThemeId
- @Published var mode: ColorMode
- @Published var lang: UiLang
- @Published var tab: AppTab = .practice
- @Published var practicePath = NavigationPath()
- /// Bumps whenever a full path replace is scheduled, so a queued push cannot land afterwards.
- private var pathGeneration = 0
- @Published var searchQuery = ""
- @Published var tick = 0
- @Published var update = UpdateState()
- @Published var showChangelog = false
-
- init() {
- let content = Content.load()
- self.content = content
- progress = ProgressStore(content: content)
- themeId = progress.themeId
- mode = progress.mode
- lang = progress.lang
- showChangelog = !content.changelog.isEmpty && !sameBuild(AppConfig.commit, progress.seenCommit)
- #if DEBUG
- // `xcrun simctl launch com.bukovinafilip.maturita -route u1e3` opens a page for screenshots.
- if let page = UserDefaults.standard.string(forKey: "route"), let r = routeFromPage(page) {
- openFromSearch(r.page)
- }
- if let theme = UserDefaults.standard.string(forKey: "theme"), let n = Int(theme), let id = ThemeId(rawValue: n) {
- themeId = id
- }
- if let m = UserDefaults.standard.string(forKey: "mode") {
- mode = m == "dark" ? .dark : .light
- }
- #endif
- }
-
- var palette: Palette { themePalette(themeId, mode) }
-
- func tr(_ key: String?) -> String { content.tr(key, lang: lang) }
-
- func fmt(_ key: String, _ args: CVarArg...) -> String {
- content.fmt(key, lang: lang, args)
- }
-
- func go(_ r: Route) {
- switch r {
- case .stats:
- tab = .progress
- case .welcome, .subjects:
- showPracticeRoot()
- default:
- applyPracticeRoute(r, replace: practicePath.isEmpty)
- }
- }
-
- func showPracticeRoot() {
- tab = .practice
- schedulePracticePath(NavigationPath())
- }
-
- func goPage(_ page: String?) {
- guard let page, let r = routeFromPage(page) else { return }
- go(r)
- }
-
- func openFromSearch(_ page: String) {
- guard let r = routeFromPage(page) else { return }
- switch r {
- case .stats:
- tab = .progress
- case .welcome, .subjects:
- showPracticeRoot()
- default:
- // Switching tabs and assigning a deep NavigationStack path in the
- // same turn crashes SwiftUI on macOS (boundPathChange / unexpectedError).
- applyPracticeRoute(r, replace: true)
- }
- }
-
- /// Sets the practice tab path safely, especially when jumping from Search.
- ///
- /// The sidebar-style tab view keeps one navigation column for every tab.
- /// A typed `[Route]` path does not match the other tabs, so the column
- /// traps in `boundPathChange` while an exercise is being opened. The path
- /// is therefore type-erased, and a push is applied on the next turn instead
- /// of replacing the whole stack during the click.
- private func applyPracticeRoute(_ r: Route, replace: Bool) {
- let switching = tab != .practice
- tab = .practice
- if switching || replace {
- schedulePracticePath(NavigationPath(r.stack))
- } else {
- let generation = pathGeneration
- DispatchQueue.main.async { [weak self] in
- guard let self, self.pathGeneration == generation else { return }
- self.practicePath.append(r)
- }
- }
- }
-
- private func schedulePracticePath(_ path: NavigationPath) {
- pathGeneration += 1
- practicePath = NavigationPath()
- DispatchQueue.main.async { [weak self] in
- self?.practicePath = path
- }
- }
-
- func setTheme(_ id: ThemeId) {
- withAnimation(Motion.settle) { themeId = id }
- progress.themeId = id
- }
-
- func applyMode(_ m: ColorMode) {
- withAnimation(Motion.settle) { mode = m }
- progress.mode = m
- }
-
- func applyLang(_ l: UiLang) {
- lang = l
- progress.lang = l
- }
-
- func refresh() { tick += 1 }
-
- func markGerman(_ unit: Int, _ ex: Int) {
- progress.markGerman(unit, ex)
- refresh()
- }
-
- func markVocab(_ unit: Int) {
- progress.markVocab(unit)
- refresh()
- }
-
- func markNet(_ id: Int) {
- progress.markNet(id)
- refresh()
- }
-
- func markHw(_ id: Int) {
- progress.markHw(id)
- refresh()
- }
-
- func markOn(_ id: Int) {
- progress.markOn(id)
- refresh()
- }
-
- func markOn2(_ id: Int) {
- progress.markOn2(id)
- refresh()
- }
-
- func markOn3(_ id: Int) {
- progress.markOn3(id)
- refresh()
- }
-
- func markOn4(_ id: Int) {
- progress.markOn4(id)
- refresh()
- }
-
- func markEn(_ year: Int, _ id: Int) {
- progress.markEn(year, id)
- refresh()
- }
-
- func markDe(_ year: Int, _ id: Int) {
- progress.markDe(year, id)
- refresh()
- }
-
- func markCourse(_ course: String, _ id: Int) {
- progress.markCourse(course, id)
- refresh()
- }
-
- func markLit(_ id: Int) {
- progress.markLit(id)
- refresh()
- }
-
- func markLit2(_ id: Int) {
- progress.markLit2(id)
- refresh()
- }
-
- func markLit3(_ id: Int) {
- progress.markLit3(id)
- refresh()
- }
-
- func markLit4(_ id: Int) {
- progress.markLit4(id)
- refresh()
- }
-
- func markChem(_ id: Int) {
- progress.markChem(id)
- refresh()
- }
-
- func markBio(_ id: Int) {
- progress.markBio(id)
- refresh()
- }
-
- func markFyz(_ id: Int) {
- progress.markFyz(id)
- refresh()
- }
-
- func markFyz2(_ id: Int) {
- progress.markFyz2(id)
- refresh()
- }
-
- func markFyz3(_ id: Int) {
- progress.markFyz3(id)
- refresh()
- }
-
- func markFyz4(_ id: Int) {
- progress.markFyz4(id)
- refresh()
- }
-
- func markTg(_ id: Int) {
- progress.markTg(id)
- refresh()
- }
-
- func markTg2(_ id: Int) {
- progress.markTg2(id)
- refresh()
- }
-
- func markTg3(_ id: Int) {
- progress.markTg3(id)
- refresh()
- }
-
- func markPg(_ id: Int) {
- progress.markPg(id)
- refresh()
- }
-
- func markPg2(_ id: Int) {
- progress.markPg2(id)
- refresh()
- }
-
- func markPg3(_ id: Int) {
- progress.markPg3(id)
- refresh()
- }
-
- func markPg4(_ id: Int) {
- progress.markPg4(id)
- refresh()
- }
-
- func markSw(_ id: Int) {
- progress.markSw(id)
- refresh()
- }
-
- func markSw2(_ id: Int) {
- progress.markSw2(id)
- refresh()
- }
-
- func markSw3(_ id: Int) {
- progress.markSw3(id)
- refresh()
- }
-
- func markSw4(_ id: Int) {
- progress.markSw4(id)
- refresh()
- }
-
- func markTg4(_ id: Int) {
- progress.markTg4(id)
- refresh()
- }
-
- func markCs(_ id: Int) {
- progress.markCs(id)
- refresh()
- }
-
- func markCs2(_ id: Int) {
- progress.markCs2(id)
- refresh()
- }
-
- func markCs3(_ id: Int) {
- progress.markCs3(id)
- refresh()
- }
-
- func markCs4(_ id: Int) {
- progress.markCs4(id)
- refresh()
- }
-
- func markMat0(_ id: Int) {
- progress.markMat0(id)
- refresh()
- }
-
- func markMat(_ id: Int) {
- progress.markMat(id)
- refresh()
- }
-
- func markMat2(_ id: Int) {
- progress.markMat2(id)
- refresh()
- }
-
- func markMat3(_ id: Int) {
- progress.markMat3(id)
- refresh()
- }
-
- func markMat4(_ id: Int) {
- progress.markMat4(id)
- refresh()
- }
-
- func markMluv(_ n: Int) {
- progress.markMluv(n)
- refresh()
- }
-
- func markBookQuiz(_ id: String) {
- progress.markBookQuiz(id)
- refresh()
- }
-
- func markBookPlot(_ id: String) {
- progress.markBookPlot(id)
- refresh()
- }
-
- func checkUpdate(_ interactive: Bool) {
- updater.check(interactive: interactive) { [weak self] state in
- self?.update = state
- }
- }
-
- func installUpdate() {
- updater.install { [weak self] state in
- self?.update = state
- }
- }
-
- func dismissChangelog() {
- progress.seenCommit = AppConfig.commit
- withAnimation(Motion.settle) { showChangelog = false }
- }
-}
-
-func sameBuild(_ a: String?, _ b: String?) -> Bool {
- guard let a, let b, !a.isEmpty, !b.isEmpty else { return false }
- if a.caseInsensitiveCompare(b) == .orderedSame { return true }
- let n = min(a.count, b.count)
- return n >= 7 && a.prefix(n).caseInsensitiveCompare(b.prefix(n)) == .orderedSame
-}
diff --git a/ios/Graduately/Assets.xcassets/AccentColor.colorset/Contents.json b/ios/Graduately/Assets.xcassets/AccentColor.colorset/Contents.json
deleted file mode 100644
index eb87897..0000000
--- a/ios/Graduately/Assets.xcassets/AccentColor.colorset/Contents.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "colors" : [
- {
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/ios/Graduately/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/ios/Graduately/Assets.xcassets/AppIcon.appiconset/AppIcon.png
deleted file mode 100644
index 9fbf69f..0000000
Binary files a/ios/Graduately/Assets.xcassets/AppIcon.appiconset/AppIcon.png and /dev/null differ
diff --git a/ios/Graduately/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Graduately/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index cefcc87..0000000
--- a/ios/Graduately/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "images" : [
- {
- "filename" : "AppIcon.png",
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/ios/Graduately/Assets.xcassets/Contents.json b/ios/Graduately/Assets.xcassets/Contents.json
deleted file mode 100644
index 73c0059..0000000
--- a/ios/Graduately/Assets.xcassets/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/ios/Graduately/Data/Answers.swift b/ios/Graduately/Data/Answers.swift
deleted file mode 100644
index c9b5df9..0000000
--- a/ios/Graduately/Data/Answers.swift
+++ /dev/null
@@ -1,715 +0,0 @@
-import Foundation
-
-func normalizeAnswer(_ input: String?) -> String {
- var pre = ""
- for ch in (input ?? "").lowercased() {
- switch ch {
- case "ä": pre += "ae"
- case "ö": pre += "oe"
- case "ü": pre += "ue"
- case "ß": pre += "ss"
- default: pre.append(ch)
- }
- }
- let decomp = pre.decomposedStringWithCanonicalMapping
- var out = ""
- var prevSpace = false
- for scalar in decomp.unicodeScalars {
- let v = scalar.value
- if (0x0300...0x036F).contains(v) || (0x1AB0...0x1AFF).contains(v) ||
- (0x1DC0...0x1DFF).contains(v) || (0x20D0...0x20FF).contains(v) ||
- (0xFE20...0xFE2F).contains(v) {
- continue
- }
- if CharacterSet.whitespacesAndNewlines.contains(scalar) {
- if !out.isEmpty && !prevSpace { out.append(" ") }
- prevSpace = true
- continue
- }
- if !CharacterSet.alphanumerics.contains(scalar) { continue }
- prevSpace = false
- out.unicodeScalars.append(scalar)
- }
- return out.trimmingCharacters(in: .whitespaces)
-}
-
-func answerAccepts(_ selNorm: String, _ ans: String?) -> Bool {
- guard let ans, !ans.isEmpty else { return false }
- return ans.split(separator: "|").contains { normalizeAnswer(String($0)) == selNorm }
-}
-
-func answerIncomplete(_ selNorm: String, _ ans: String?) -> Bool {
- guard !selNorm.isEmpty, let ans, !ans.isEmpty else { return false }
- for part in ans.split(separator: "|") {
- let a = normalizeAnswer(String(part))
- if a.hasPrefix(selNorm + " ") && a.count > selNorm.count + 1 { return true }
- }
- return false
-}
-
-func netTxtEq(_ a: String?, _ b: String?) -> Bool {
- guard let a, let b else { return a == b }
- let x = a.filter { !$0.isWhitespace }.lowercased()
- let y = b.filter { !$0.isWhitespace }.lowercased()
- return x == y
-}
-
-func hasUmlaut(_ s: String?) -> Bool {
- guard let s else { return false }
- return s.contains(where: { "äöüÄÖÜß".contains($0) })
-}
-
-// MARK: - Mathematics
-// Same rules as src/math_check.c. Do not route numbers through normalizeAnswer.
-
-private struct MFrac { var n: Int64 = 0; var d: Int64 = 1 }
-private struct MTerm {
- var ok = true
- var a = MFrac()
- var b = MFrac()
- var rad: Int64 = 0
- var p = MFrac()
-}
-
-private func mGcd(_ a0: Int64, _ b0: Int64) -> Int64 {
- var a = a0 < 0 ? -a0 : a0
- var b = b0 < 0 ? -b0 : b0
- while b != 0 {
- let t = a % b
- a = b
- b = t
- }
- return a == 0 ? 1 : a
-}
-
-private func mMul(_ a: Int64, _ b: Int64) -> Int64? {
- let r = a.multipliedReportingOverflow(by: b)
- return r.overflow ? nil : r.partialValue
-}
-
-private func mSet(_ f: inout MFrac, _ n: Int64, _ d: Int64) -> Bool {
- if d == 0 { return false }
- var n = n, d = d
- if d < 0 { n = -n; d = -d }
- let g = mGcd(n, d)
- f.n = n / g
- f.d = d / g
- return true
-}
-
-private func mAddF(_ x: MFrac, _ y: MFrac) -> MFrac? {
- guard let xn = mMul(x.n, y.d), let yn = mMul(y.n, x.d),
- let d = mMul(x.d, y.d) else { return nil }
- let s = xn.addingReportingOverflow(yn)
- if s.overflow { return nil }
- var o = MFrac()
- return mSet(&o, s.partialValue, d) ? o : nil
-}
-
-private func mMulF(_ x: MFrac, _ y: MFrac) -> MFrac? {
- guard let n = mMul(x.n, y.n), let d = mMul(x.d, y.d) else { return nil }
- var o = MFrac()
- return mSet(&o, n, d) ? o : nil
-}
-
-private func mDivF(_ x: MFrac, _ y: MFrac) -> MFrac? {
- if y.n == 0 { return nil }
- return mMulF(x, MFrac(n: y.d, d: y.n))
-}
-
-private func mSameF(_ x: MFrac, _ y: MFrac) -> Bool { x.n == y.n && x.d == y.d }
-
-private func mSqrtReduce(_ n0: Int64) -> (out: Int64, rad: Int64)? {
- if n0 < 0 { return nil }
- if n0 > 1_000_000 { return nil }
- var n = n0
- var out: Int64 = 1
- var f: Int64 = 2
- while f * f <= n {
- var c = 0
- while n % f == 0 { n /= f; c += 1 }
- if c >= 2 { out *= mPow(f, c / 2) }
- if c % 2 == 1 { n *= f }
- if f == 2 { f = 3 } else { f += 2 }
- if f > 10000 { break }
- }
- return (out, n)
-}
-
-private func mPow(_ b: Int64, _ e: Int) -> Int64 {
- var r: Int64 = 1
- var i = 0
- while i < e { r *= b; i += 1 }
- return r
-}
-
-private func mNorm(_ t: inout MTerm) {
- if !t.ok { return }
- if t.rad > 1, let s = mSqrtReduce(t.rad) {
- guard let nb = mMulF(t.b, MFrac(n: s.out, d: 1)) else { t.ok = false; return }
- t.b = nb
- t.rad = s.rad
- }
- if t.rad <= 1 {
- guard let na = mAddF(t.a, t.b) else { t.ok = false; return }
- t.a = na
- t.b = MFrac()
- t.rad = 0
- }
- if t.b.n == 0 { t.rad = 0 }
-}
-
-private func mAdd(_ x: MTerm, _ y: MTerm) -> MTerm {
- if !x.ok || !y.ok { return MTerm(ok: false) }
- var o = MTerm()
- guard let a = mAddF(x.a, y.a), let p = mAddF(x.p, y.p) else { return MTerm(ok: false) }
- o.a = a
- o.p = p
- if x.rad == 0 && y.rad == 0 {
- o.b = MFrac(); o.rad = 0
- } else if x.rad == 0 {
- o.b = y.b; o.rad = y.rad
- } else if y.rad == 0 || x.rad == y.rad {
- guard let b = mAddF(x.b, y.b) else { return MTerm(ok: false) }
- o.b = b; o.rad = x.rad
- } else { return MTerm(ok: false) }
- mNorm(&o)
- return o
-}
-
-private func mScale(_ x: MTerm, _ f: MFrac) -> MTerm {
- if !x.ok { return MTerm(ok: false) }
- var o = MTerm()
- guard let a = mMulF(x.a, f), let b = mMulF(x.b, f), let p = mMulF(x.p, f) else {
- return MTerm(ok: false)
- }
- o.a = a; o.b = b; o.rad = x.rad; o.p = p
- mNorm(&o)
- return o
-}
-
-private func mMulT(_ x: MTerm, _ y: MTerm) -> MTerm {
- if !x.ok || !y.ok { return MTerm(ok: false) }
- if (x.p.n != 0 && (y.p.n != 0 || y.rad != 0)) ||
- (y.p.n != 0 && (x.p.n != 0 || x.rad != 0)) { return MTerm(ok: false) }
- if x.rad != 0 && y.rad != 0 && x.rad != y.rad { return MTerm(ok: false) }
- var o = MTerm()
- guard let aa = mMulF(x.a, y.a) else { return MTerm(ok: false) }
- o.a = aa
- if x.rad == 0 && y.rad == 0 {
- o.b = MFrac(); o.rad = 0
- } else if x.rad == 0 {
- guard let b = mMulF(x.a, y.b) else { return MTerm(ok: false) }
- o.b = b; o.rad = y.rad
- } else if y.rad == 0 {
- guard let b = mMulF(y.a, x.b) else { return MTerm(ok: false) }
- o.b = b; o.rad = x.rad
- } else {
- guard let ab = mMulF(x.a, y.b), let ba = mMulF(x.b, y.a),
- let cross = mAddF(ab, ba),
- let sq = mMul(x.rad, y.rad) else { return MTerm(ok: false) }
- guard let bb = mMulF(x.b, y.b), let extra = mMulF(bb, MFrac(n: sq, d: 1)),
- let na = mAddF(o.a, extra) else { return MTerm(ok: false) }
- o.a = na; o.b = cross; o.rad = x.rad
- }
- if x.p.n != 0 { o.p = x.p } else { o.p = y.p }
- if y.a.n != 1 || y.a.d != 1 || y.b.n != 0 || y.rad != 0 || y.p.n != 0 {
- if x.p.n != 0 {
- guard let p = mMulF(x.p, y.a) else { return MTerm(ok: false) }
- o.p = p
- }
- }
- if x.a.n != 1 || x.a.d != 1 || x.b.n != 0 || x.rad != 0 || x.p.n != 0 {
- if y.p.n != 0 && x.p.n == 0 {
- guard let p = mMulF(y.p, x.a) else { return MTerm(ok: false) }
- o.p = p
- }
- }
- mNorm(&o)
- return o
-}
-
-private func mDivT(_ x: MTerm, _ y: MTerm) -> MTerm {
- if !x.ok || !y.ok || y.p.n != 0 { return MTerm(ok: false) }
- if y.rad == 0 {
- guard let f = mDivF(MFrac(n: 1, d: 1), y.a) else { return MTerm(ok: false) }
- return mScale(x, f)
- }
- if x.p.n != 0 { return MTerm(ok: false) }
- guard let yb = mMulF(y.b, MFrac(n: y.rad, d: 1)),
- let bb = mMulF(y.b, yb),
- let aa = mMulF(y.a, y.a),
- let den = mAddF(aa, MFrac(n: -bb.n, d: bb.d)) else { return MTerm(ok: false) }
- var num = MTerm()
- num.a = y.a
- num.b = MFrac(n: -y.b.n, d: y.b.d)
- num.rad = y.rad
- guard let inv = mDivF(MFrac(n: 1, d: 1), den) else { return MTerm(ok: false) }
- return mMulT(x, mScale(num, inv))
-}
-
-private enum MTok {
- case num(Int64, Int64)
- case sqrt, pi, add, sub, mul, div, lp, rp
-}
-
-private func mLex(_ s: String) -> [MTok]? {
- var toks: [MTok] = []
- var i = s.startIndex
- func peek(_ k: Int) -> Character? {
- var j = i
- var n = 0
- while n < k && j < s.endIndex { j = s.index(after: j); n += 1 }
- return j < s.endIndex ? s[j] : nil
- }
- while i < s.endIndex {
- while i < s.endIndex && s[i].isWhitespace { i = s.index(after: i) }
- if i >= s.endIndex { break }
- let ch = s[i]
- if ch.isNumber || ((ch == "." || ch == ",") && (peek(1)?.isNumber == true)) {
- var whole: Int64 = 0
- var saw = false
- while i < s.endIndex && s[i].isNumber {
- saw = true
- whole = whole * 10 + Int64(s[i].wholeNumberValue ?? 0)
- i = s.index(after: i)
- }
- if i < s.endIndex && (s[i] == "." || s[i] == ",") && (s.index(after: i) < s.endIndex && s[s.index(after: i)].isNumber) {
- i = s.index(after: i)
- var frac: Int64 = 0
- var div: Int64 = 1
- while i < s.endIndex && s[i].isNumber {
- frac = frac * 10 + Int64(s[i].wholeNumberValue ?? 0)
- div *= 10
- i = s.index(after: i)
- }
- let n = whole * div + frac
- toks.append(.num(n, div))
- } else if saw {
- toks.append(.num(whole, 1))
- } else { return nil }
- continue
- }
- if ch == "+" { toks.append(.add); i = s.index(after: i); continue }
- if ch == "-" || ch == "−" || ch == "–" { toks.append(.sub); i = s.index(after: i); continue }
- if ch == "*" || ch == "×" || ch == "·" { toks.append(.mul); i = s.index(after: i); continue }
- if ch == "/" || ch == ":" { toks.append(.div); i = s.index(after: i); continue }
- if ch == "(" { toks.append(.lp); i = s.index(after: i); continue }
- if ch == ")" { toks.append(.rp); i = s.index(after: i); continue }
- if ch == "π" { toks.append(.pi); i = s.index(after: i); continue }
- if ch == "√" { toks.append(.sqrt); i = s.index(after: i); continue }
- if ch.isLetter {
- var j = i
- while j < s.endIndex && s[j].isLetter { j = s.index(after: j) }
- let w = s[i.. MTok? { i < t.count ? t[i] : nil }
- mutating func take() -> MTok? {
- if i >= t.count { return nil }
- defer { i += 1 }
- return t[i]
- }
- mutating func expr() -> MTerm? {
- guard var left = term() else { return nil }
- while let p = peek() {
- if case .add = p { _ = take() }
- else if case .sub = p { _ = take() }
- else { break }
- guard let r = term() else { return nil }
- if case .sub = p { left = mAdd(left, mScale(r, MFrac(n: -1, d: 1))) }
- else { left = mAdd(left, r) }
- if !left.ok { return nil }
- }
- return left
- }
- mutating func term() -> MTerm? {
- guard var left = unary() else { return nil }
- while let p = peek() {
- if case .mul = p { _ = take() }
- else if case .div = p { _ = take() }
- else { break }
- guard let r = unary() else { return nil }
- if case .div = p { left = mDivT(left, r) }
- else { left = mMulT(left, r) }
- if !left.ok { return nil }
- }
- return left
- }
- mutating func unary() -> MTerm? {
- if case .sub = peek() {
- _ = take()
- guard let v = unary() else { return nil }
- return mScale(v, MFrac(n: -1, d: 1))
- }
- if case .add = peek() {
- _ = take()
- return unary()
- }
- return factor()
- }
- mutating func factor() -> MTerm? {
- guard var left = primary() else { return nil }
- while true {
- guard let p = peek() else { break }
- switch p {
- case .sqrt, .pi, .lp:
- guard let r = primary() else { return nil }
- left = mMulT(left, r)
- if !left.ok { return nil }
- case .num:
- return nil
- default:
- return left
- }
- }
- return left
- }
- mutating func primary() -> MTerm? {
- guard let p = take() else { return nil }
- switch p {
- case .num(let n, let d):
- if case .div = peek(), case .num = t[safe: i + 1] {
- _ = take()
- if case .num(let n2, let d2) = take() {
- if n2 <= 0 || d2 != 1 || d != 1 { return nil }
- var o = MTerm()
- if !mSet(&o.a, n, n2) { return nil }
- return o
- }
- return nil
- }
- if d == 1, case .num(let n2, let d2) = peek(), n >= 0 {
- _ = take()
- if case .div = peek(), case .num(let n3, let d3) = t[safe: i + 1], d2 == 1, d3 == 1, n2 >= 0, n3 > 0 {
- _ = take()
- _ = take()
- var frac = MFrac()
- if !mSet(&frac, n2, n3) { return nil }
- var whole = MTerm()
- whole.a = MFrac(n: n, d: 1)
- var part = MTerm()
- part.a = frac
- return mAdd(whole, part)
- }
- i -= 1
- }
- var o = MTerm()
- if !mSet(&o.a, n, d) { return nil }
- return o
- case .pi:
- var o = MTerm()
- o.p = MFrac(n: 1, d: 1)
- return o
- case .sqrt:
- if case .lp = peek() {
- _ = take()
- guard let inner = expr(), case .rp = take() else { return nil }
- return mSqrtTerm(inner)
- }
- if case .num(let n, let d) = peek(), d == 1, n >= 0 {
- _ = take()
- return mSqrtTerm(MTerm(a: MFrac(n: n, d: 1)))
- }
- return nil
- case .lp:
- guard let inner = expr(), case .rp = take() else { return nil }
- return inner
- default:
- return nil
- }
- }
-}
-
-private extension Array {
- subscript(safe i: Int) -> Element? { indices.contains(i) ? self[i] : nil }
-}
-
-private func mSqrtTerm(_ inner: MTerm) -> MTerm? {
- if !inner.ok || inner.p.n != 0 || inner.rad != 0 || inner.b.n != 0 { return nil }
- if inner.a.n < 0 || inner.a.d != 1 { return nil }
- guard let s = mSqrtReduce(inner.a.n) else { return nil }
- var o = MTerm()
- o.b = MFrac(n: s.out, d: 1)
- o.rad = s.rad
- mNorm(&o)
- return o.ok ? o : nil
-}
-
-private func mPrep(_ raw: String) -> String {
- var s = raw.trimmingCharacters(in: .whitespaces)
- var again = true
- while again {
- again = false
- if s.count >= 2 {
- let pairs: [(Character, Character)] = [("(", ")"), ("[", "]"), ("{", "}")]
- if let last = s.last, pairs.contains(where: { $0.0 == s.first && $0.1 == last }) {
- s = String(s.dropFirst().dropLast()).trimmingCharacters(in: .whitespaces)
- again = true
- continue
- }
- }
- }
- s = mStripUnits(s).trimmingCharacters(in: .whitespaces)
- return s
-}
-
-private func mParseTerm(_ s: String) -> MTerm? {
- let s = mPrep(s)
- if s.isEmpty { return nil }
- guard let toks = mLex(s) else { return nil }
- if toks.isEmpty { return nil }
- var p = MParse(t: toks)
- guard let v = p.expr(), p.i == toks.count, v.ok else { return nil }
- return v
-}
-
-private func mExact(_ x: MTerm, _ y: MTerm) -> Bool {
- x.ok && y.ok && mSameF(x.a, y.a) && mSameF(x.b, y.b) && x.rad == y.rad && mSameF(x.p, y.p)
-}
-
-private func mDouble(_ t: MTerm) -> Double {
- var v = Double(t.a.n) / Double(t.a.d) + Double(t.p.n) / Double(t.p.d) * Double.pi
- if t.b.n != 0 && t.rad > 0 {
- v += Double(t.b.n) / Double(t.b.d) * (Double(t.rad)).squareRoot()
- }
- return v
-}
-
-private func mClose(_ x: MTerm, _ y: MTerm) -> Bool {
- if !x.ok || !y.ok { return false }
- if mExact(x, y) { return true }
- if x.p.n != 0 || y.p.n != 0 { return false }
- let diff = abs(mDouble(x) - mDouble(y))
- if x.b.n != 0 || y.b.n != 0 { return diff <= 0.001 }
- return diff <= 0.0005
-}
-
-private func mFold(_ s: String) -> String {
- var out = ""
- for ch in s.lowercased() {
- switch ch {
- case "á", "à", "ä", "â": out.append("a")
- case "č", "ć": out.append("c")
- case "ď": out.append("d")
- case "é", "ě", "ë", "ê": out.append("e")
- case "í", "ï": out.append("i")
- case "ň": out.append("n")
- case "ó", "ö", "ô": out.append("o")
- case "ř": out.append("r")
- case "š", "ś": out.append("s")
- case "ť": out.append("t")
- case "ú", "ů", "ü": out.append("u")
- case "ý": out.append("y")
- case "ž", "ź": out.append("z")
- default:
- if ch.isLetter || ch.isNumber { out.append(ch) }
- }
- }
- return out
-}
-
-private func mStripUnits(_ raw: String) -> String {
- var s = raw.trimmingCharacters(in: .whitespaces)
- let units = ["procenta", "procent", "cm", "mm", "dm", "km", "m"]
- var cut = true
- while cut {
- cut = false
- while s.last?.isWhitespace == true { s.removeLast() }
- if s.hasSuffix("°") || s.hasSuffix("%") || s.hasSuffix(".") {
- s.removeLast()
- cut = true
- continue
- }
- for u in units {
- guard s.count > u.count else { continue }
- if s.lowercased().hasSuffix(u) {
- let keep = s.dropLast(u.count)
- if keep.last?.isLetter == true { continue }
- s = String(keep)
- cut = true
- break
- }
- }
- }
- return s.trimmingCharacters(in: .whitespaces)
-}
-
-private func mSplit(_ s: String) -> [String]? {
- let s = s.trimmingCharacters(in: .whitespaces)
- if s.isEmpty { return nil }
- let semi = s.contains(";")
- var parts: [String] = []
- var rest = Substring(s)
- while !rest.isEmpty {
- let end: Substring.Index
- let step: Int
- if semi {
- if let i = rest.firstIndex(of: ";") { end = i; step = 1 }
- else { end = rest.endIndex; step = 0 }
- } else if let r = rest.range(of: ", ") {
- end = r.lowerBound
- step = 2
- } else {
- end = rest.endIndex
- step = 0
- }
- let piece = rest[.. Bool {
- guard var spec, let user else { return false }
- spec = spec.trimmingCharacters(in: .whitespaces)
- if spec.hasPrefix("word:") {
- let opts = spec.dropFirst(5).split(separator: "|").map { mFold(String($0)) }
- return opts.contains(mFold(user))
- }
- if spec.hasPrefix("set:") || spec.hasPrefix("pair:") {
- let ordered = spec.hasPrefix("pair:")
- let body = String(spec.dropFirst(ordered ? 5 : 4))
- guard let want = mSplit(body), let got = mSplit(user) else { return false }
- if want.count != got.count || want.isEmpty { return false }
- var wt: [MTerm] = []
- var gt: [MTerm] = []
- for w in want {
- guard let t = mParseTerm(w) else { return false }
- wt.append(t)
- }
- for g in got {
- guard let t = mParseTerm(g) else { return false }
- gt.append(t)
- }
- if ordered {
- for i in wt.indices where !mClose(wt[i], gt[i]) { return false }
- return true
- }
- var used = Array(repeating: false, count: gt.count)
- for w in wt {
- var hit = gt.indices.first { !used[$0] && mExact(w, gt[$0]) }
- if hit == nil { hit = gt.indices.first { !used[$0] && mClose(w, gt[$0]) } }
- guard let hit else { return false }
- used[hit] = true
- }
- return true
- }
- let body = spec.hasPrefix("num:") ? String(spec.dropFirst(4)) : spec
- guard let tw = mParseTerm(body), let tg = mParseTerm(user) else { return false }
- return mClose(tw, tg)
-}
-
-func mathIsDraw(_ spec: String?) -> Bool {
- guard let spec else { return false }
- return spec.hasPrefix("square:") || spec.hasPrefix("mid:") || spec.hasPrefix("right:")
- || spec.hasPrefix("para:") || spec.hasPrefix("points:")
-}
-
-func mathDrawNeed(_ spec: String?) -> Int {
- guard let spec else { return 0 }
- if spec.hasPrefix("square:") { return 2 }
- if spec.hasPrefix("mid:") || spec.hasPrefix("right:") || spec.hasPrefix("para:") { return 1 }
- if spec.hasPrefix("points:") {
- let body = spec.dropFirst(7)
- return body.split(separator: ";").filter { !$0.hasPrefix("fix:") }.count
- }
- return 0
-}
-
-private func mParseXY(_ s: Substring) -> (Int, Int)? {
- let t = s.trimmingCharacters(in: .whitespaces)
- let bits = t.split(separator: ",")
- guard bits.count == 2, let x = Int(bits[0].trimmingCharacters(in: .whitespaces)),
- let y = Int(bits[1].trimmingCharacters(in: .whitespaces)) else { return nil }
- return (x, y)
-}
-
-func mathDrawGiven(_ spec: String?) -> [(Int, Int)] {
- guard let spec else { return [] }
- if spec.hasPrefix("points:") {
- return spec.dropFirst(7).split(separator: ";").compactMap { part -> (Int, Int)? in
- guard part.hasPrefix("fix:") else { return nil }
- return mParseXY(part.dropFirst(4))
- }
- }
- let body: Substring
- let n: Int
- if spec.hasPrefix("para:") { body = spec.dropFirst(5); n = 3 }
- else if spec.hasPrefix("square:") { body = spec.dropFirst(7); n = 2 }
- else if spec.hasPrefix("right:") { body = spec.dropFirst(6); n = 2 }
- else if spec.hasPrefix("mid:") { body = spec.dropFirst(4); n = 2 }
- else { return [] }
- return body.split(separator: ";").prefix(n).compactMap { mParseXY($0) }
-}
-
-private func mOnBoard(_ p: (Int, Int)) -> Bool {
- p.0 >= 0 && p.0 <= 10 && p.1 >= 0 && p.1 <= 8
-}
-
-func mathDrawOk(_ spec: String?, _ pts: [(Int, Int)]) -> Bool {
- guard let spec, mathDrawNeed(spec) == pts.count else { return false }
- if pts.contains(where: { !mOnBoard($0) }) { return false }
- if Set(pts.map { "\($0.0),\($0.1)" }).count != pts.count { return false }
- let g = mathDrawGiven(spec)
- if spec.hasPrefix("mid:") {
- guard g.count == 2, (g[0].0 + g[1].0) % 2 == 0, (g[0].1 + g[1].1) % 2 == 0 else { return false }
- return pts[0] == ((g[0].0 + g[1].0) / 2, (g[0].1 + g[1].1) / 2)
- }
- if spec.hasPrefix("para:") {
- guard g.count == 3 else { return false }
- return pts[0] == (g[1].0 + g[2].0 - g[0].0, g[1].1 + g[2].1 - g[0].1)
- }
- if spec.hasPrefix("right:") {
- guard g.count == 2 else { return false }
- let c = pts[0]
- if c == g[0] || c == g[1] { return false }
- let ux = g[0].0 - c.0, uy = g[0].1 - c.1
- let vx = g[1].0 - c.0, vy = g[1].1 - c.1
- return ux * vx + uy * vy == 0 && (ux != 0 || uy != 0) && (vx != 0 || vy != 0)
- }
- if spec.hasPrefix("square:") {
- guard g.count == 2, pts.count == 2 else { return false }
- let a = g[0], b = g[1]
- let dx = b.0 - a.0, dy = b.1 - a.1
- if dx == 0 && dy == 0 { return false }
- let c1 = (b.0 - dy, b.1 + dx), d1 = (a.0 - dy, a.1 + dx)
- let c2 = (b.0 + dy, b.1 - dx), d2 = (a.0 + dy, a.1 - dx)
- func same(_ p: (Int, Int), _ q: (Int, Int), _ r: (Int, Int), _ s: (Int, Int)) -> Bool {
- (p == r && q == s) || (p == s && q == r)
- }
- if !mOnBoard(c1) || !mOnBoard(d1) {
- if !mOnBoard(c2) || !mOnBoard(d2) { return false }
- }
- return same(pts[0], pts[1], c1, d1) || same(pts[0], pts[1], c2, d2)
- }
- if spec.hasPrefix("points:") {
- let want = spec.dropFirst(7).split(separator: ";").compactMap { part -> (Int, Int)? in
- if part.hasPrefix("fix:") { return nil }
- return mParseXY(part)
- }
- if want.count != pts.count { return false }
- var used = Array(repeating: false, count: pts.count)
- for w in want {
- guard let j = pts.indices.first(where: { !used[$0] && pts[$0] == w }) else { return false }
- used[j] = true
- }
- return true
- }
- return false
-}
diff --git a/ios/Graduately/Data/Content.swift b/ios/Graduately/Data/Content.swift
deleted file mode 100644
index dafa975..0000000
--- a/ios/Graduately/Data/Content.swift
+++ /dev/null
@@ -1,267 +0,0 @@
-import Foundation
-
-enum AppConfig {
- static var commit: String {
- let raw = (Bundle.main.object(forInfoDictionaryKey: "GITCommit") as? String)?
- .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
- return raw.isEmpty || raw.hasPrefix("$(") ? "dev" : raw
- }
-
- static var versionName: String {
- Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "ios-1"
- }
-
- static var updateRepo: String {
- (Bundle.main.object(forInfoDictionaryKey: "UPDATE_REPO") as? String)
- ?? "pepaskopec-blip/Graduately"
- }
-
- static var updateBranch: String {
- (Bundle.main.object(forInfoDictionaryKey: "UPDATE_BRANCH") as? String) ?? "builds"
- }
-}
-
-final class Content {
- let raw: J
- let i18n: [String: (String?, String?)]
- let subjects: [J]
- let german: [J]
- let net: J
- let netLessons: [J]
- let netTasks: [J]
- let netAnswers: [[J]]
- let hw: [J]
- let net2: [J]
- let net3: [J]
- let net4: [J]
- let hw2: [J]
- let hw3: [J]
- let hw4: [J]
- let on: [J]
- let on2: [J]
- let on3: [J]
- let on4: [J]
- let en: [J]
- let en2: [J]
- let en3: [J]
- let en4: [J]
- let de: [J]
- let de2: [J]
- let de3: [J]
- let de4: [J]
- let lit: [J]
- let lit2: [J]
- let lit3: [J]
- let lit4: [J]
- let chem: [J]
- let bio: [J]
- let fyz: [J]
- let fyz2: [J]
- let fyz3: [J]
- let fyz4: [J]
- let tg: [J]
- let tg2: [J]
- let tg3: [J]
- let tg4: [J]
- let pg: [J]
- let pg2: [J]
- let pg3: [J]
- let pg4: [J]
- let sw: [J]
- let sw2: [J]
- let sw3: [J]
- let sw4: [J]
- let cs: [J]
- let cs2: [J]
- let cs3: [J]
- let cs4: [J]
- let mat0: [J]
- let mat: [J]
- let mat2: [J]
- let mat3: [J]
- let mat4: [J]
- let mluvnice: [J]
- let books: [J]
- let changelog: [J]
- let progressKeys: [String: String]
- let progressLegacy: [String: String]
-
- init(_ root: [String: Any]) {
- raw = J(root)
- let obj = root["i18n"] as? [String: Any] ?? [:]
- var map: [String: (String?, String?)] = [:]
- for (key, value) in obj {
- let e = J(value)
- let cs = e.strOrNull("cs")
- let en = e.strOrNull("en")
- map[key] = (cs, en)
- }
- i18n = map
- subjects = raw.arr("subjects")
- german = raw.arr("german")
- net = raw.obj("net") ?? J([:])
- netLessons = net.arr("lessons")
- netTasks = net.arr("tasks")
- if let a = net.o["answers"] as? [Any] {
- netAnswers = a.map { block in
- guard let rows = block as? [Any] else { return [] }
- return rows.compactMap { item in
- item is [String: Any] ? J(item) : nil
- }
- }
- } else {
- netAnswers = []
- }
- hw = raw.arr("hw")
- net2 = raw.arr("net2")
- net3 = raw.arr("net3")
- net4 = raw.arr("net4")
- hw2 = raw.arr("hw2")
- hw3 = raw.arr("hw3")
- hw4 = raw.arr("hw4")
- on = raw.arr("on")
- on2 = raw.arr("on2")
- on3 = raw.arr("on3")
- on4 = raw.arr("on4")
- en = raw.arr("en")
- en2 = raw.arr("en2")
- en3 = raw.arr("en3")
- en4 = raw.arr("en4")
- de = raw.arr("de")
- de2 = raw.arr("de2")
- de3 = raw.arr("de3")
- de4 = raw.arr("de4")
- lit = raw.arr("lit")
- lit2 = raw.arr("lit2")
- lit3 = raw.arr("lit3")
- lit4 = raw.arr("lit4")
- chem = raw.arr("chem")
- bio = raw.arr("bio")
- fyz = raw.arr("fyz")
- fyz2 = raw.arr("fyz2")
- fyz3 = raw.arr("fyz3")
- fyz4 = raw.arr("fyz4")
- tg = raw.arr("tg")
- tg2 = raw.arr("tg2")
- tg3 = raw.arr("tg3")
- tg4 = raw.arr("tg4")
- pg = raw.arr("pg")
- pg2 = raw.arr("pg2")
- pg3 = raw.arr("pg3")
- pg4 = raw.arr("pg4")
- sw = raw.arr("sw")
- sw2 = raw.arr("sw2")
- sw3 = raw.arr("sw3")
- sw4 = raw.arr("sw4")
- cs = raw.arr("cs")
- cs2 = raw.arr("cs2")
- cs3 = raw.arr("cs3")
- cs4 = raw.arr("cs4")
- mat0 = raw.arr("mat0")
- mat = raw.arr("mat")
- mat2 = raw.arr("mat2")
- mat3 = raw.arr("mat3")
- mat4 = raw.arr("mat4")
- mluvnice = raw.arr("mluvnice")
- books = raw.arr("books")
- changelog = raw.arr("changelog")
- let progress = root["progress"] as? [String: Any]
- progressKeys = progress?["keys"] as? [String: String] ?? [:]
- progressLegacy = progress?["legacy"] as? [String: String] ?? [:]
- }
-
- func germanUnit(_ id: Int) -> J? { german.first { $0.int("id") == id } }
- func netLesson(_ id: Int) -> J? { netLessons.first { $0.int("id") == id } }
- func hwLesson(_ id: Int) -> J? { hw.first { $0.int("id") == id } }
- func itLessons(_ course: String) -> [J] {
- switch course {
- case "net2": return net2
- case "net3": return net3
- case "net4": return net4
- case "hw2": return hw2
- case "hw3": return hw3
- case "hw4": return hw4
- default: return []
- }
- }
- func itLesson(_ course: String, _ id: Int) -> J? {
- itLessons(course).first { $0.int("id") == id }
- }
- func onLesson(_ id: Int) -> J? { on.first { $0.int("id") == id } }
- func on2Lesson(_ id: Int) -> J? { on2.first { $0.int("id") == id } }
- func on3Lesson(_ id: Int) -> J? { on3.first { $0.int("id") == id } }
- func on4Lesson(_ id: Int) -> J? { on4.first { $0.int("id") == id } }
- func enYear(_ year: Int) -> [J] {
- switch year {
- case 2: return en2
- case 3: return en3
- case 4: return en4
- default: return en
- }
- }
- func enLesson(_ year: Int, _ id: Int) -> J? { enYear(year).first { $0.int("id") == id } }
- func deYear(_ year: Int) -> [J] {
- switch year {
- case 2: return de2
- case 3: return de3
- case 4: return de4
- default: return de
- }
- }
- func deLesson(_ year: Int, _ id: Int) -> J? { deYear(year).first { $0.int("id") == id } }
- func litLesson(_ id: Int) -> J? { lit.first { $0.int("id") == id } }
- func lit2Lesson(_ id: Int) -> J? { lit2.first { $0.int("id") == id } }
- func lit3Lesson(_ id: Int) -> J? { lit3.first { $0.int("id") == id } }
- func lit4Lesson(_ id: Int) -> J? { lit4.first { $0.int("id") == id } }
- func chemLesson(_ id: Int) -> J? { chem.first { $0.int("id") == id } }
- func bioLesson(_ id: Int) -> J? { bio.first { $0.int("id") == id } }
- func fyzLesson(_ id: Int) -> J? { fyz.first { $0.int("id") == id } }
- func fyz2Lesson(_ id: Int) -> J? { fyz2.first { $0.int("id") == id } }
- func fyz3Lesson(_ id: Int) -> J? { fyz3.first { $0.int("id") == id } }
- func fyz4Lesson(_ id: Int) -> J? { fyz4.first { $0.int("id") == id } }
- func tgLesson(_ id: Int) -> J? { tg.first { $0.int("id") == id } }
- func tg2Lesson(_ id: Int) -> J? { tg2.first { $0.int("id") == id } }
- func tg3Lesson(_ id: Int) -> J? { tg3.first { $0.int("id") == id } }
- func tg4Lesson(_ id: Int) -> J? { tg4.first { $0.int("id") == id } }
- func pgLesson(_ id: Int) -> J? { pg.first { $0.int("id") == id } }
- func pg2Lesson(_ id: Int) -> J? { pg2.first { $0.int("id") == id } }
- func pg3Lesson(_ id: Int) -> J? { pg3.first { $0.int("id") == id } }
- func pg4Lesson(_ id: Int) -> J? { pg4.first { $0.int("id") == id } }
- func swLesson(_ id: Int) -> J? { sw.first { $0.int("id") == id } }
- func sw2Lesson(_ id: Int) -> J? { sw2.first { $0.int("id") == id } }
- func sw3Lesson(_ id: Int) -> J? { sw3.first { $0.int("id") == id } }
- func sw4Lesson(_ id: Int) -> J? { sw4.first { $0.int("id") == id } }
- func csLesson(_ id: Int) -> J? { cs.first { $0.int("id") == id } }
- func cs2Lesson(_ id: Int) -> J? { cs2.first { $0.int("id") == id } }
- func cs3Lesson(_ id: Int) -> J? { cs3.first { $0.int("id") == id } }
- func cs4Lesson(_ id: Int) -> J? { cs4.first { $0.int("id") == id } }
- func mat0Lesson(_ id: Int) -> J? { mat0.first { $0.int("id") == id } }
- func matLesson(_ id: Int) -> J? { mat.first { $0.int("id") == id } }
- func mat2Lesson(_ id: Int) -> J? { mat2.first { $0.int("id") == id } }
- func mat3Lesson(_ id: Int) -> J? { mat3.first { $0.int("id") == id } }
- func mat4Lesson(_ id: Int) -> J? { mat4.first { $0.int("id") == id } }
- func mluv(_ n: Int) -> J? { mluvnice.first { $0.int("id") == n } }
- func book(_ id: String) -> J? { books.first { $0.str("id") == id } }
-
- func tr(_ key: String?, lang: UiLang) -> String {
- guard let key, !key.isEmpty else { return "" }
- let hit = i18n[key]
- if lang == .en { return hit?.1 ?? key }
- return hit?.0 ?? key
- }
-
- func fmt(_ key: String, lang: UiLang, _ args: [CVarArg]) -> String {
- let template = tr(key, lang: lang).replacingOccurrences(of: "%s", with: "%@")
- return String(format: template, arguments: args)
- }
-
- static func load() -> Content {
- guard let url = Bundle.main.url(forResource: "content", withExtension: "json"),
- let data = try? Data(contentsOf: url),
- let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
- else {
- return Content([:])
- }
- return Content(obj)
- }
-}
diff --git a/ios/Graduately/Data/JSON.swift b/ios/Graduately/Data/JSON.swift
deleted file mode 100644
index 137e769..0000000
--- a/ios/Graduately/Data/JSON.swift
+++ /dev/null
@@ -1,102 +0,0 @@
-import Foundation
-
-struct J {
- let o: [String: Any]
-
- init(_ obj: Any?) {
- o = obj as? [String: Any] ?? [:]
- }
-
- func has(_ key: String) -> Bool {
- guard let value = o[key], !(value is NSNull) else { return false }
- return true
- }
-
- func str(_ key: String, _ fallback: String = "") -> String {
- guard has(key) else { return fallback }
- if let s = o[key] as? String { return s }
- if let n = o[key] as? NSNumber { return n.stringValue }
- return fallback
- }
-
- func strOrNull(_ key: String) -> String? {
- guard has(key) else { return nil }
- if let s = o[key] as? String, !s.isEmpty, s != "null" { return s }
- return nil
- }
-
- func bool(_ key: String, _ fallback: Bool = false) -> Bool {
- if let b = o[key] as? Bool { return b }
- if let n = o[key] as? NSNumber { return n.boolValue }
- return fallback
- }
-
- func int(_ key: String, _ fallback: Int = 0) -> Int {
- if let n = o[key] as? Int { return n }
- if let n = o[key] as? NSNumber { return n.intValue }
- if let s = o[key] as? String, let n = Int(s) { return n }
- return fallback
- }
-
- func obj(_ key: String) -> J? {
- guard let value = o[key], !(value is NSNull) else { return nil }
- if value is [String: Any] { return J(value) }
- return nil
- }
-
- func exercise(_ n: Int) -> J? {
- guard let ex = o["exercises"] as? [String: Any] else { return nil }
- return J(ex["\(n)"])
- }
-
- func arr(_ key: String) -> [J] {
- guard let a = o[key] as? [Any] else { return [] }
- return a.compactMap { item in
- item is [String: Any] ? J(item) : nil
- }
- }
-
- func strs(_ key: String) -> [String] {
- guard let a = o[key] as? [Any] else { return [] }
- return a.compactMap { item in
- if item is NSNull { return nil }
- if let s = item as? String, !s.isEmpty { return s }
- return nil
- }
- }
-
- func strsOrEmpty(_ key: String) -> [String?] {
- guard let a = o[key] as? [Any] else { return [] }
- return a.map { item in
- if item is NSNull { return nil }
- return item as? String
- }
- }
-
- func intRows(_ key: String) -> [[String]] {
- guard let a = o[key] as? [Any] else { return [] }
- return a.map { row in
- guard let cells = row as? [Any] else { return [] }
- return cells.map { cell in
- if let s = cell as? String { return s }
- if let n = cell as? NSNumber { return n.stringValue }
- return ""
- }
- }
- }
-
- func rawArray(_ key: String) -> [Any] {
- o[key] as? [Any] ?? []
- }
-
- func answerList() -> [String] {
- if let a = o["answers"] as? [Any] {
- return a.compactMap { item in
- if item is NSNull { return nil }
- if let s = item as? String, !s.isEmpty { return s }
- return nil
- }
- }
- return [strOrNull("answers")].compactMap { $0 }
- }
-}
diff --git a/ios/Graduately/Data/ProgressStore.swift b/ios/Graduately/Data/ProgressStore.swift
deleted file mode 100644
index f77c330..0000000
--- a/ios/Graduately/Data/ProgressStore.swift
+++ /dev/null
@@ -1,497 +0,0 @@
-import Foundation
-
-final class ProgressStore {
- private let defaults = UserDefaults.standard
- private let ids: [String: String]
-
- init(content: Content) {
- ids = content.progressKeys
- migrate(content.progressLegacy)
- }
-
- /// Progress is stored under each lesson's id from content/, so lessons can be
- /// reordered or renamed without losing it. `address` is the in-app position.
- private func key(_ address: String) -> String { "done." + (ids[address] ?? address) }
- private func done(_ address: String) -> Bool { defaults.bool(forKey: key(address)) }
- private func mark(_ address: String) { defaults.set(true, forKey: key(address)) }
-
- private func migrate(_ legacy: [String: String]) {
- guard !defaults.bool(forKey: "progress_ids_v2") else { return }
- for (old, id) in legacy where defaults.bool(forKey: old) {
- defaults.set(true, forKey: "done." + id)
- }
- defaults.set(true, forKey: "progress_ids_v2")
- }
-
- var themeId: ThemeId {
- get { ThemeId(rawValue: defaults.integer(forKey: "theme")) ?? .catppuccin }
- set { defaults.set(newValue.rawValue, forKey: "theme") }
- }
-
- var mode: ColorMode {
- get { defaults.string(forKey: "mode") == "dark" ? .dark : .light }
- set { defaults.set(newValue == .light ? "light" : "dark", forKey: "mode") }
- }
-
- var lang: UiLang {
- get { defaults.string(forKey: "lang") == "en" ? .en : .cs }
- set { defaults.set(newValue == .en ? "en" : "cs", forKey: "lang") }
- }
-
- var seenCommit: String {
- get { defaults.string(forKey: "seen_commit") ?? "" }
- set { defaults.set(newValue, forKey: "seen_commit") }
- }
-
- func germanDone(_ unit: Int, _ ex: Int) -> Bool { done("g.\(unit).\(ex)") }
- func markGerman(_ unit: Int, _ ex: Int) { mark("g.\(unit).\(ex)") }
- func vocabDone(_ unit: Int) -> Bool { done("g.\(unit).vocab") }
- func markVocab(_ unit: Int) { mark("g.\(unit).vocab") }
-
- func netDone(_ id: Int) -> Bool { done("net.\(id)") }
- func markNet(_ id: Int) { mark("net.\(id)") }
-
- func hwDone(_ id: Int) -> Bool { done("hw.\(id)") }
- func markHw(_ id: Int) { mark("hw.\(id)") }
-
- func onDone(_ id: Int) -> Bool { done("on.\(id)") }
- func markOn(_ id: Int) { mark("on.\(id)") }
-
- func on2Done(_ id: Int) -> Bool { done("on2.\(id)") }
- func markOn2(_ id: Int) { mark("on2.\(id)") }
-
- func on3Done(_ id: Int) -> Bool { done("on3.\(id)") }
- func markOn3(_ id: Int) { mark("on3.\(id)") }
-
- func on4Done(_ id: Int) -> Bool { done("on4.\(id)") }
- func markOn4(_ id: Int) { mark("on4.\(id)") }
-
- func enDone(_ year: Int, _ id: Int) -> Bool { done("en.\(year).\(id)") }
- func markEn(_ year: Int, _ id: Int) { mark("en.\(year).\(id)") }
- func deDone(_ year: Int, _ id: Int) -> Bool { done("de.\(year).\(id)") }
- func markDe(_ year: Int, _ id: Int) { mark("de.\(year).\(id)") }
- func courseDone(_ course: String, _ id: Int) -> Bool { done("\(course).\(id)") }
- func markCourse(_ course: String, _ id: Int) { mark("\(course).\(id)") }
-
- func litDone(_ id: Int) -> Bool { done("lit.\(id)") }
- func markLit(_ id: Int) { mark("lit.\(id)") }
- func lit2Done(_ id: Int) -> Bool { done("lit2.\(id)") }
- func markLit2(_ id: Int) { mark("lit2.\(id)") }
- func lit3Done(_ id: Int) -> Bool { done("lit3.\(id)") }
- func markLit3(_ id: Int) { mark("lit3.\(id)") }
- func lit4Done(_ id: Int) -> Bool { done("lit4.\(id)") }
- func markLit4(_ id: Int) { mark("lit4.\(id)") }
-
- func chemDone(_ id: Int) -> Bool { done("chem.\(id)") }
- func markChem(_ id: Int) { mark("chem.\(id)") }
- func bioDone(_ id: Int) -> Bool { done("bio.\(id)") }
- func markBio(_ id: Int) { mark("bio.\(id)") }
- func fyzDone(_ id: Int) -> Bool { done("fyz.\(id)") }
- func markFyz(_ id: Int) { mark("fyz.\(id)") }
- func fyz2Done(_ id: Int) -> Bool { done("fyz2.\(id)") }
- func markFyz2(_ id: Int) { mark("fyz2.\(id)") }
- func fyz3Done(_ id: Int) -> Bool { done("fyz3.\(id)") }
- func markFyz3(_ id: Int) { mark("fyz3.\(id)") }
- func fyz4Done(_ id: Int) -> Bool { done("fyz4.\(id)") }
- func markFyz4(_ id: Int) { mark("fyz4.\(id)") }
- func tgDone(_ id: Int) -> Bool { done("tg.\(id)") }
- func markTg(_ id: Int) { mark("tg.\(id)") }
- func tg2Done(_ id: Int) -> Bool { done("tg2.\(id)") }
- func markTg2(_ id: Int) { mark("tg2.\(id)") }
- func tg3Done(_ id: Int) -> Bool { done("tg3.\(id)") }
- func markTg3(_ id: Int) { mark("tg3.\(id)") }
- func tg4Done(_ id: Int) -> Bool { done("tg4.\(id)") }
- func markTg4(_ id: Int) { mark("tg4.\(id)") }
- func pgDone(_ id: Int) -> Bool { done("pg.\(id)") }
- func markPg(_ id: Int) { mark("pg.\(id)") }
- func pg2Done(_ id: Int) -> Bool { done("pg2.\(id)") }
- func markPg2(_ id: Int) { mark("pg2.\(id)") }
- func pg3Done(_ id: Int) -> Bool { done("pg3.\(id)") }
- func markPg3(_ id: Int) { mark("pg3.\(id)") }
- func pg4Done(_ id: Int) -> Bool { done("pg4.\(id)") }
- func markPg4(_ id: Int) { mark("pg4.\(id)") }
- func swDone(_ id: Int) -> Bool { done("sw.\(id)") }
- func markSw(_ id: Int) { mark("sw.\(id)") }
- func sw2Done(_ id: Int) -> Bool { done("sw2.\(id)") }
- func markSw2(_ id: Int) { mark("sw2.\(id)") }
- func sw3Done(_ id: Int) -> Bool { done("sw3.\(id)") }
- func markSw3(_ id: Int) { mark("sw3.\(id)") }
- func sw4Done(_ id: Int) -> Bool { done("sw4.\(id)") }
- func markSw4(_ id: Int) { mark("sw4.\(id)") }
- func csDone(_ id: Int) -> Bool { done("cs.\(id)") }
- func markCs(_ id: Int) { mark("cs.\(id)") }
- func cs2Done(_ id: Int) -> Bool { done("cs2.\(id)") }
- func markCs2(_ id: Int) { mark("cs2.\(id)") }
- func cs3Done(_ id: Int) -> Bool { done("cs3.\(id)") }
- func markCs3(_ id: Int) { mark("cs3.\(id)") }
- func cs4Done(_ id: Int) -> Bool { done("cs4.\(id)") }
- func markCs4(_ id: Int) { mark("cs4.\(id)") }
- func mat0Done(_ id: Int) -> Bool { done("mat0.\(id)") }
- func markMat0(_ id: Int) { mark("mat0.\(id)") }
- func matDone(_ id: Int) -> Bool { done("mat.\(id)") }
- func markMat(_ id: Int) { mark("mat.\(id)") }
- func mat2Done(_ id: Int) -> Bool { done("mat2.\(id)") }
- func markMat2(_ id: Int) { mark("mat2.\(id)") }
- func mat3Done(_ id: Int) -> Bool { done("mat3.\(id)") }
- func markMat3(_ id: Int) { mark("mat3.\(id)") }
- func mat4Done(_ id: Int) -> Bool { done("mat4.\(id)") }
- func markMat4(_ id: Int) { mark("mat4.\(id)") }
-
- func mluvDone(_ n: Int) -> Bool { done("mluv.\(n)") }
- func markMluv(_ n: Int) { mark("mluv.\(n)") }
-
- func bookQuiz(_ id: String) -> Bool { done("book.\(id).quiz") }
- func markBookQuiz(_ id: String) { mark("book.\(id).quiz") }
- func bookPlot(_ id: String) -> Bool { done("book.\(id).plot") }
- func markBookPlot(_ id: String) { mark("book.\(id).plot") }
-}
-
-struct ProgressSum {
- var doneEx = 0
- var totalEx = 0
- var doneUnits = 0
- var openUnits = 0
-}
-
-func summarize(_ content: Content, _ p: ProgressStore) -> ProgressSum {
- var sum = ProgressSum()
- for u in content.german where u.bool("unlocked") {
- let id = u.int("id")
- let names = u.strs("names")
- sum.totalEx += names.count
- sum.openUnits += 1
- let done = names.indices.filter { p.germanDone(id, $0 + 1) }.count
- sum.doneEx += done
- if !names.isEmpty && done == names.count { sum.doneUnits += 1 }
- }
- for l in content.netLessons {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.netDone(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.hw {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.hwDone(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for course in ["net2", "net3", "net4", "hw2", "hw3", "hw4"] {
- for l in content.itLessons(course) {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.courseDone(course, l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- }
- for l in content.on {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.onDone(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.on2 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.on2Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.on3 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.on3Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.on4 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.on4Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for year in 1...4 {
- for l in content.enYear(year) {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.enDone(year, l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.deYear(year) {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.deDone(year, l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- }
- for l in content.lit {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.litDone(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.lit2 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.lit2Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.lit3 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.lit3Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.lit4 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.lit4Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.chem {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.chemDone(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.bio {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.bioDone(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.fyz {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.fyzDone(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.fyz2 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.fyz2Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.fyz3 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.fyz3Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.fyz4 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.fyz4Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.tg {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.tgDone(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.tg2 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.tg2Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.tg3 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.tg3Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.tg4 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.tg4Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.pg {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.pgDone(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.pg2 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.pg2Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.pg3 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.pg3Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.pg4 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.pg4Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.sw {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.swDone(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.sw2 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.sw2Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.sw3 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.sw3Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.sw4 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.sw4Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.cs {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.csDone(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.cs2 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.cs2Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.cs3 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.cs3Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.cs4 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.cs4Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.mat0 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.mat0Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.mat {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.matDone(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.mat2 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.mat2Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.mat3 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.mat3Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- for l in content.mat4 {
- sum.totalEx += 1
- sum.openUnits += 1
- if p.mat4Done(l.int("id")) {
- sum.doneEx += 1
- sum.doneUnits += 1
- }
- }
- sum.totalEx += content.mluvnice.count
- sum.openUnits += 1
- let md = content.mluvnice.filter { p.mluvDone($0.int("id")) }.count
- sum.doneEx += md
- if md == content.mluvnice.count && !content.mluvnice.isEmpty { sum.doneUnits += 1 }
- return sum
-}
diff --git a/ios/Graduately/Data/Theme.swift b/ios/Graduately/Data/Theme.swift
deleted file mode 100644
index fb439aa..0000000
--- a/ios/Graduately/Data/Theme.swift
+++ /dev/null
@@ -1,199 +0,0 @@
-import SwiftUI
-
-enum ThemeId: Int, CaseIterable {
- case catppuccin, nord, dracula, rosePine, ocean, gruvbox, solarized, everforest, monokai, oneDark
-}
-
-enum ColorMode {
- case dark, light
-}
-
-enum UiLang {
- case cs, en
-}
-
-let themeNames = [
- "Catppuccin", "Nord", "Dracula", "Rose Pine", "Ocean",
- "Gruvbox", "Solarized", "Everforest", "Monokai", "One Dark",
-]
-
-struct Palette {
- let crust, base, mantle, surface0, surface1, surface2: Color
- let overlay, text, subtext: Color
- let accent, accent2, accent3: Color
- let success, success2, warning, error: Color
- let onAccent, node, lockedBg, lockedBorder, rail: Color
- let finishDark, finishLight, finishStroke: Color
- /// Accent that stays readable as text and icon tint on the theme's background.
- let tint: Color
-}
-
-private func hx(_ v: UInt32) -> Color {
- Color(
- red: Double((v >> 16) & 0xFF) / 255,
- green: Double((v >> 8) & 0xFF) / 255,
- blue: Double(v & 0xFF) / 255
- )
-}
-
-private func luminance(_ v: UInt32) -> Double {
- func channel(_ c: UInt32) -> Double {
- let s = Double(c) / 255
- return s <= 0.03928 ? s / 12.92 : pow((s + 0.055) / 1.055, 2.4)
- }
- return 0.2126 * channel((v >> 16) & 0xFF) + 0.7152 * channel((v >> 8) & 0xFF) + 0.0722 * channel(v & 0xFF)
-}
-
-private func contrast(_ a: UInt32, _ b: UInt32) -> Double {
- let la = luminance(a), lb = luminance(b)
- return (max(la, lb) + 0.05) / (min(la, lb) + 0.05)
-}
-
-/// Pick the first accent with enough contrast against the background; yellow on
-/// white is unreadable as tab labels and link text.
-private func readableTint(_ c: [UInt32]) -> UInt32 {
- let background = c[1]
- for candidate in [c[9], c[10], c[11]] where contrast(candidate, background) >= 3.0 {
- return candidate
- }
- return c[7]
-}
-
-private func pal(_ c: [UInt32]) -> Palette {
- Palette(
- crust: hx(c[0]), base: hx(c[1]), mantle: hx(c[2]), surface0: hx(c[3]),
- surface1: hx(c[4]), surface2: hx(c[5]), overlay: hx(c[6]), text: hx(c[7]),
- subtext: hx(c[8]), accent: hx(c[9]), accent2: hx(c[10]), accent3: hx(c[11]),
- success: hx(c[12]), success2: hx(c[13]), warning: hx(c[14]), error: hx(c[15]),
- onAccent: hx(c[16]), node: hx(c[17]), lockedBg: hx(c[18]),
- lockedBorder: hx(c[19]), rail: hx(c[20]), finishDark: hx(c[21]),
- finishLight: hx(c[22]), finishStroke: hx(c[23]),
- tint: hx(readableTint(c))
- )
-}
-
-func themePalette(_ id: ThemeId, _ mode: ColorMode) -> Palette {
- let light = mode == .light
- switch id {
- case .catppuccin:
- return light ? pal([
- 0xf3eee6, 0xfaf7f2, 0xffffff, 0xffffff, 0xf0ebe3, 0xe5dfd4,
- 0x8a8378, 0x1a1a1a, 0x5c574f, 0xffcc00, 0x0071e3, 0x2997ff,
- 0x1f8a4c, 0x1a7f4a, 0xe6a800, 0xd32f2f, 0x1a1a1a,
- 0xffffff, 0xf0ebe3, 0xd5cfc4, 0xe5dfd4, 0xf3eee6, 0xd5cfc4, 0x8a8378,
- ]) : pal([
- 0x000000, 0x000000, 0x111111, 0x1d1d1f, 0x2c2c2e, 0x3a3a3c,
- 0x86868b, 0xf5f5f7, 0xa1a1a6, 0xffd60a, 0x2997ff, 0x64d2ff,
- 0x30d158, 0x64d2ff, 0xffd60a, 0xff453a, 0x1d1d1f,
- 0x1d1d1f, 0x111111, 0x3a3a3c, 0x2c2c2e, 0x000000, 0x3a3a3c, 0x86868b,
- ])
- case .nord:
- return light ? pal([
- 0xd8dee9, 0xeceff4, 0xe5e9f0, 0xd8dee9, 0xc7ceda, 0xb0b8c8,
- 0x7b88a1, 0x2e3440, 0x4c566a, 0x5e81ac, 0x81a1c1, 0x5e81ac,
- 0xa3be8c, 0x8fbcbb, 0xebcb8b, 0xbf616a, 0xeceff4,
- 0xe5e9f0, 0xd8dee9, 0xc7ceda, 0xd8dee9, 0xd8dee9, 0xc7ceda, 0x7b88a1,
- ]) : pal([
- 0x2e3440, 0x3b4252, 0x2e3440, 0x434c5e, 0x4c566a, 0x616e88,
- 0x7b88a1, 0xeceff4, 0xd8dee9, 0x88c0d0, 0x81a1c1, 0x5e81ac,
- 0xa3be8c, 0x8fbcbb, 0xebcb8b, 0xbf616a, 0x2e3440,
- 0x434c5e, 0x3b4252, 0x4c566a, 0x2e3440, 0x2e3440, 0x434c5e, 0x4c566a,
- ])
- case .dracula:
- return light ? pal([
- 0xd5d7e0, 0xf8f8f2, 0xefefea, 0xe2e2d8, 0xd0d0c4, 0xbfbfb2,
- 0x9a9a8c, 0x282a36, 0x44475a, 0x7c3aed, 0xd946a6, 0x0891b2,
- 0x16a34a, 0x0e7490, 0xca8a04, 0xdc2626, 0xf8f8f2,
- 0xefefea, 0xe2e2d8, 0xd0d0c4, 0xe2e2d8, 0xd5d7e0, 0xd0d0c4, 0x9a9a8c,
- ]) : pal([
- 0x191a21, 0x282a36, 0x21222c, 0x44475a, 0x6272a4, 0x707eb0,
- 0x6272a4, 0xf8f8f2, 0xbfbfb2, 0xbd93f9, 0xff79c6, 0x8be9fd,
- 0x50fa7b, 0x8be9fd, 0xf1fa8c, 0xff5555, 0x282a36,
- 0x383a4a, 0x2f3240, 0x44475a, 0x21222c, 0x191a21, 0x383a4a, 0x6272a4,
- ])
- case .rosePine:
- return light ? pal([
- 0xf2e9e1, 0xfaf4ed, 0xfffaf3, 0xf2e9e1, 0xdfdad9, 0xcecacd,
- 0x9893a5, 0x575279, 0x797593, 0x907aa9, 0xd7827e, 0x56949f,
- 0x286983, 0x56949f, 0xea9d34, 0xb4637a, 0xfaf4ed,
- 0xfffaf3, 0xf2e9e1, 0xdfdad9, 0xf2e9e1, 0xf2e9e1, 0xdfdad9, 0x9893a5,
- ]) : pal([
- 0x191724, 0x1f1d2e, 0x1f1d2e, 0x26233a, 0x403d52, 0x524f67,
- 0x6e6a86, 0xe0def4, 0x908caa, 0xc4a7e7, 0xebbcba, 0x9ccfd8,
- 0x31748f, 0x9ccfd8, 0xf6c177, 0xeb6f92, 0x191724,
- 0x26233a, 0x21202e, 0x403d52, 0x191724, 0x191724, 0x26233a, 0x524f67,
- ])
- case .ocean:
- return light ? pal([
- 0xd6dae8, 0xe1e2ec, 0xd5d6e2, 0xc8c9d6, 0xb4b6c5, 0x9aa0b5,
- 0x7a8199, 0x2e3446, 0x4a5168, 0x2e7de9, 0x7847bd, 0x007197,
- 0x387068, 0x007197, 0x8c6c3e, 0xc64343, 0xe1e2ec,
- 0xd5d6e2, 0xc8c9d6, 0xb4b6c5, 0xc8c9d6, 0xd6dae8, 0xb4b6c5, 0x7a8199,
- ]) : pal([
- 0x16161e, 0x1a1b26, 0x16161e, 0x24283b, 0x414868, 0x565f89,
- 0x565f89, 0xc0caf5, 0xa9b1d6, 0x7aa2f7, 0xbb9af7, 0x7dcfff,
- 0x9ece6a, 0x73daca, 0xe0af68, 0xf7768e, 0x1a1b26,
- 0x24283b, 0x1f2335, 0x3b4261, 0x16161e, 0x16161e, 0x24283b, 0x414868,
- ])
- case .gruvbox:
- return light ? pal([
- 0xebdbb2, 0xfbf1c7, 0xf2e5bc, 0xebdbb2, 0xd5c4a1, 0xbdae93,
- 0x928374, 0x3c3836, 0x504945, 0xd65d0e, 0xb16286, 0x076678,
- 0x79740e, 0x427b58, 0xb57614, 0x9d0006, 0xfbf1c7,
- 0xf2e5bc, 0xebdbb2, 0xd5c4a1, 0xebdbb2, 0xebdbb2, 0xd5c4a1, 0x928374,
- ]) : pal([
- 0x1d2021, 0x282828, 0x1d2021, 0x3c3836, 0x504945, 0x665c54,
- 0x928374, 0xebdbb2, 0xa89984, 0xfe8019, 0xd3869b, 0x83a598,
- 0xb8bb26, 0x8ec07c, 0xfabd2f, 0xfb4934, 0x1d2021,
- 0x3c3836, 0x32302f, 0x504945, 0x1d2021, 0x1d2021, 0x3c3836, 0x504945,
- ])
- case .solarized:
- return light ? pal([
- 0xeee8d5, 0xfdf6e3, 0xeee8d5, 0xeee8d5, 0xe2dbc6, 0xc9c0a6,
- 0x657b83, 0x586e75, 0x657b83, 0x268bd2, 0x6c71c4, 0x2aa198,
- 0x859900, 0x2aa198, 0xb58900, 0xdc322f, 0xfdf6e3,
- 0xeee8d5, 0xe6dfc8, 0x93a1a1, 0xeee8d5, 0xeee8d5, 0x93a1a1, 0x586e75,
- ]) : pal([
- 0x002b36, 0x073642, 0x002b36, 0x586e75, 0x657b83, 0x839496,
- 0x93a1a1, 0xfdf6e3, 0x93a1a1, 0x268bd2, 0x6c71c4, 0x2aa198,
- 0x859900, 0x2aa198, 0xb58900, 0xdc322f, 0x002b36,
- 0x073642, 0x002b36, 0x586e75, 0x002b36, 0x002b36, 0x073642, 0x586e75,
- ])
- case .everforest:
- return light ? pal([
- 0xe6e2cc, 0xfdf6e3, 0xf4f0d9, 0xefebd4, 0xe0dcc7, 0xa6b0a0,
- 0x939f91, 0x5c6a72, 0x829181, 0x8da101, 0xdf69ba, 0x35a77c,
- 0x8da101, 0x35a77c, 0xdfa000, 0xf85552, 0xfdf6e3,
- 0xf4f0d9, 0xefebd4, 0xe0dcc7, 0xe6e2cc, 0xe6e2cc, 0xe0dcc7, 0x939f91,
- ]) : pal([
- 0x1e2326, 0x272e33, 0x2d353b, 0x374145, 0x4f5b58, 0x7a8478,
- 0x859289, 0xd3c6aa, 0xa7c080, 0xa7c080, 0xd699b6, 0x7fbbb3,
- 0xa7c080, 0x83c092, 0xdbbc7f, 0xe67e80, 0x272e33,
- 0x374145, 0x2d353b, 0x4f5b58, 0x1e2326, 0x1e2326, 0x374145, 0x4f5b58,
- ])
- case .monokai:
- return light ? pal([
- 0xe6e3d4, 0xfaf8f0, 0xf3f0e4, 0xe8e4d4, 0xd6d2c0, 0xb8b49e,
- 0x8a8674, 0x2d2a2e, 0x5b5854, 0xe14775, 0x7c5cbf, 0x1c7d9b,
- 0x5b8c2a, 0x1c7d9b, 0xb08900, 0xe14775, 0xfaf8f0,
- 0xf3f0e4, 0xe8e4d4, 0xd6d2c0, 0xe6e3d4, 0xe6e3d4, 0xd6d2c0, 0x8a8674,
- ]) : pal([
- 0x1d1e19, 0x272822, 0x1e1f1a, 0x3e3d32, 0x49483e, 0x75715e,
- 0x75715e, 0xf8f8f2, 0xa59f85, 0xf92672, 0xae81ff, 0x66d9ef,
- 0xa6e22e, 0xa1efe4, 0xe6db74, 0xf92672, 0x272822,
- 0x3e3d32, 0x2e2f29, 0x49483e, 0x1d1e19, 0x1d1e19, 0x3e3d32, 0x49483e,
- ])
- case .oneDark:
- return light ? pal([
- 0xe5e5e6, 0xfafafa, 0xf0f0f0, 0xe5e5e6, 0xd0d0d2, 0xa0a1a7,
- 0xa0a1a7, 0x383a42, 0x696c77, 0xa626a4, 0x4078f2, 0x0184bc,
- 0x50a14f, 0x0184bc, 0xc18401, 0xe45649, 0xfafafa,
- 0xf0f0f0, 0xe5e5e6, 0xd0d0d2, 0xe5e5e6, 0xe5e5e6, 0xd0d0d2, 0xa0a1a7,
- ]) : pal([
- 0x21252b, 0x282c34, 0x21252b, 0x3b4048, 0x4b5263, 0x5c6370,
- 0x5c6370, 0xabb2bf, 0x7f848e, 0xc678dd, 0x61afef, 0x56b6c2,
- 0x98c379, 0x56b6c2, 0xe5c07b, 0xe06c75, 0x282c34,
- 0x3b4048, 0x2c313a, 0x4b5263, 0x21252b, 0x21252b, 0x3b4048, 0x4b5263,
- ])
- }
-}
diff --git a/ios/Graduately/GraduatelyApp.swift b/ios/Graduately/GraduatelyApp.swift
deleted file mode 100644
index 366e93c..0000000
--- a/ios/Graduately/GraduatelyApp.swift
+++ /dev/null
@@ -1,104 +0,0 @@
-import SwiftUI
-
-@main
-struct GraduatelyApp: App {
- @StateObject private var vm = AppModel()
-
- var body: some Scene {
- WindowGroup {
- NativeRoot(vm: vm)
- }
- #if os(macOS)
- .defaultSize(width: 1280, height: 840)
- .windowResizability(.contentMinSize)
- #endif
- }
-}
-
-struct NativeRoot: View {
- @ObservedObject var vm: AppModel
- @State private var progressPath = NavigationPath()
- @State private var settingsPath = NavigationPath()
- @State private var searchPath = NavigationPath()
-
- var body: some View {
- Group {
- if showsUpdateAccessory {
- tabs.tabViewBottomAccessory { UpdateAccessory(vm: vm) }
- } else {
- tabs
- }
- }
- .tint(vm.palette.tint)
- .preferredColorScheme(vm.mode == .dark ? .dark : .light)
- .onAppear { vm.checkUpdate(false) }
- #if os(macOS)
- .frame(minWidth: 980, minHeight: 640)
- #endif
- }
-
- private var showsUpdateAccessory: Bool {
- vm.update.canInstall || vm.update.status == "downloading" || vm.update.status == "staged"
- }
-
- private var tabs: some View {
- TabView(selection: $vm.tab) {
- Tab(vm.tr("subjects_title"), systemImage: "square.grid.2x2.fill", value: .practice) {
- NavigationStack(path: $vm.practicePath) {
- PracticeHome(vm: vm)
- .navigationDestination(for: Route.self) { route in
- RouteDestination(vm: vm, route: route)
- }
- }
- }
- Tab(vm.tr("stats"), systemImage: "chart.bar.fill", value: .progress) {
- NavigationStack(path: $progressPath) {
- StatsScreen(vm: vm)
- }
- }
- Tab(vm.tr("settings"), systemImage: "gearshape.fill", value: .settings) {
- NavigationStack(path: $settingsPath) {
- SettingsScreen(vm: vm)
- }
- }
- Tab(vm.tr("search"), systemImage: "magnifyingglass", value: .search) {
- NavigationStack(path: $searchPath) {
- SearchScreen(vm: vm)
- }
- }
- }
- .tabViewStyle(.sidebarAdaptable)
- .tabBarMinimizeBehavior(.onScrollDown)
- }
-}
-
-private struct UpdateAccessory: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- HStack {
- Text(bannerText)
- #if os(macOS)
- .font(.body)
- #else
- .font(.subheadline)
- #endif
- Spacer()
- if vm.update.canInstall {
- Button(vm.tr("update_install")) { vm.installUpdate() }
- .buttonStyle(.borderedProminent)
- #if os(macOS)
- .controlSize(.large)
- #endif
- }
- }
- .padding(.horizontal, 4)
- }
-
- private var bannerText: String {
- if let arg = vm.update.messageArg {
- return vm.fmt(vm.update.messageKey, arg)
- }
- return vm.tr(vm.update.messageKey)
- }
-}
diff --git a/ios/Graduately/Info.plist b/ios/Graduately/Info.plist
deleted file mode 100644
index 01db115..0000000
--- a/ios/Graduately/Info.plist
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- GITCommit
- $(COMMIT)
- UILaunchScreen
-
- UPDATE_BRANCH
- builds
- UPDATE_REPO
- pepaskopec-blip/Graduately
-
-
diff --git a/ios/Graduately/Route.swift b/ios/Graduately/Route.swift
deleted file mode 100644
index a85395c..0000000
--- a/ios/Graduately/Route.swift
+++ /dev/null
@@ -1,1015 +0,0 @@
-import Foundation
-
-enum AppTab: Hashable {
- case practice
- case progress
- case search
- case settings
-}
-
-enum Route: Hashable {
- case welcome
- case subjects
- case stats
- case roadmap
- case deHome
- case deMap(Int)
- case deLesson(Int, Int)
- case deEx(Int, Int)
- case unitMap(Int)
- case germanEx(Int, Int)
- case vocab(Int)
- case netYears
- case netMap
- case netLesson(Int)
- case netEx(Int)
- case net2Map
- case net2Lesson(Int)
- case net2Ex(Int)
- case net3Map
- case net3Lesson(Int)
- case net3Ex(Int)
- case net4Map
- case net4Lesson(Int)
- case net4Ex(Int)
- case hwYears
- case hwMap
- case hwLesson(Int)
- case hwEx(Int)
- case hw2Map
- case hw2Lesson(Int)
- case hw2Ex(Int)
- case hw3Map
- case hw3Lesson(Int)
- case hw3Ex(Int)
- case hw4Map
- case hw4Lesson(Int)
- case hw4Ex(Int)
- case onYears
- case onMap
- case onLesson(Int)
- case onEx(Int)
- case on2Map
- case on2Lesson(Int)
- case on2Ex(Int)
- case on3Map
- case on3Lesson(Int)
- case on3Ex(Int)
- case on4Map
- case on4Lesson(Int)
- case on4Ex(Int)
- case enYears
- case enMap(Int)
- case enLesson(Int, Int)
- case enEx(Int, Int)
- case litYears
- case litMap
- case litLesson(Int)
- case litEx(Int)
- case lit2Map
- case lit2Lesson(Int)
- case lit2Ex(Int)
- case lit3Map
- case lit3Lesson(Int)
- case lit3Ex(Int)
- case lit4Map
- case lit4Lesson(Int)
- case lit4Ex(Int)
- case sciMap
- case chemMap
- case chemLesson(Int)
- case chemEx(Int)
- case bioMap
- case bioLesson(Int)
- case bioEx(Int)
- case matYears
- case mat0Map
- case mat0Lesson(Int)
- case mat0Ex(Int)
- case matMap
- case matLesson(Int)
- case matEx(Int)
- case mat2Map
- case mat2Lesson(Int)
- case mat2Ex(Int)
- case mat3Map
- case mat3Lesson(Int)
- case mat3Ex(Int)
- case mat4Map
- case mat4Lesson(Int)
- case mat4Ex(Int)
- case fyzYears
- case fyzMap
- case fyzLesson(Int)
- case fyzEx(Int)
- case fyz2Map
- case fyz2Lesson(Int)
- case fyz2Ex(Int)
- case fyz3Map
- case fyz3Lesson(Int)
- case fyz3Ex(Int)
- case fyz4Map
- case fyz4Lesson(Int)
- case fyz4Ex(Int)
- case tgYears
- case tgMap
- case tgLesson(Int)
- case tgEx(Int)
- case tg2Map
- case tg2Lesson(Int)
- case tg2Ex(Int)
- case tg3Map
- case tg3Lesson(Int)
- case tg3Ex(Int)
- case tg4Map
- case tg4Lesson(Int)
- case tg4Ex(Int)
- case pgYears
- case pgMap
- case pgLesson(Int)
- case pgEx(Int)
- case pg2Map
- case pg2Lesson(Int)
- case pg2Ex(Int)
- case pg3Map
- case pg3Lesson(Int)
- case pg3Ex(Int)
- case pg4Map
- case pg4Lesson(Int)
- case pg4Ex(Int)
- case swYears
- case swMap
- case swLesson(Int)
- case swEx(Int)
- case sw2Map
- case sw2Lesson(Int)
- case sw2Ex(Int)
- case sw3Map
- case sw3Lesson(Int)
- case sw3Ex(Int)
- case sw4Map
- case sw4Lesson(Int)
- case sw4Ex(Int)
- case csYears
- case csMap
- case csLesson(Int)
- case cs2Map
- case cs2Lesson(Int)
- case cs3Map
- case cs3Lesson(Int)
- case cs4Map
- case cs4Lesson(Int)
- case czechMap
- case mluvnice
- case mluvEx(Int)
- case readingList
- case book(String)
- case bookQuiz(String)
- case bookPlot(String)
-
- var page: String {
- switch self {
- case .welcome: return "welcome"
- case .subjects: return "subjects"
- case .stats: return "stats"
- case .roadmap: return "roadmap"
- case .deHome: return "dehome"
- case .deMap(let year): return "de\(year)map"
- case .deLesson(let year, let id): return "de\(year)unit\(id)"
- case .deEx(let year, let id): return "de\(year)ex\(id)"
- case .unitMap(let unitId): return "unit\(unitId + 1)"
- case .germanEx(let unitId, let ex): return "u\(unitId + 1)e\(ex)"
- case .vocab(let unitId): return "u\(unitId + 1)vocab"
- case .netYears: return "netyears"
- case .netMap: return "netmap"
- case .netLesson(let id): return "netunit\(id)"
- case .netEx(let id): return "netex\(id)"
- case .net2Map: return "net2map"
- case .net2Lesson(let id): return "net2unit\(id)"
- case .net2Ex(let id): return "net2ex\(id)"
- case .net3Map: return "net3map"
- case .net3Lesson(let id): return "net3unit\(id)"
- case .net3Ex(let id): return "net3ex\(id)"
- case .net4Map: return "net4map"
- case .net4Lesson(let id): return "net4unit\(id)"
- case .net4Ex(let id): return "net4ex\(id)"
- case .hwYears: return "hwyears"
- case .hwMap: return "hwmap"
- case .hwLesson(let id): return "hwunit\(id)"
- case .hwEx(let id): return "hwex\(id)"
- case .hw2Map: return "hw2map"
- case .hw2Lesson(let id): return "hw2unit\(id)"
- case .hw2Ex(let id): return "hw2ex\(id)"
- case .hw3Map: return "hw3map"
- case .hw3Lesson(let id): return "hw3unit\(id)"
- case .hw3Ex(let id): return "hw3ex\(id)"
- case .hw4Map: return "hw4map"
- case .hw4Lesson(let id): return "hw4unit\(id)"
- case .hw4Ex(let id): return "hw4ex\(id)"
- case .onYears: return "onyears"
- case .onMap: return "onmap"
- case .onLesson(let id): return "onunit\(id)"
- case .onEx(let id): return "onex\(id)"
- case .on2Map: return "on2map"
- case .on2Lesson(let id): return "on2unit\(id)"
- case .on2Ex(let id): return "on2ex\(id)"
- case .on3Map: return "on3map"
- case .on3Lesson(let id): return "on3unit\(id)"
- case .on3Ex(let id): return "on3ex\(id)"
- case .on4Map: return "on4map"
- case .on4Lesson(let id): return "on4unit\(id)"
- case .on4Ex(let id): return "on4ex\(id)"
- case .enYears: return "enyears"
- case .enMap(let year): return "en\(year)map"
- case .enLesson(let year, let id): return "en\(year)unit\(id)"
- case .enEx(let year, let id): return "en\(year)ex\(id)"
- case .litYears: return "lityears"
- case .litMap: return "litmap"
- case .litLesson(let id): return "litunit\(id)"
- case .litEx(let id): return "litex\(id)"
- case .lit2Map: return "lit2map"
- case .lit2Lesson(let id): return "lit2unit\(id)"
- case .lit2Ex(let id): return "lit2ex\(id)"
- case .lit3Map: return "lit3map"
- case .lit3Lesson(let id): return "lit3unit\(id)"
- case .lit3Ex(let id): return "lit3ex\(id)"
- case .lit4Map: return "lit4map"
- case .lit4Lesson(let id): return "lit4unit\(id)"
- case .lit4Ex(let id): return "lit4ex\(id)"
- case .sciMap: return "scimap"
- case .chemMap: return "chemmap"
- case .chemLesson(let id): return "chemunit\(id)"
- case .chemEx(let id): return "chemex\(id)"
- case .bioMap: return "biomap"
- case .bioLesson(let id): return "biounit\(id)"
- case .bioEx(let id): return "bioex\(id)"
- case .matYears: return "matyears"
- case .mat0Map: return "mat0map"
- case .mat0Lesson(let id): return "mat0unit\(id)"
- case .mat0Ex(let id): return "mat0ex\(id)"
- case .matMap: return "matmap"
- case .matLesson(let id): return "matunit\(id)"
- case .matEx(let id): return "matex\(id)"
- case .mat2Map: return "mat2map"
- case .mat2Lesson(let id): return "mat2unit\(id)"
- case .mat2Ex(let id): return "mat2ex\(id)"
- case .mat3Map: return "mat3map"
- case .mat3Lesson(let id): return "mat3unit\(id)"
- case .mat3Ex(let id): return "mat3ex\(id)"
- case .mat4Map: return "mat4map"
- case .mat4Lesson(let id): return "mat4unit\(id)"
- case .mat4Ex(let id): return "mat4ex\(id)"
- case .fyzYears: return "fyzyears"
- case .fyzMap: return "fyzmap"
- case .fyzLesson(let id): return "fyzunit\(id)"
- case .fyzEx(let id): return "fyzex\(id)"
- case .fyz2Map: return "fyz2map"
- case .fyz2Lesson(let id): return "fyz2unit\(id)"
- case .fyz2Ex(let id): return "fyz2ex\(id)"
- case .fyz3Map: return "fyz3map"
- case .fyz3Lesson(let id): return "fyz3unit\(id)"
- case .fyz3Ex(let id): return "fyz3ex\(id)"
- case .fyz4Map: return "fyz4map"
- case .fyz4Lesson(let id): return "fyz4unit\(id)"
- case .fyz4Ex(let id): return "fyz4ex\(id)"
- case .tgYears: return "tgyears"
- case .tgMap: return "tgmap"
- case .tgLesson(let id): return "tgunit\(id)"
- case .tgEx(let id): return "tgex\(id)"
- case .tg2Map: return "tg2map"
- case .tg2Lesson(let id): return "tg2unit\(id)"
- case .tg2Ex(let id): return "tg2ex\(id)"
- case .tg3Map: return "tg3map"
- case .tg3Lesson(let id): return "tg3unit\(id)"
- case .tg3Ex(let id): return "tg3ex\(id)"
- case .tg4Map: return "tg4map"
- case .tg4Lesson(let id): return "tg4unit\(id)"
- case .tg4Ex(let id): return "tg4ex\(id)"
- case .pgYears: return "pgyears"
- case .pgMap: return "pgmap"
- case .pgLesson(let id): return "pgunit\(id)"
- case .pgEx(let id): return "pgex\(id)"
- case .pg2Map: return "pg2map"
- case .pg2Lesson(let id): return "pg2unit\(id)"
- case .pg2Ex(let id): return "pg2ex\(id)"
- case .pg3Map: return "pg3map"
- case .pg3Lesson(let id): return "pg3unit\(id)"
- case .pg3Ex(let id): return "pg3ex\(id)"
- case .pg4Map: return "pg4map"
- case .pg4Lesson(let id): return "pg4unit\(id)"
- case .pg4Ex(let id): return "pg4ex\(id)"
- case .swYears: return "swyears"
- case .swMap: return "swmap"
- case .swLesson(let id): return "swunit\(id)"
- case .swEx(let id): return "swex\(id)"
- case .sw2Map: return "sw2map"
- case .sw2Lesson(let id): return "sw2unit\(id)"
- case .sw2Ex(let id): return "sw2ex\(id)"
- case .sw3Map: return "sw3map"
- case .sw3Lesson(let id): return "sw3unit\(id)"
- case .sw3Ex(let id): return "sw3ex\(id)"
- case .sw4Map: return "sw4map"
- case .sw4Lesson(let id): return "sw4unit\(id)"
- case .sw4Ex(let id): return "sw4ex\(id)"
- case .csYears: return "csyears"
- case .csMap: return "csmap"
- case .csLesson(let id): return "csunit\(id)"
- case .cs2Map: return "cs2map"
- case .cs2Lesson(let id): return "cs2unit\(id)"
- case .cs3Map: return "cs3map"
- case .cs3Lesson(let id): return "cs3unit\(id)"
- case .cs4Map: return "cs4map"
- case .cs4Lesson(let id): return "cs4unit\(id)"
- case .czechMap: return "czechmap"
- case .mluvnice: return "mluvnice"
- case .mluvEx(let n): return "mluve\(n)"
- case .readingList: return "readinglist"
- case .book(let id):
- if id == "1984" { return "cetba1984" }
- if id == "fuks" { return "cetbaFuks" }
- return "cetba_" + id.replacingOccurrences(of: "-", with: "_")
- case .bookQuiz(let id):
- if id == "1984" { return "cetba1984quiz" }
- if id == "fuks" { return "cetbaFuksQuiz" }
- return "cetba_" + id.replacingOccurrences(of: "-", with: "_") + "_quiz"
- case .bookPlot(let id):
- if id == "1984" { return "cetba1984dej" }
- if id == "fuks" { return "cetbaFuksDej" }
- return "cetba_" + id.replacingOccurrences(of: "-", with: "_") + "_dej"
- }
- }
-
- /// Stack of destinations on top of the practice root.
- var stack: [Route] {
- switch self {
- case .welcome, .subjects, .stats:
- return []
- case .deHome:
- return [.deHome]
- case .deMap(let year):
- return [.deHome, .deMap(year)]
- case .deLesson(let year, let id):
- return [.deHome, .deMap(year), .deLesson(year, id)]
- case .deEx(let year, let id):
- return [.deHome, .deMap(year), .deLesson(year, id), .deEx(year, id)]
- case .roadmap:
- return [.deHome, .roadmap]
- case .unitMap(let id):
- return [.deHome, .roadmap, .unitMap(id)]
- case .germanEx(let unit, let ex):
- return [.deHome, .roadmap, .unitMap(unit), .germanEx(unit, ex)]
- case .vocab(let unit):
- return [.deHome, .roadmap, .unitMap(unit), .vocab(unit)]
- case .netYears:
- return [.netYears]
- case .netMap:
- return [.netYears, .netMap]
- case .netLesson(let id):
- return [.netYears, .netMap, .netLesson(id)]
- case .netEx(let id):
- return [.netYears, .netMap, .netLesson(id), .netEx(id)]
- case .net2Map:
- return [.netYears, .net2Map]
- case .net2Lesson(let id):
- return [.netYears, .net2Map, .net2Lesson(id)]
- case .net2Ex(let id):
- return [.netYears, .net2Map, .net2Lesson(id), .net2Ex(id)]
- case .net3Map:
- return [.netYears, .net3Map]
- case .net3Lesson(let id):
- return [.netYears, .net3Map, .net3Lesson(id)]
- case .net3Ex(let id):
- return [.netYears, .net3Map, .net3Lesson(id), .net3Ex(id)]
- case .net4Map:
- return [.netYears, .net4Map]
- case .net4Lesson(let id):
- return [.netYears, .net4Map, .net4Lesson(id)]
- case .net4Ex(let id):
- return [.netYears, .net4Map, .net4Lesson(id), .net4Ex(id)]
- case .hwYears:
- return [.hwYears]
- case .hwMap:
- return [.hwYears, .hwMap]
- case .hwLesson(let id):
- return [.hwYears, .hwMap, .hwLesson(id)]
- case .hwEx(let id):
- return [.hwYears, .hwMap, .hwLesson(id), .hwEx(id)]
- case .hw2Map:
- return [.hwYears, .hw2Map]
- case .hw2Lesson(let id):
- return [.hwYears, .hw2Map, .hw2Lesson(id)]
- case .hw2Ex(let id):
- return [.hwYears, .hw2Map, .hw2Lesson(id), .hw2Ex(id)]
- case .hw3Map:
- return [.hwYears, .hw3Map]
- case .hw3Lesson(let id):
- return [.hwYears, .hw3Map, .hw3Lesson(id)]
- case .hw3Ex(let id):
- return [.hwYears, .hw3Map, .hw3Lesson(id), .hw3Ex(id)]
- case .hw4Map:
- return [.hwYears, .hw4Map]
- case .hw4Lesson(let id):
- return [.hwYears, .hw4Map, .hw4Lesson(id)]
- case .hw4Ex(let id):
- return [.hwYears, .hw4Map, .hw4Lesson(id), .hw4Ex(id)]
- case .onYears:
- return [.onYears]
- case .onMap:
- return [.onYears, .onMap]
- case .onLesson(let id):
- return [.onYears, .onMap, .onLesson(id)]
- case .onEx(let id):
- return [.onYears, .onMap, .onLesson(id), .onEx(id)]
- case .on2Map:
- return [.onYears, .on2Map]
- case .on2Lesson(let id):
- return [.onYears, .on2Map, .on2Lesson(id)]
- case .on2Ex(let id):
- return [.onYears, .on2Map, .on2Lesson(id), .on2Ex(id)]
- case .on3Map:
- return [.onYears, .on3Map]
- case .on3Lesson(let id):
- return [.onYears, .on3Map, .on3Lesson(id)]
- case .on3Ex(let id):
- return [.onYears, .on3Map, .on3Lesson(id), .on3Ex(id)]
- case .on4Map:
- return [.onYears, .on4Map]
- case .on4Lesson(let id):
- return [.onYears, .on4Map, .on4Lesson(id)]
- case .on4Ex(let id):
- return [.onYears, .on4Map, .on4Lesson(id), .on4Ex(id)]
- case .enYears:
- return [.enYears]
- case .enMap(let year):
- return [.enYears, .enMap(year)]
- case .enLesson(let year, let id):
- return [.enYears, .enMap(year), .enLesson(year, id)]
- case .enEx(let year, let id):
- return [.enYears, .enMap(year), .enLesson(year, id), .enEx(year, id)]
- case .litYears:
- return [.czechMap, .litYears]
- case .litMap:
- return [.czechMap, .litYears, .litMap]
- case .litLesson(let id):
- return [.czechMap, .litYears, .litMap, .litLesson(id)]
- case .litEx(let id):
- return [.czechMap, .litYears, .litMap, .litLesson(id), .litEx(id)]
- case .lit2Map:
- return [.czechMap, .litYears, .lit2Map]
- case .lit2Lesson(let id):
- return [.czechMap, .litYears, .lit2Map, .lit2Lesson(id)]
- case .lit2Ex(let id):
- return [.czechMap, .litYears, .lit2Map, .lit2Lesson(id), .lit2Ex(id)]
- case .lit3Map:
- return [.czechMap, .litYears, .lit3Map]
- case .lit3Lesson(let id):
- return [.czechMap, .litYears, .lit3Map, .lit3Lesson(id)]
- case .lit3Ex(let id):
- return [.czechMap, .litYears, .lit3Map, .lit3Lesson(id), .lit3Ex(id)]
- case .lit4Map:
- return [.czechMap, .litYears, .lit4Map]
- case .lit4Lesson(let id):
- return [.czechMap, .litYears, .lit4Map, .lit4Lesson(id)]
- case .lit4Ex(let id):
- return [.czechMap, .litYears, .lit4Map, .lit4Lesson(id), .lit4Ex(id)]
- case .sciMap:
- return [.sciMap]
- case .chemMap:
- return [.sciMap, .chemMap]
- case .chemLesson(let id):
- return [.sciMap, .chemMap, .chemLesson(id)]
- case .chemEx(let id):
- return [.sciMap, .chemMap, .chemLesson(id), .chemEx(id)]
- case .bioMap:
- return [.sciMap, .bioMap]
- case .bioLesson(let id):
- return [.sciMap, .bioMap, .bioLesson(id)]
- case .bioEx(let id):
- return [.sciMap, .bioMap, .bioLesson(id), .bioEx(id)]
- case .matYears:
- return [.matYears]
- case .mat0Map:
- return [.matYears, .mat0Map]
- case .mat0Lesson(let id):
- return [.matYears, .mat0Map, .mat0Lesson(id)]
- case .mat0Ex(let id):
- return [.matYears, .mat0Map, .mat0Lesson(id), .mat0Ex(id)]
- case .matMap:
- return [.matYears, .matMap]
- case .matLesson(let id):
- return [.matYears, .matMap, .matLesson(id)]
- case .matEx(let id):
- return [.matYears, .matMap, .matLesson(id), .matEx(id)]
- case .mat2Map:
- return [.matYears, .mat2Map]
- case .mat2Lesson(let id):
- return [.matYears, .mat2Map, .mat2Lesson(id)]
- case .mat2Ex(let id):
- return [.matYears, .mat2Map, .mat2Lesson(id), .mat2Ex(id)]
- case .mat3Map:
- return [.matYears, .mat3Map]
- case .mat3Lesson(let id):
- return [.matYears, .mat3Map, .mat3Lesson(id)]
- case .mat3Ex(let id):
- return [.matYears, .mat3Map, .mat3Lesson(id), .mat3Ex(id)]
- case .mat4Map:
- return [.matYears, .mat4Map]
- case .mat4Lesson(let id):
- return [.matYears, .mat4Map, .mat4Lesson(id)]
- case .mat4Ex(let id):
- return [.matYears, .mat4Map, .mat4Lesson(id), .mat4Ex(id)]
- case .fyzYears:
- return [.fyzYears]
- case .fyzMap:
- return [.fyzYears, .fyzMap]
- case .fyzLesson(let id):
- return [.fyzYears, .fyzMap, .fyzLesson(id)]
- case .fyzEx(let id):
- return [.fyzYears, .fyzMap, .fyzLesson(id), .fyzEx(id)]
- case .fyz2Map:
- return [.fyzYears, .fyz2Map]
- case .fyz2Lesson(let id):
- return [.fyzYears, .fyz2Map, .fyz2Lesson(id)]
- case .fyz2Ex(let id):
- return [.fyzYears, .fyz2Map, .fyz2Lesson(id), .fyz2Ex(id)]
- case .fyz3Map:
- return [.fyzYears, .fyz3Map]
- case .fyz3Lesson(let id):
- return [.fyzYears, .fyz3Map, .fyz3Lesson(id)]
- case .fyz3Ex(let id):
- return [.fyzYears, .fyz3Map, .fyz3Lesson(id), .fyz3Ex(id)]
- case .fyz4Map:
- return [.fyzYears, .fyz4Map]
- case .fyz4Lesson(let id):
- return [.fyzYears, .fyz4Map, .fyz4Lesson(id)]
- case .fyz4Ex(let id):
- return [.fyzYears, .fyz4Map, .fyz4Lesson(id), .fyz4Ex(id)]
- case .tgYears:
- return [.tgYears]
- case .tgMap:
- return [.tgYears, .tgMap]
- case .tgLesson(let id):
- return [.tgYears, .tgMap, .tgLesson(id)]
- case .tgEx(let id):
- return [.tgYears, .tgMap, .tgLesson(id), .tgEx(id)]
- case .tg2Map:
- return [.tgYears, .tg2Map]
- case .tg2Lesson(let id):
- return [.tgYears, .tg2Map, .tg2Lesson(id)]
- case .tg2Ex(let id):
- return [.tgYears, .tg2Map, .tg2Lesson(id), .tg2Ex(id)]
- case .tg3Map:
- return [.tgYears, .tg3Map]
- case .tg3Lesson(let id):
- return [.tgYears, .tg3Map, .tg3Lesson(id)]
- case .tg3Ex(let id):
- return [.tgYears, .tg3Map, .tg3Lesson(id), .tg3Ex(id)]
- case .tg4Map:
- return [.tgYears, .tg4Map]
- case .tg4Lesson(let id):
- return [.tgYears, .tg4Map, .tg4Lesson(id)]
- case .tg4Ex(let id):
- return [.tgYears, .tg4Map, .tg4Lesson(id), .tg4Ex(id)]
- case .pgYears:
- return [.pgYears]
- case .pgMap:
- return [.pgYears, .pgMap]
- case .pgLesson(let id):
- return [.pgYears, .pgMap, .pgLesson(id)]
- case .pgEx(let id):
- return [.pgYears, .pgMap, .pgLesson(id), .pgEx(id)]
- case .pg2Map:
- return [.pgYears, .pg2Map]
- case .pg2Lesson(let id):
- return [.pgYears, .pg2Map, .pg2Lesson(id)]
- case .pg2Ex(let id):
- return [.pgYears, .pg2Map, .pg2Lesson(id), .pg2Ex(id)]
- case .pg3Map:
- return [.pgYears, .pg3Map]
- case .pg3Lesson(let id):
- return [.pgYears, .pg3Map, .pg3Lesson(id)]
- case .pg3Ex(let id):
- return [.pgYears, .pg3Map, .pg3Lesson(id), .pg3Ex(id)]
- case .pg4Map:
- return [.pgYears, .pg4Map]
- case .pg4Lesson(let id):
- return [.pgYears, .pg4Map, .pg4Lesson(id)]
- case .pg4Ex(let id):
- return [.pgYears, .pg4Map, .pg4Lesson(id), .pg4Ex(id)]
- case .swYears:
- return [.swYears]
- case .swMap:
- return [.swYears, .swMap]
- case .swLesson(let id):
- return [.swYears, .swMap, .swLesson(id)]
- case .swEx(let id):
- return [.swYears, .swMap, .swLesson(id), .swEx(id)]
- case .sw2Map:
- return [.swYears, .sw2Map]
- case .sw2Lesson(let id):
- return [.swYears, .sw2Map, .sw2Lesson(id)]
- case .sw2Ex(let id):
- return [.swYears, .sw2Map, .sw2Lesson(id), .sw2Ex(id)]
- case .sw3Map:
- return [.swYears, .sw3Map]
- case .sw3Lesson(let id):
- return [.swYears, .sw3Map, .sw3Lesson(id)]
- case .sw3Ex(let id):
- return [.swYears, .sw3Map, .sw3Lesson(id), .sw3Ex(id)]
- case .sw4Map:
- return [.swYears, .sw4Map]
- case .sw4Lesson(let id):
- return [.swYears, .sw4Map, .sw4Lesson(id)]
- case .sw4Ex(let id):
- return [.swYears, .sw4Map, .sw4Lesson(id), .sw4Ex(id)]
- case .csYears:
- return [.csYears]
- case .csMap:
- return [.csYears, .csMap]
- case .csLesson(let id):
- return [.csYears, .csMap, .csLesson(id)]
- case .cs2Map:
- return [.csYears, .cs2Map]
- case .cs2Lesson(let id):
- return [.csYears, .cs2Map, .cs2Lesson(id)]
- case .cs3Map:
- return [.csYears, .cs3Map]
- case .cs3Lesson(let id):
- return [.csYears, .cs3Map, .cs3Lesson(id)]
- case .cs4Map:
- return [.csYears, .cs4Map]
- case .cs4Lesson(let id):
- return [.csYears, .cs4Map, .cs4Lesson(id)]
- case .czechMap:
- return [.czechMap]
- case .mluvnice:
- return [.czechMap, .mluvnice]
- case .mluvEx(let n):
- return [.czechMap, .mluvnice, .mluvEx(n)]
- case .readingList:
- return [.czechMap, .readingList]
- case .book(let id):
- return [.czechMap, .readingList, .book(id)]
- case .bookQuiz(let id):
- return [.czechMap, .readingList, .book(id), .bookQuiz(id)]
- case .bookPlot(let id):
- return [.czechMap, .readingList, .book(id), .bookPlot(id)]
- }
- }
-}
-
-func routeFromPage(_ page: String) -> Route? {
- switch page {
- case "welcome": return .welcome
- case "subjects": return .subjects
- case "stats": return .stats
- case "roadmap": return .roadmap
- case "dehome": return .deHome
- case "netyears": return .netYears
- case "netmap": return .netMap
- case "net2map": return .net2Map
- case "net3map": return .net3Map
- case "net4map": return .net4Map
- case "hw2map": return .hw2Map
- case "hw3map": return .hw3Map
- case "hw4map": return .hw4Map
- case "hwyears": return .hwYears
- case "hwmap": return .hwMap
- case "onyears": return .onYears
- case "onmap": return .onMap
- case "on2map": return .on2Map
- case "on3map": return .on3Map
- case "on4map": return .on4Map
- case "enyears": return .enYears
- case "lityears": return .litYears
- case "litmap": return .litMap
- case "lit2map": return .lit2Map
- case "lit3map": return .lit3Map
- case "lit4map": return .lit4Map
- case "scimap": return .sciMap
- case "chemmap": return .chemMap
- case "biomap": return .bioMap
- case "matyears": return .matYears
- case "mat0map": return .mat0Map
- case "matmap": return .matMap
- case "mat2map": return .mat2Map
- case "mat3map": return .mat3Map
- case "mat4map": return .mat4Map
- case "fyzyears": return .fyzYears
- case "fyzmap": return .fyzMap
- case "fyz2map": return .fyz2Map
- case "fyz3map": return .fyz3Map
- case "fyz4map": return .fyz4Map
- case "tgyears": return .tgYears
- case "tgmap": return .tgMap
- case "tg2map": return .tg2Map
- case "tg3map": return .tg3Map
- case "tg4map": return .tg4Map
- case "pgyears": return .pgYears
- case "pgmap": return .pgMap
- case "pg2map": return .pg2Map
- case "pg3map": return .pg3Map
- case "pg4map": return .pg4Map
- case "swyears": return .swYears
- case "swmap": return .swMap
- case "sw2map": return .sw2Map
- case "sw3map": return .sw3Map
- case "sw4map": return .sw4Map
- case "csyears": return .csYears
- case "csmap": return .csMap
- case "cs2map": return .cs2Map
- case "cs3map": return .cs3Map
- case "cs4map": return .cs4Map
- case "czechmap": return .czechMap
- case "mluvnice": return .mluvnice
- case "readinglist": return .readingList
- case "cetba1984": return .book("1984")
- case "cetba1984quiz": return .bookQuiz("1984")
- case "cetba1984dej": return .bookPlot("1984")
- case "cetbaFuks": return .book("fuks")
- case "cetbaFuksQuiz": return .bookQuiz("fuks")
- case "cetbaFuksDej": return .bookPlot("fuks")
- default:
- if page.hasPrefix("cetba_"), page.hasSuffix("_quiz") {
- let slug = String(page.dropFirst(6).dropLast(5)).replacingOccurrences(of: "_", with: "-")
- return .bookQuiz(slug)
- }
- if page.hasPrefix("cetba_"), page.hasSuffix("_dej") {
- let slug = String(page.dropFirst(6).dropLast(4)).replacingOccurrences(of: "_", with: "-")
- return .bookPlot(slug)
- }
- if page.hasPrefix("cetba_") {
- let slug = String(page.dropFirst(6)).replacingOccurrences(of: "_", with: "-")
- return .book(slug)
- }
- if let m = page.wholeMatch(of: /unit(\d+)/), let n = Int(m.1) {
- return .unitMap(n - 1)
- }
- if let m = page.wholeMatch(of: /u(\d+)e(\d+)/), let u = Int(m.1), let e = Int(m.2) {
- return .germanEx(u - 1, e)
- }
- if let m = page.wholeMatch(of: /u(\d+)vocab/), let u = Int(m.1) {
- return .vocab(u - 1)
- }
- if let m = page.wholeMatch(of: /netunit(\d+)/), let n = Int(m.1) {
- return .netLesson(n)
- }
- if let m = page.wholeMatch(of: /netex(\d+)/), let n = Int(m.1) {
- return .netEx(n)
- }
- if let m = page.wholeMatch(of: /hwunit(\d+)/), let n = Int(m.1) {
- return .hwLesson(n)
- }
- if let m = page.wholeMatch(of: /hwex(\d+)/), let n = Int(m.1) {
- return .hwEx(n)
- }
- if let m = page.wholeMatch(of: /chemunit(\d+)/), let n = Int(m.1) {
- return .chemLesson(n)
- }
- if let m = page.wholeMatch(of: /chemex(\d+)/), let n = Int(m.1) {
- return .chemEx(n)
- }
- if let m = page.wholeMatch(of: /biounit(\d+)/), let n = Int(m.1) {
- return .bioLesson(n)
- }
- if let m = page.wholeMatch(of: /bioex(\d+)/), let n = Int(m.1) {
- return .bioEx(n)
- }
- if let m = page.wholeMatch(of: /mat0unit(\d+)/), let n = Int(m.1) {
- return .mat0Lesson(n)
- }
- if let m = page.wholeMatch(of: /mat0ex(\d+)/), let n = Int(m.1) {
- return .mat0Ex(n)
- }
- if let m = page.wholeMatch(of: /mat4unit(\d+)/), let n = Int(m.1) {
- return .mat4Lesson(n)
- }
- if let m = page.wholeMatch(of: /mat4ex(\d+)/), let n = Int(m.1) {
- return .mat4Ex(n)
- }
- if let m = page.wholeMatch(of: /mat3unit(\d+)/), let n = Int(m.1) {
- return .mat3Lesson(n)
- }
- if let m = page.wholeMatch(of: /mat3ex(\d+)/), let n = Int(m.1) {
- return .mat3Ex(n)
- }
- if let m = page.wholeMatch(of: /mat2unit(\d+)/), let n = Int(m.1) {
- return .mat2Lesson(n)
- }
- if let m = page.wholeMatch(of: /mat2ex(\d+)/), let n = Int(m.1) {
- return .mat2Ex(n)
- }
- if let m = page.wholeMatch(of: /matunit(\d+)/), let n = Int(m.1) {
- return .matLesson(n)
- }
- if let m = page.wholeMatch(of: /matex(\d+)/), let n = Int(m.1) {
- return .matEx(n)
- }
- if let m = page.wholeMatch(of: /fyz4unit(\d+)/), let n = Int(m.1) {
- return .fyz4Lesson(n)
- }
- if let m = page.wholeMatch(of: /fyz4ex(\d+)/), let n = Int(m.1) {
- return .fyz4Ex(n)
- }
- if let m = page.wholeMatch(of: /fyz3unit(\d+)/), let n = Int(m.1) {
- return .fyz3Lesson(n)
- }
- if let m = page.wholeMatch(of: /fyz3ex(\d+)/), let n = Int(m.1) {
- return .fyz3Ex(n)
- }
- if let m = page.wholeMatch(of: /fyz2unit(\d+)/), let n = Int(m.1) {
- return .fyz2Lesson(n)
- }
- if let m = page.wholeMatch(of: /fyz2ex(\d+)/), let n = Int(m.1) {
- return .fyz2Ex(n)
- }
- if let m = page.wholeMatch(of: /fyzunit(\d+)/), let n = Int(m.1) {
- return .fyzLesson(n)
- }
- if let m = page.wholeMatch(of: /fyzex(\d+)/), let n = Int(m.1) {
- return .fyzEx(n)
- }
- if let m = page.wholeMatch(of: /sw4unit(\d+)/), let n = Int(m.1) {
- return .sw4Lesson(n)
- }
- if let m = page.wholeMatch(of: /sw4ex(\d+)/), let n = Int(m.1) {
- return .sw4Ex(n)
- }
- if let m = page.wholeMatch(of: /sw3unit(\d+)/), let n = Int(m.1) {
- return .sw3Lesson(n)
- }
- if let m = page.wholeMatch(of: /sw3ex(\d+)/), let n = Int(m.1) {
- return .sw3Ex(n)
- }
- if let m = page.wholeMatch(of: /sw2unit(\d+)/), let n = Int(m.1) {
- return .sw2Lesson(n)
- }
- if let m = page.wholeMatch(of: /sw2ex(\d+)/), let n = Int(m.1) {
- return .sw2Ex(n)
- }
- if let m = page.wholeMatch(of: /swunit(\d+)/), let n = Int(m.1) {
- return .swLesson(n)
- }
- if let m = page.wholeMatch(of: /swex(\d+)/), let n = Int(m.1) {
- return .swEx(n)
- }
- if let m = page.wholeMatch(of: /pg4unit(\d+)/), let n = Int(m.1) {
- return .pg4Lesson(n)
- }
- if let m = page.wholeMatch(of: /pg4ex(\d+)/), let n = Int(m.1) {
- return .pg4Ex(n)
- }
- if let m = page.wholeMatch(of: /pg3unit(\d+)/), let n = Int(m.1) {
- return .pg3Lesson(n)
- }
- if let m = page.wholeMatch(of: /pg3ex(\d+)/), let n = Int(m.1) {
- return .pg3Ex(n)
- }
- if let m = page.wholeMatch(of: /pg2unit(\d+)/), let n = Int(m.1) {
- return .pg2Lesson(n)
- }
- if let m = page.wholeMatch(of: /pg2ex(\d+)/), let n = Int(m.1) {
- return .pg2Ex(n)
- }
- if let m = page.wholeMatch(of: /pgunit(\d+)/), let n = Int(m.1) {
- return .pgLesson(n)
- }
- if let m = page.wholeMatch(of: /pgex(\d+)/), let n = Int(m.1) {
- return .pgEx(n)
- }
- if let m = page.wholeMatch(of: /tg4unit(\d+)/), let n = Int(m.1) {
- return .tg4Lesson(n)
- }
- if let m = page.wholeMatch(of: /tg4ex(\d+)/), let n = Int(m.1) {
- return .tg4Ex(n)
- }
- if let m = page.wholeMatch(of: /tg3unit(\d+)/), let n = Int(m.1) {
- return .tg3Lesson(n)
- }
- if let m = page.wholeMatch(of: /tg3ex(\d+)/), let n = Int(m.1) {
- return .tg3Ex(n)
- }
- if let m = page.wholeMatch(of: /tg2unit(\d+)/), let n = Int(m.1) {
- return .tg2Lesson(n)
- }
- if let m = page.wholeMatch(of: /tg2ex(\d+)/), let n = Int(m.1) {
- return .tg2Ex(n)
- }
- if let m = page.wholeMatch(of: /tgunit(\d+)/), let n = Int(m.1) {
- return .tgLesson(n)
- }
- if let m = page.wholeMatch(of: /tgex(\d+)/), let n = Int(m.1) {
- return .tgEx(n)
- }
- if let m = page.wholeMatch(of: /cs4unit(\d+)/), let n = Int(m.1) {
- return .cs4Lesson(n)
- }
- if let m = page.wholeMatch(of: /cs3unit(\d+)/), let n = Int(m.1) {
- return .cs3Lesson(n)
- }
- if let m = page.wholeMatch(of: /cs2unit(\d+)/), let n = Int(m.1) {
- return .cs2Lesson(n)
- }
- if let m = page.wholeMatch(of: /csunit(\d+)/), let n = Int(m.1) {
- return .csLesson(n)
- }
- if let m = page.wholeMatch(of: /lit4unit(\d+)/), let n = Int(m.1) {
- return .lit4Lesson(n)
- }
- if let m = page.wholeMatch(of: /lit4ex(\d+)/), let n = Int(m.1) {
- return .lit4Ex(n)
- }
- if let m = page.wholeMatch(of: /lit3unit(\d+)/), let n = Int(m.1) {
- return .lit3Lesson(n)
- }
- if let m = page.wholeMatch(of: /lit3ex(\d+)/), let n = Int(m.1) {
- return .lit3Ex(n)
- }
- if let m = page.wholeMatch(of: /lit2unit(\d+)/), let n = Int(m.1) {
- return .lit2Lesson(n)
- }
- if let m = page.wholeMatch(of: /lit2ex(\d+)/), let n = Int(m.1) {
- return .lit2Ex(n)
- }
- if let m = page.wholeMatch(of: /litunit(\d+)/), let n = Int(m.1) {
- return .litLesson(n)
- }
- if let m = page.wholeMatch(of: /litex(\d+)/), let n = Int(m.1) {
- return .litEx(n)
- }
- if let m = page.wholeMatch(of: /de(\d)map/), let y = Int(m.1) {
- return .deMap(y)
- }
- if let m = page.wholeMatch(of: /de(\d)unit(\d+)/), let y = Int(m.1), let n = Int(m.2) {
- return .deLesson(y, n)
- }
- if let m = page.wholeMatch(of: /de(\d)ex(\d+)/), let y = Int(m.1), let n = Int(m.2) {
- return .deEx(y, n)
- }
- if let m = page.wholeMatch(of: /net([234])unit(\d+)/), let y = Int(m.1), let n = Int(m.2) {
- switch y {
- case 3: return .net3Lesson(n)
- case 4: return .net4Lesson(n)
- default: return .net2Lesson(n)
- }
- }
- if let m = page.wholeMatch(of: /net([234])ex(\d+)/), let y = Int(m.1), let n = Int(m.2) {
- switch y {
- case 3: return .net3Ex(n)
- case 4: return .net4Ex(n)
- default: return .net2Ex(n)
- }
- }
- if let m = page.wholeMatch(of: /hw([234])unit(\d+)/), let y = Int(m.1), let n = Int(m.2) {
- switch y {
- case 3: return .hw3Lesson(n)
- case 4: return .hw4Lesson(n)
- default: return .hw2Lesson(n)
- }
- }
- if let m = page.wholeMatch(of: /hw([234])ex(\d+)/), let y = Int(m.1), let n = Int(m.2) {
- switch y {
- case 3: return .hw3Ex(n)
- case 4: return .hw4Ex(n)
- default: return .hw2Ex(n)
- }
- }
- if let m = page.wholeMatch(of: /en(\d)map/), let y = Int(m.1) {
- return .enMap(y)
- }
- if let m = page.wholeMatch(of: /en(\d)unit(\d+)/), let y = Int(m.1), let n = Int(m.2) {
- return .enLesson(y, n)
- }
- if let m = page.wholeMatch(of: /en(\d)ex(\d+)/), let y = Int(m.1), let n = Int(m.2) {
- return .enEx(y, n)
- }
- if let m = page.wholeMatch(of: /on4unit(\d+)/), let n = Int(m.1) {
- return .on4Lesson(n)
- }
- if let m = page.wholeMatch(of: /on4ex(\d+)/), let n = Int(m.1) {
- return .on4Ex(n)
- }
- if let m = page.wholeMatch(of: /on3unit(\d+)/), let n = Int(m.1) {
- return .on3Lesson(n)
- }
- if let m = page.wholeMatch(of: /on3ex(\d+)/), let n = Int(m.1) {
- return .on3Ex(n)
- }
- if let m = page.wholeMatch(of: /on2unit(\d+)/), let n = Int(m.1) {
- return .on2Lesson(n)
- }
- if let m = page.wholeMatch(of: /on2ex(\d+)/), let n = Int(m.1) {
- return .on2Ex(n)
- }
- if let m = page.wholeMatch(of: /onunit(\d+)/), let n = Int(m.1) {
- return .onLesson(n)
- }
- if let m = page.wholeMatch(of: /onex(\d+)/), let n = Int(m.1) {
- return .onEx(n)
- }
- if let m = page.wholeMatch(of: /mluve(\d+)/), let n = Int(m.1) {
- return .mluvEx(n)
- }
- return nil
- }
-}
diff --git a/ios/Graduately/UI/Components.swift b/ios/Graduately/UI/Components.swift
deleted file mode 100644
index e0aaf2a..0000000
--- a/ios/Graduately/UI/Components.swift
+++ /dev/null
@@ -1,741 +0,0 @@
-import SwiftUI
-
-enum Motion {
- static let press = Animation.easeOut(duration: 0.16)
- static let settle = Animation.easeOut(duration: 0.28)
- static let feedback = Animation.spring(response: 0.32, dampingFraction: 0.82)
-}
-
-/// Slight press-in for custom cards and map nodes. System buttons keep their own style.
-struct PressScaleStyle: ButtonStyle {
- @Environment(\.accessibilityReduceMotion) private var reduceMotion
-
- func makeBody(configuration: Configuration) -> some View {
- configuration.label
- .scaleEffect(reduceMotion || !configuration.isPressed ? 1 : 0.98)
- .animation(reduceMotion ? nil : Motion.press, value: configuration.isPressed)
- }
-}
-
-/// Named so it does not collide with the app's `Content` type, which would
-/// otherwise hide `ViewModifier.Content` and fail the build.
-private struct SoftAppear: View {
- @Environment(\.accessibilityReduceMotion) private var reduceMotion
- @State private var shown = false
- var inner: Inner
-
- var body: some View {
- inner
- .opacity(reduceMotion || shown ? 1 : 0)
- .offset(y: reduceMotion || shown ? 0 : 8)
- .onAppear {
- guard !reduceMotion else {
- shown = true
- return
- }
- withAnimation(Motion.settle) { shown = true }
- }
- }
-}
-
-extension View {
- /// One short fade-in. Use on a landing surface, not on every pushed screen.
- func softAppear() -> some View {
- SoftAppear(inner: self)
- }
-}
-
-#if os(macOS)
-enum MacChrome {
- static let pageInset: CGFloat = 28
- static let cardPadding: CGFloat = 22
- static let listRowMinHeight: CGFloat = 52
- static let homeTileMin: CGFloat = 260
- static let homeTileHeight: CGFloat = 172
- static let pathNode: CGFloat = 84
- /// Pages stop growing here so cards and text keep their shape on wide displays.
- static let contentWidth: CGFloat = 1240
- static let readableWidth: CGFloat = 860
- static let actionWidth: CGFloat = 520
-}
-
-/// Fixed-height cards in as many columns as fit, centred and capped in width.
-struct MacFillGrid: View {
- var count: Int
- var minWidth: CGFloat = 240
- var minHeight: CGFloat = 148
- var spacing: CGFloat = 16
- var inset: CGFloat = 28
- var maxTileWidth: CGFloat = 340
- @ViewBuilder var content: (Int) -> Content
-
- var body: some View {
- GeometryReader { geo in
- let available = max(geo.size.width - inset * 2, minWidth)
- let fit = max(1, Int((available + spacing) / (minWidth + spacing)))
- let cols = max(1, min(count, fit))
- let width = min(available, CGFloat(cols) * maxTileWidth + CGFloat(cols - 1) * spacing)
- ScrollView {
- LazyVGrid(
- columns: Array(repeating: GridItem(.flexible(), spacing: spacing, alignment: .top), count: cols),
- alignment: .leading,
- spacing: spacing
- ) {
- ForEach(0.. 0 {
- HStack(spacing: 10) {
- ProgressBar(value: Double(progress.done) / Double(progress.total))
- Text("\(progress.done) / \(progress.total)")
- .font(.subheadline.monospacedDigit())
- .foregroundStyle(.secondary)
- }
- }
- }
- .padding(20)
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
- .softSurface(cornerRadius: 20)
- .contentShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
- }
-}
-#endif
-
-/// Flat capsule bar. Unlike ProgressView it shows nothing at zero.
-struct ProgressBar: View {
- let value: Double
- var height: CGFloat = 8
-
- var body: some View {
- GeometryReader { geo in
- ZStack(alignment: .leading) {
- Capsule().fill(Color.primary.opacity(0.08))
- if value > 0 {
- Capsule()
- .fill(Color.accentColor)
- .frame(width: max(height, geo.size.width * min(value, 1)))
- }
- }
- }
- .frame(height: height)
- .animation(Motion.settle, value: value)
- }
-}
-
-struct ProgressRing: View {
- let value: Double
- var lineWidth: CGFloat = 12
- @ViewBuilder var label: () -> Label
-
- var body: some View {
- ZStack {
- Circle().stroke(Color.primary.opacity(0.08), lineWidth: lineWidth)
- if value > 0 {
- Circle()
- .trim(from: 0, to: min(value, 1))
- .stroke(Color.accentColor, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
- .rotationEffect(.degrees(-90))
- }
- label()
- }
- .animation(Motion.settle, value: value)
- }
-}
-
-struct FlowLayout: Layout {
- var spacing: CGFloat = 6
-
- func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
- arrange(width: proposal.width ?? .infinity, subviews: subviews).size
- }
-
- func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
- let result = arrange(width: bounds.width, subviews: subviews)
- for (i, frame) in result.frames.enumerated() {
- subviews[i].place(
- at: CGPoint(x: bounds.minX + frame.minX, y: bounds.minY + frame.minY),
- proposal: ProposedViewSize(frame.size)
- )
- }
- }
-
- private func arrange(width: CGFloat, subviews: Subviews) -> (size: CGSize, frames: [CGRect]) {
- var frames: [CGRect] = []
- var x: CGFloat = 0
- var y: CGFloat = 0
- var rowH: CGFloat = 0
- var maxW: CGFloat = 0
- for view in subviews {
- let size = view.sizeThatFits(.unspecified)
- if x > 0 && x + size.width > width {
- x = 0
- y += rowH + spacing
- rowH = 0
- }
- frames.append(CGRect(origin: CGPoint(x: x, y: y), size: size))
- x += size.width + spacing
- rowH = max(rowH, size.height)
- maxW = max(maxW, x - spacing)
- }
- return (CGSize(width: max(maxW, 0), height: y + rowH), frames)
- }
-}
-
-struct PrimaryButton: View {
- let label: String
- var action: () -> Void
-
- var body: some View {
- Button(action: action) {
- Text(label)
- .font(.body.weight(.semibold))
- .frame(maxWidth: .infinity)
- #if os(macOS)
- .padding(.vertical, 4)
- #endif
- }
- #if os(macOS)
- .buttonStyle(.borderedProminent)
- .controlSize(.extraLarge)
- #else
- .buttonStyle(.glassProminent)
- .controlSize(.large)
- #endif
- }
-}
-
-struct PillButton: View {
- let label: String
- var action: () -> Void
-
- var body: some View {
- Button(label, action: action)
- .buttonStyle(.bordered)
- #if os(macOS)
- .controlSize(.large)
- #endif
- }
-}
-
-struct GlassCard: View {
- @ViewBuilder var content: () -> Content
-
- var body: some View {
- content()
- #if os(macOS)
- .padding(MacChrome.cardPadding)
- #else
- .padding(16)
- #endif
- .frame(maxWidth: .infinity, alignment: .leading)
- .softSurface(cornerRadius: 22)
- }
-}
-
-extension View {
- /// Soft card surface without Liquid Glass refraction (which reads as a mirror
- /// on large macOS panels over the wallpaper / sidebar extension).
- func softSurface(cornerRadius: CGFloat) -> some View {
- background {
- RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
- .fill(.background.secondary)
- }
- .overlay {
- RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
- .strokeBorder(.separator.opacity(0.35), lineWidth: 0.5)
- }
- }
-}
-
-struct FeedbackLine: View {
- let text: String
- let kind: String
- let palette: Palette
- @Environment(\.accessibilityReduceMotion) private var reduceMotion
-
- var body: some View {
- Group {
- if !text.isEmpty {
- Label {
- Text(text)
- } icon: {
- Image(systemName: kind == "ok" ? "checkmark.circle.fill" : kind == "warn" ? "exclamationmark.triangle.fill" : "xmark.circle.fill")
- }
- #if os(macOS)
- .font(.body.weight(.semibold))
- #else
- .font(.subheadline.weight(.semibold))
- #endif
- .foregroundStyle(kind == "ok" ? palette.success : kind == "warn" ? palette.warning : palette.error)
- .transition(.opacity.combined(with: .scale(scale: 0.96, anchor: .leading)))
- }
- }
- .animation(reduceMotion ? nil : Motion.feedback, value: text)
- .animation(reduceMotion ? nil : Motion.feedback, value: kind)
- }
-}
-
-struct Meaning: View {
- let text: String
- let palette: Palette
- let visible: Bool
-
- @Environment(\.accessibilityReduceMotion) private var reduceMotion
-
- var body: some View {
- Group {
- if visible && !text.isEmpty {
- Text(text)
- #if os(macOS)
- .font(.body)
- #else
- .font(.footnote)
- #endif
- .foregroundStyle(.secondary)
- .padding(.bottom, 4)
- .transition(.opacity)
- }
- }
- .animation(reduceMotion ? nil : Motion.settle, value: visible)
- }
-}
-
-struct WordField: View {
- @Binding var value: String
- var placeholder: String
- var mark: Bool?
- var palette: Palette?
- @Environment(\.accessibilityReduceMotion) private var reduceMotion
-
- var body: some View {
- HStack(spacing: 8) {
- TextField(placeholder, text: $value)
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
- #if os(macOS)
- .font(.title3)
- #endif
- if let mark {
- Image(systemName: mark ? "checkmark.circle.fill" : "xmark.circle.fill")
- .foregroundStyle(mark ? (palette?.success ?? .green) : (palette?.error ?? .red))
- }
- }
- .padding(.horizontal, 12)
- #if os(macOS)
- .padding(.vertical, 14)
- #else
- .padding(.vertical, 10)
- #endif
- .background(Color(.secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
- .overlay(
- RoundedRectangle(cornerRadius: 12, style: .continuous)
- .stroke(border, lineWidth: mark == nil ? 0.5 : 1.5)
- )
- .animation(reduceMotion ? nil : Motion.settle, value: mark)
- }
-
- private var border: Color {
- if mark == true { return palette?.success ?? .green }
- if mark == false { return palette?.error ?? .red }
- return Color(.separator)
- }
-}
-
-struct SegChip: View {
- let label: String
- let selected: Bool
- var action: () -> Void
-
- var body: some View {
- Button(label, action: action)
- .buttonStyle(.bordered)
- .tint(selected ? .accentColor : .secondary)
- #if os(macOS)
- .controlSize(.large)
- #endif
- }
-}
-
-struct Hint: View {
- let text: String
- var body: some View {
- Text(text)
- #if os(macOS)
- .font(.body)
- #else
- .font(.footnote)
- #endif
- .foregroundStyle(.secondary)
- .padding(.bottom, 6)
- }
-}
-
-struct Prompt: View {
- let text: String
- var body: some View {
- Text(text)
- #if os(macOS)
- .font(.title3.weight(.medium))
- #else
- .font(.body.weight(.medium))
- #endif
- }
-}
-
-struct WordChip: View {
- let text: String
- var action: () -> Void
-
- var body: some View {
- Button(text, action: action)
- .buttonStyle(.bordered)
- .tint(.primary)
- #if os(macOS)
- .controlSize(.large)
- #else
- .controlSize(.small)
- #endif
- }
-}
-
-/// Drop zone for word ordering: visible even when empty.
-struct WordSlot: View {
- let empty: Bool
- @ViewBuilder var content: () -> Content
-
- var body: some View {
- content()
- #if os(macOS)
- .frame(maxWidth: .infinity, minHeight: 56, alignment: .leading)
- .padding(12)
- #else
- .frame(maxWidth: .infinity, minHeight: 44, alignment: .leading)
- .padding(8)
- #endif
- .background(Color(.tertiarySystemFill), in: RoundedRectangle(cornerRadius: 14, style: .continuous))
- .overlay(
- RoundedRectangle(cornerRadius: 14, style: .continuous)
- .strokeBorder(style: StrokeStyle(lineWidth: empty ? 1 : 0, dash: [5, 4]))
- .foregroundStyle(.tertiary)
- )
- }
-}
-
-struct OptionChip: View {
- let text: String
- let selected: Bool
- let mark: Bool?
- var action: () -> Void
- @Environment(\.accessibilityReduceMotion) private var reduceMotion
-
- var body: some View {
- Button(action: action) {
- HStack(spacing: 10) {
- Image(systemName: symbol)
- .font(.title3)
- .foregroundStyle(symbolColor)
- Text(text)
- #if os(macOS)
- .font(.title3)
- #endif
- .foregroundStyle(.primary)
- .multilineTextAlignment(.leading)
- .frame(maxWidth: .infinity, alignment: .leading)
- }
- #if os(macOS)
- .padding(.vertical, 8)
- #else
- .padding(.vertical, 4)
- #endif
- }
- .buttonStyle(.bordered)
- .tint(tint)
- #if os(macOS)
- .controlSize(.large)
- #endif
- .padding(.bottom, 4)
- .animation(reduceMotion ? nil : Motion.settle, value: mark)
- .animation(reduceMotion ? nil : Motion.press, value: selected)
- }
-
- private var symbol: String {
- if mark == true { return "checkmark.circle.fill" }
- if mark == false { return "xmark.circle.fill" }
- return selected ? "largecircle.fill.circle" : "circle"
- }
-
- private var symbolColor: Color {
- if mark == true { return .green }
- if mark == false { return .red }
- return selected ? .accentColor : .secondary
- }
-
- private var tint: Color {
- if mark == true { return .green }
- if mark == false { return .red }
- return selected ? .accentColor : .secondary
- }
-}
-
-extension View {
- @ViewBuilder
- func navSubtitle(_ text: String?) -> some View {
- if let text, !text.isEmpty {
- navigationSubtitle(text)
- } else {
- self
- }
- }
-
- /// Detail screens with their own bottom action hide the tab bar so the
- /// primary button is never covered by it.
- @ViewBuilder
- func detailScreen(_ title: String, subtitle: String? = nil) -> some View {
- #if os(iOS)
- navigationTitle(title)
- .navigationBarTitleDisplayMode(.inline)
- .navSubtitle(subtitle)
- .toolbar(.hidden, for: .tabBar)
- #else
- navigationTitle(title)
- .navSubtitle(subtitle)
- #endif
- }
-
- @ViewBuilder
- func sheetDetents() -> some View {
- #if os(iOS)
- presentationDetents([.medium, .large])
- .presentationDragIndicator(.visible)
- // Liquid Glass sheets refract the exercise chrome underneath into a
- // bright tinted band across the word list; keep the sheet opaque.
- .presentationBackground(.background)
- #else
- self
- #endif
- }
-
- /// Leading-aligned content for exercise screens; on the Mac a centred readable column.
- func exerciseContent() -> some View {
- #if os(macOS)
- frame(maxWidth: MacChrome.readableWidth, alignment: .leading)
- .padding(.horizontal, MacChrome.pageInset)
- .padding(.vertical, 28)
- .frame(maxWidth: .infinity)
- #else
- frame(maxWidth: .infinity, alignment: .leading)
- .padding()
- #endif
- }
-
- /// Caps a Mac page at [width] and centres it; no-op on iOS.
- @ViewBuilder
- func macPage(_ width: CGFloat? = nil) -> some View {
- #if os(macOS)
- frame(maxWidth: width ?? MacChrome.contentWidth)
- .frame(maxWidth: .infinity)
- #else
- self
- #endif
- }
-
- /// macOS content lists use the inset style so they sit under the floating
- /// glass sidebar instead of looking like a second source list.
- @ViewBuilder
- func appListStyle() -> some View {
- #if os(macOS)
- listStyle(.inset)
- .environment(\.defaultMinListRowHeight, MacChrome.listRowMinHeight)
- .scrollContentBackground(.hidden)
- .padding(.horizontal, 8)
- #else
- self
- #endif
- }
-
- @ViewBuilder
- func appFormStyle() -> some View {
- #if os(macOS)
- formStyle(.grouped)
- .scenePadding()
- #else
- self
- #endif
- }
-
- /// Pin a primary action above scrolling content using the system scroll
- /// edge effect instead of an opaque bar.
- func glassBottomBar(@ViewBuilder _ bar: () -> Bar) -> some View {
- safeAreaBar(edge: .bottom, spacing: 8, content: bar)
- }
-}
-
-/// Bottom action area used by exercises and lesson slides.
-struct BottomAction: View {
- let label: String
- var action: () -> Void
- @ViewBuilder var extra: () -> Extra
-
- var body: some View {
- VStack(spacing: 10) {
- extra()
- PrimaryButton(label: label, action: action)
- }
- .padding(.horizontal, {
- #if os(macOS)
- MacChrome.pageInset
- #else
- 16
- #endif
- }())
- #if os(macOS)
- .padding(.top, 12)
- .padding(.bottom, 14)
- .frame(maxWidth: MacChrome.actionWidth)
- .frame(maxWidth: .infinity)
- #else
- .padding(.top, 8)
- .padding(.bottom, 4)
- #endif
- }
-}
-
-/// Primary + secondary text sized for desktop lists, with optional exercise progress.
-struct ListRowLabel: View {
- let title: String
- var subtitle: String? = nil
- var progress: (done: Int, total: Int)? = nil
-
- var body: some View {
- VStack(alignment: .leading, spacing: 3) {
- Text(title)
- #if os(macOS)
- .font(.title3)
- #endif
- if let subtitle, !subtitle.isEmpty {
- Text(subtitle)
- #if os(macOS)
- .font(.body)
- #else
- .font(.caption)
- #endif
- .foregroundStyle(.secondary)
- }
- if let progress, progress.total > 0 {
- ProgressLine(done: progress.done, total: progress.total)
- .padding(.top, 4)
- }
- }
- }
-}
-
-/// Slim bar with "done / total" beside it, for list rows.
-struct ProgressLine: View {
- let done: Int
- let total: Int
-
- var body: some View {
- HStack(spacing: 8) {
- ProgressBar(value: total == 0 ? 0 : Double(done) / Double(total), height: 5)
- Text("\(done) / \(total)")
- .font(.caption.monospacedDigit())
- .foregroundStyle(.secondary)
- }
- }
-}
-
-func subjectSymbol(_ icon: String) -> String {
- switch icon {
- case "de": return "character.book.closed.fill"
- case "wifi": return "wifi"
- case "chip": return "cpu"
- case "cz": return "text.book.closed.fill"
- case "people": return "person.2.fill"
- case "flask": return "flask.fill"
- case "bolt": return "bolt.fill"
- case "function": return "function"
- case "ruler": return "ruler.fill"
- case "code": return "chevron.left.forwardslash.chevron.right"
- case "brush": return "paintbrush.fill"
- case "apps": return "square.grid.2x2.fill"
- default: return "lock.fill"
- }
-}
-
-/// Subject glyph for list rows: real flags for the languages, SF Symbols otherwise.
-struct SubjectIcon: View {
- let icon: String
- var open = true
- var size: CGFloat = 26
-
- var body: some View {
- Group {
- switch icon {
- case "de": GermanFlag(size: size)
- case "cz": CzechFlag(size: size)
- case "uk": UkFlag(size: size)
- default:
- Image(systemName: subjectSymbol(icon))
- .font(.system(size: size * 0.72, weight: .semibold))
- .foregroundStyle(open ? AnyShapeStyle(Color.accentColor) : AnyShapeStyle(.tertiary))
- }
- }
- .frame(width: size, height: size)
- .opacity(open ? 1 : 0.6)
- }
-}
diff --git a/ios/Graduately/UI/Exercises.swift b/ios/Graduately/UI/Exercises.swift
deleted file mode 100644
index fb4ccea..0000000
--- a/ios/Graduately/UI/Exercises.swift
+++ /dev/null
@@ -1,668 +0,0 @@
-import SwiftUI
-
-struct ExerciseScaffold: View {
- @ObservedObject var vm: AppModel
- let title: String
- var sub: String?
- let action: String
- var onAction: () -> Void
- let feedback: String
- let kind: String
- @ViewBuilder var content: () -> Content
-
- var body: some View {
- let p = vm.palette
- ScrollView {
- VStack(alignment: .leading, spacing: 8) { content() }
- .exerciseContent()
- }
- .scrollDismissesKeyboard(.interactively)
- .detailScreen(title, subtitle: sub)
- .glassBottomBar {
- BottomAction(label: action, action: onAction) {
- if !feedback.isEmpty {
- FeedbackLine(text: feedback, kind: kind, palette: p)
- .frame(maxWidth: .infinity, alignment: .leading)
- .transition(.opacity.combined(with: .scale(scale: 0.96, anchor: .leading)))
- }
- }
- }
- .animation(Motion.feedback, value: feedback)
- }
-}
-
-struct GermanExercise: View {
- @ObservedObject var vm: AppModel
- let unitId: Int
- let ex: Int
-
- var body: some View {
- if let spec = vm.content.germanUnit(unitId)?.exercise(ex) {
- DispatchExercise(vm: vm, spec: spec, titleFallback: spec.str("title")) {
- vm.markGerman(unitId, ex)
- }
- }
- }
-}
-
-struct VocabExercise: View {
- @ObservedObject var vm: AppModel
- let unitId: Int
-
- var body: some View {
- if let spec = vm.content.germanUnit(unitId)?.obj("vocab") {
- DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr("Vokabeltraining")) {
- vm.markVocab(unitId)
- }
- }
- }
-}
-
-struct MluvExercise: View {
- @ObservedObject var vm: AppModel
- let n: Int
-
- var body: some View {
- if let spec = vm.content.mluv(n) {
- DispatchExercise(vm: vm, spec: spec, titleFallback: spec.str("title")) {
- vm.markMluv(n)
- }
- }
- }
-}
-
-struct DispatchExercise: View {
- @ObservedObject var vm: AppModel
- let spec: J
- let titleFallback: String
- var onDone: () -> Void
-
- var body: some View {
- let title = spec.str("title").isEmpty ? titleFallback : spec.str("title")
- let sub = spec.strOrNull("sub").map { vm.tr($0) }
- switch spec.str("type") {
- case "choice": ChoiceEx(vm: vm, title: title, sub: sub, spec: spec, onDone: onDone)
- case "assembly": AssemblyEx(vm: vm, title: title, sub: sub, spec: spec, onDone: onDone)
- case "typed", "kw": TypedEx(vm: vm, title: title, sub: sub, spec: spec, onDone: onDone)
- case "free", "profile": FreeEx(vm: vm, title: title, sub: sub, spec: spec, onDone: onDone)
- case "assign": AssignEx(vm: vm, title: title, sub: sub, spec: spec, onDone: onDone)
- case "hangman": HangmanEx(vm: vm, title: title, sub: sub, spec: spec, onDone: onDone)
- case "vocab": VocabEx(vm: vm, title: title, sub: sub, spec: spec, onDone: onDone)
- case "dialog": DialogEx(vm: vm, title: title, sub: sub, spec: spec, onDone: onDone)
- case "number", "count", "seq", "verb", "verbclue", "verb_sections", "ordne", "fill":
- ComboEx(vm: vm, title: title, sub: sub, spec: spec, onDone: onDone)
- case "ex2": Ex2Ex(vm: vm, title: title, sub: sub, spec: spec, onDone: onDone)
- case "letters", "letters_gap": LettersEx(vm: vm, title: title, sub: sub, spec: spec, onDone: onDone)
- case "table": TableEx(vm: vm, title: title, sub: sub, spec: spec, onDone: onDone)
- case "reveal": RevealEx(vm: vm, title: title, sub: sub, spec: spec, onDone: onDone)
- default: Text("?").foregroundStyle(.primary)
- }
- }
-}
-
-private struct ChoiceEx: View {
- @ObservedObject var vm: AppModel
- let title: String
- var sub: String?
- let spec: J
- var onDone: () -> Void
- @State private var picks: [Int]
- @State private var fb = ("", "")
- @State private var revealed = false
-
- init(vm: AppModel, title: String, sub: String?, spec: J, onDone: @escaping () -> Void) {
- self.vm = vm
- self.title = title
- self.sub = sub
- self.spec = spec
- self.onDone = onDone
- _picks = State(initialValue: Array(repeating: -1, count: spec.arr("questions").count))
- }
-
- var body: some View {
- let qs = spec.arr("questions")
- let meanings = spec.strs("meanings")
- let expls = spec.strs("expls")
- let p = vm.palette
- ExerciseScaffold(vm: vm, title: title, sub: sub, action: vm.tr("check"), onAction: {
- let ok = qs.indices.filter { picks[$0] == qs[$0].int("correct") }.count
- if ok == qs.count { fb = (vm.tr("feedback_ok"), "ok"); onDone() }
- else { fb = (vm.tr("feedback_retry_short"), "err") }
- revealed = true
- }, feedback: fb.0, kind: fb.1) {
- ForEach(qs.indices, id: \.self) { i in
- Text("\(i + 1). \(qs[i].str("prompt"))")
- .font(.body.weight(.semibold))
- .padding(.bottom, 6)
- choiceOptions(qs[i], index: i)
- if revealed {
- if i < meanings.count { Meaning(text: vm.tr(meanings[i]), palette: p, visible: true) }
- if i < expls.count { Meaning(text: expls[i], palette: p, visible: true) }
- if let expl = qs[i].strOrNull("expl") {
- Meaning(text: expl, palette: p, visible: true)
- }
- }
- Spacer().frame(height: 10)
- }
- }
- }
-
- @ViewBuilder
- private func choiceOptions(_ question: J, index: Int) -> some View {
- let options = question.strs("options")
- #if os(macOS)
- LazyVGrid(
- columns: [
- GridItem(.flexible(), spacing: 12, alignment: .top),
- GridItem(.flexible(), spacing: 12, alignment: .top),
- ],
- spacing: 8
- ) {
- optionChips(options, index: index, correct: question.int("correct"))
- }
- #else
- optionChips(options, index: index, correct: question.int("correct"))
- #endif
- }
-
- @ViewBuilder
- private func optionChips(_ options: [String], index: Int, correct: Int) -> some View {
- ForEach(Array(options.enumerated()), id: \.offset) { o, opt in
- let sel = picks[index] == o
- let mark: Bool? = revealed && sel ? picks[index] == correct : nil
- OptionChip(text: opt, selected: sel, mark: mark) { picks[index] = o }
- }
- }
-}
-
-private struct AssemblyEx: View {
- @ObservedObject var vm: AppModel
- let title: String
- var sub: String?
- let spec: J
- var onDone: () -> Void
- @State private var placed: [[Int]]
- @State private var pools: [[Int]]
- @State private var fb = ("", "")
- @State private var revealed = false
-
- init(vm: AppModel, title: String, sub: String?, spec: J, onDone: @escaping () -> Void) {
- self.vm = vm
- self.title = title
- self.sub = sub
- self.spec = spec
- self.onDone = onDone
- let items = spec.arr("items")
- _placed = State(initialValue: items.map { _ in [] })
- _pools = State(initialValue: items.map { item in
- Array(0.. Void
- @State private var fields: [[String]]
- @State private var fb = ("", "")
- @State private var revealed = false
-
- init(vm: AppModel, title: String, sub: String?, spec: J, onDone: @escaping () -> Void) {
- self.vm = vm
- self.title = title
- self.sub = sub
- self.spec = spec
- self.onDone = onDone
- let items = spec.arr("rows").isEmpty ? spec.arr("items") : spec.arr("rows")
- _fields = State(initialValue: items.map { q in
- Array(repeating: "", count: max(1, q.strs("hints").count, q.answerList().count))
- })
- }
-
- var body: some View {
- let items = spec.arr("rows").isEmpty ? spec.arr("items") : spec.arr("rows")
- let p = vm.palette
- let umlaut = items.contains { q in
- q.answerList().contains { hasUmlaut($0) } || hasUmlaut(q.str("prompt"))
- }
- ExerciseScaffold(vm: vm, title: title, sub: sub, action: vm.tr("check"), onAction: {
- var ok = 0, total = 0, incomplete = false
- for (i, q) in items.enumerated() {
- let answers = q.answerList()
- for (j, value) in fields[i].enumerated() {
- total += 1
- let ans = j < answers.count ? answers[j] : ""
- let norm = normalizeAnswer(value)
- if answerAccepts(norm, ans) { ok += 1 }
- else if answerIncomplete(norm, ans) { incomplete = true }
- }
- }
- if total > 0 && ok == total { fb = (vm.tr("feedback_ok"), "ok"); onDone() }
- else if incomplete { fb = (vm.tr("trans_incomplete"), "warn") }
- else { fb = (vm.tr("feedback_retry"), "err") }
- revealed = true
- }, feedback: fb.0, kind: fb.1) {
- if let note = spec.strOrNull("note") { Hint(text: note) }
- if let sample = spec.strOrNull("sample") { Hint(text: sample) }
- if let bank = spec.strOrNull("bank") { Hint(text: "\(vm.tr("wordbank")) \(bank)") }
- if umlaut { Hint(text: vm.tr("hint_umlauts")) }
- ForEach(items.indices, id: \.self) { i in
- let q = items[i]
- Prompt(text: q.str("prompt"))
- let answers = q.answerList()
- let hints = q.strs("hints")
- ForEach(fields[i].indices, id: \.self) { j in
- let ans = j < answers.count ? answers[j] : ""
- WordField(
- value: Binding(get: { fields[i][j] }, set: { fields[i][j] = $0 }),
- placeholder: j < hints.count ? hints[j] : vm.tr("your_answer"),
- mark: revealed ? answerAccepts(normalizeAnswer(fields[i][j]), ans) : nil,
- palette: p
- )
- .padding(.bottom, 4)
- }
- if revealed {
- if let m = q.strOrNull("meaning") { Meaning(text: vm.tr(m), palette: p, visible: true) }
- if let m = q.strOrNull("mean") { Meaning(text: vm.tr(m), palette: p, visible: true) }
- if let g = q.strOrNull("german") { Meaning(text: g, palette: p, visible: true) }
- if let s = q.strOrNull("shown") { Meaning(text: s, palette: p, visible: true) }
- }
- Spacer().frame(height: 10)
- }
- }
- }
-}
-
-private struct FreeEx: View {
- @ObservedObject var vm: AppModel
- let title: String
- var sub: String?
- let spec: J
- var onDone: () -> Void
- @State private var shown: [Bool]
- @State private var values: [String]
- @State private var fb = ("", "")
-
- init(vm: AppModel, title: String, sub: String?, spec: J, onDone: @escaping () -> Void) {
- self.vm = vm
- self.title = title
- self.sub = sub
- self.spec = spec
- self.onDone = onDone
- let qs = spec.arr("questions").isEmpty ? spec.arr("rows") : spec.arr("questions")
- _shown = State(initialValue: Array(repeating: false, count: qs.count))
- _values = State(initialValue: Array(repeating: "", count: qs.count))
- }
-
- var body: some View {
- let qs = spec.arr("questions").isEmpty ? spec.arr("rows") : spec.arr("questions")
- let p = vm.palette
- ExerciseScaffold(vm: vm, title: title, sub: sub, action: vm.tr("finish"), onAction: {
- fb = (vm.tr("feedback_ok"), "ok")
- shown = shown.map { _ in true }
- onDone()
- }, feedback: fb.0, kind: fb.1) {
- if let tip = spec.strOrNull("tip") { Hint(text: vm.tr(tip)) }
- ForEach(qs.indices, id: \.self) { i in
- let q = qs[i]
- Prompt(text: q.str("question").isEmpty ? q.str("stem") : q.str("question"))
- WordField(value: Binding(get: { values[i] }, set: { values[i] = $0 }), placeholder: vm.tr("your_answer"), palette: p)
- PillButton(label: vm.tr("show_sample")) { shown[i].toggle() }
- .padding(.vertical, 6)
- if shown[i] {
- let sample = q.str("sample")
- let qcs = q.str("qCs")
- let acs = q.str("aCs")
- Meaning(
- text: qcs.isEmpty ? sample : vm.fmt("sample_fmt", vm.tr(qcs), sample, vm.tr(acs)),
- palette: p,
- visible: true
- )
- }
- Spacer().frame(height: 10)
- }
- }
- }
-}
-
-private struct AssignEx: View {
- @ObservedObject var vm: AppModel
- let title: String
- var sub: String?
- let spec: J
- var onDone: () -> Void
- @State private var active = 0
- @State private var loc: [Int]
- @State private var fb = ("", "")
- @State private var revealed = false
-
- init(vm: AppModel, title: String, sub: String?, spec: J, onDone: @escaping () -> Void) {
- self.vm = vm
- self.title = title
- self.sub = sub
- self.spec = spec
- self.onDone = onDone
- _loc = State(initialValue: Array(repeating: -1, count: spec.arr("items").count))
- }
-
- var body: some View {
- let items = spec.arr("items")
- let groups = spec.strs("groups")
- let meanings = spec.strs("meanings")
- let p = vm.palette
- ExerciseScaffold(vm: vm, title: title, sub: sub, action: vm.tr("check"), onAction: {
- let ok = items.indices.allSatisfy { loc[$0] == items[$0].int("group") }
- if ok { fb = (vm.tr("feedback_ok"), "ok"); onDone() }
- else { fb = (vm.tr("feedback_retry"), "err") }
- revealed = true
- }, feedback: fb.0, kind: fb.1) {
- Hint(text: vm.tr("assign_hint"))
- FlowLayout(spacing: 8) {
- ForEach(groups.indices, id: \.self) { i in
- SegChip(label: groups[i], selected: active == i) { active = i }
- }
- }
- Text(vm.tr("wordbank"))
- #if os(macOS)
- .font(.body)
- #else
- .font(.caption)
- #endif
- .foregroundStyle(.secondary)
- .padding(.top, 10)
- FlowLayout(spacing: 6) {
- ForEach(items.indices, id: \.self) { i in
- if loc[i] == -1 {
- let label = [items[i].strOrNull("emoji"), items[i].str("label")].compactMap { $0 }.joined(separator: " ")
- WordChip(text: label) { loc[i] = active }
- }
- }
- }
- ForEach(groups.indices, id: \.self) { g in
- Text(groups[g]).font(.caption.weight(.semibold)).foregroundStyle(.secondary).padding(.top, 10)
- FlowLayout(spacing: 6) {
- ForEach(items.indices, id: \.self) { i in
- if loc[i] == g {
- let label = [items[i].strOrNull("emoji"), items[i].str("label")].compactMap { $0 }.joined(separator: " ")
- WordChip(text: label) { loc[i] = -1 }
- }
- }
- }
- }
- if revealed {
- ForEach(items.indices, id: \.self) { i in
- if i < meanings.count { Meaning(text: vm.tr(meanings[i]), palette: p, visible: true) }
- }
- }
- }
- }
-}
-
-private struct HangmanEx: View {
- @ObservedObject var vm: AppModel
- let title: String
- var sub: String?
- let spec: J
- var onDone: () -> Void
- @State private var word = 0
- @State private var misses = 0
- @State private var guessed: [Bool]
- @State private var fb = ("", "")
- @State private var finished = false
-
- init(vm: AppModel, title: String, sub: String?, spec: J, onDone: @escaping () -> Void) {
- self.vm = vm
- self.title = title
- self.sub = sub
- self.spec = spec
- self.onDone = onDone
- _guessed = State(initialValue: Array(repeating: false, count: spec.strs("letters").count))
- }
-
- var body: some View {
- let words = spec.strs("words")
- let tips = spec.strs("tips")
- let letters = spec.strs("letters")
- let p = vm.palette
- let cur = word < words.count ? words[word].uppercased() : ""
- ScrollView {
- VStack(alignment: .leading, spacing: 8) {
- Text(vm.fmt("hm_progress", word + 1, words.count)).foregroundStyle(.secondary)
- if word < tips.count { Hint(text: "\(vm.tr("hm_hint")): \(vm.tr(tips[word]))") }
- Canvas { ctx, size in
- var gallows = Path()
- gallows.move(to: CGPoint(x: 40, y: size.height - 10))
- gallows.addLine(to: CGPoint(x: size.width * 0.55, y: size.height - 10))
- gallows.move(to: CGPoint(x: 70, y: size.height - 10))
- gallows.addLine(to: CGPoint(x: 70, y: 16))
- gallows.addLine(to: CGPoint(x: size.width * 0.3 + 40, y: 16))
- ctx.stroke(gallows, with: .color(.primary), style: StrokeStyle(lineWidth: 6, lineCap: .round))
- let x = size.width * 0.3 + 40
- if misses > 0 {
- var rope = Path(); rope.move(to: CGPoint(x: x, y: 16)); rope.addLine(to: CGPoint(x: x, y: 36))
- ctx.stroke(rope, with: .color(.primary), lineWidth: 4)
- }
- if misses > 1 {
- ctx.stroke(Path(ellipseIn: CGRect(x: x - 16, y: 36, width: 32, height: 32)), with: .color(.primary), lineWidth: 4)
- }
- if misses > 2 {
- var body = Path(); body.move(to: CGPoint(x: x, y: 68)); body.addLine(to: CGPoint(x: x, y: 110))
- ctx.stroke(body, with: .color(.primary), lineWidth: 4)
- }
- if misses > 3 {
- var a = Path(); a.move(to: CGPoint(x: x, y: 80)); a.addLine(to: CGPoint(x: x - 22, y: 100))
- ctx.stroke(a, with: .color(.primary), lineWidth: 4)
- }
- if misses > 4 {
- var a = Path(); a.move(to: CGPoint(x: x, y: 80)); a.addLine(to: CGPoint(x: x + 22, y: 100))
- ctx.stroke(a, with: .color(.primary), lineWidth: 4)
- }
- if misses > 5 {
- var a = Path(); a.move(to: CGPoint(x: x, y: 110)); a.addLine(to: CGPoint(x: x - 22, y: 140))
- ctx.stroke(a, with: .color(.primary), lineWidth: 4)
- }
- }
- .frame(height: 160)
- HStack(spacing: 6) {
- ForEach(Array(cur.enumerated()), id: \.offset) { _, ch in
- let idx = letters.firstIndex { $0.compare(String(ch), options: .caseInsensitive) == .orderedSame }
- let show = idx.map { guessed[$0] } ?? false
- Text(show ? String(ch) : "_")
- .font(.system(size: 26, weight: .bold, design: .rounded))
- .monospaced()
- }
- }
- .frame(maxWidth: .infinity)
- .padding(.vertical, 8)
- FlowLayout(spacing: 6) {
- ForEach(letters, id: \.self) { letter in
- if let i = letters.firstIndex(of: letter) {
- Button {
- guessed[i] = true
- if !cur.localizedCaseInsensitiveContains(letter) {
- misses += 1
- if misses >= 6 { fb = (vm.fmt("hm_fail", cur), "err") }
- } else if cur.allSatisfy({ ch in
- let idx = letters.firstIndex { $0.compare(String(ch), options: .caseInsensitive) == .orderedSame }
- return idx.map { guessed[$0] } ?? false
- }) {
- if word + 1 >= words.count {
- finished = true
- fb = (vm.tr("hm_done"), "ok")
- onDone()
- } else {
- fb = (vm.tr("hm_wrong"), "ok")
- word += 1
- misses = 0
- guessed = guessed.map { _ in false }
- }
- }
- } label: {
- Text(letter)
- .font(.body.weight(.semibold))
- .frame(minWidth: 22)
- }
- .buttonStyle(.bordered)
- .tint(guessed[i] ? .secondary : .primary)
- .disabled(guessed[i] || finished)
- }
- }
- }
- FeedbackLine(text: fb.0, kind: fb.1, palette: p)
- if misses >= 6 && !finished {
- PillButton(label: vm.tr("hm_retry")) {
- misses = 0
- guessed = guessed.map { _ in false }
- fb = ("", "")
- }
- }
- }
- .exerciseContent()
- }
- .detailScreen(title, subtitle: sub)
- }
-}
-
-private extension Array {
- func chunked(_ size: Int) -> [[Element]] {
- stride(from: 0, to: count, by: size).map { Array(self[$0.. Void
- @State private var cards: [(String, String, String)]
- @State private var index = 0
- @State private var done = 0
- @State private var value = ""
- @State private var fb = ("", "")
- let total: Int
-
- init(vm: AppModel, title: String, sub: String?, spec: J, onDone: @escaping () -> Void) {
- self.vm = vm
- self.title = title
- self.sub = sub
- self.spec = spec
- self.onDone = onDone
- let list = spec.arr("sections").flatMap { sec in
- sec.arr("rows").map { (sec.str("header"), $0.str("prompt"), $0.str("answers")) }
- }
- _cards = State(initialValue: list)
- total = list.count
- }
-
- var body: some View {
- let p = vm.palette
- ScrollView {
- VStack(alignment: .leading, spacing: 8) {
- ProgressView(value: Double(done), total: Double(max(total, 1)))
- Text(vm.fmt("trans_progress", done, total)).font(.footnote).foregroundStyle(.secondary)
- if cards.isEmpty {
- Label(vm.tr("trans_done"), systemImage: "checkmark.seal.fill")
- .font(.headline)
- .foregroundStyle(p.success)
- .padding(.top, 8)
- } else if index < cards.count {
- let cur = cards[index]
- GlassCard {
- VStack(alignment: .leading, spacing: 6) {
- Text(cur.0).font(.caption.weight(.semibold)).foregroundStyle(.secondary)
- Text(cur.1).font(.title2.weight(.semibold))
- }
- }
- .padding(.vertical, 8)
- if hasUmlaut(cur.2) { Hint(text: vm.tr("hint_umlauts")) }
- WordField(value: $value, placeholder: vm.tr("your_answer"), palette: p)
- .submitLabel(.done)
- .onSubmit { check(cur) }
- FeedbackLine(text: fb.0, kind: fb.1, palette: p)
- }
- }
- .exerciseContent()
- }
- .scrollDismissesKeyboard(.interactively)
- .detailScreen(title, subtitle: sub)
- .glassBottomBar {
- if index < cards.count {
- BottomAction(label: vm.tr("check"), action: { check(cards[index]) }) { EmptyView() }
- }
- }
- }
-
- private func check(_ cur: (String, String, String)) {
- let norm = normalizeAnswer(value)
- if answerAccepts(norm, cur.2) {
- done += 1
- cards.remove(at: index)
- if cards.isEmpty {
- fb = (vm.tr("trans_done"), "ok")
- onDone()
- } else {
- if index >= cards.count { index = 0 }
- value = ""
- fb = ("", "")
- }
- } else if answerIncomplete(norm, cur.2) {
- fb = (vm.tr("trans_incomplete"), "warn")
- } else {
- fb = (vm.fmt("trans_wrong", cur.2.split(separator: "|").first.map(String.init) ?? ""), "err")
- let item = cards.remove(at: index)
- cards.append(item)
- if index >= cards.count { index = 0 }
- value = ""
- }
- }
-}
diff --git a/ios/Graduately/UI/ExercisesMore.swift b/ios/Graduately/UI/ExercisesMore.swift
deleted file mode 100644
index 783dd4d..0000000
--- a/ios/Graduately/UI/ExercisesMore.swift
+++ /dev/null
@@ -1,1884 +0,0 @@
-import AVFoundation
-import SwiftUI
-
-struct DialogEx: View {
- @ObservedObject var vm: AppModel
- let title: String
- var sub: String?
- let spec: J
- var onDone: () -> Void
-
- var body: some View {
- let pool = spec.strs("pool")
- let meanings = spec.strs("meanings")
- let lines = dialogLines()
- ComboRows(vm: vm, title: title, sub: sub, pool: pool, n: lines.count, onDone: onDone) { picks, expected, revealed in
- ForEach(lines.indices, id: \.self) { i in
- let line = lines[i]
- if let name = line.name {
- Text(name)
- .font(.subheadline.weight(.semibold))
- .foregroundStyle(.secondary)
- .padding(.vertical, 6)
- }
- ComboLine(
- vm: vm, pool: pool, picks: picks, expected: expected, idx: i,
- answer: line.answer,
- prefix: line.prefix,
- suffix: line.suffix,
- meaning: i < meanings.count ? meanings[i] : nil,
- revealed: revealed
- )
- }
- }
- }
-
- private func dialogLines() -> [(name: String?, prefix: String, answer: String, suffix: String)] {
- var out: [(name: String?, prefix: String, answer: String, suffix: String)] = []
- for d in spec.arr("dialogues") {
- var header: String? = d.str("name")
- for row in d.arr("rows") {
- out.append((
- header,
- [row.str("speaker"), row.str("before")].filter { !$0.isEmpty }.joined(separator: " "),
- row.str("answer"),
- row.str("after")
- ))
- header = nil
- }
- }
- return out
- }
-}
-
-struct ComboEx: View {
- @ObservedObject var vm: AppModel
- let title: String
- var sub: String?
- let spec: J
- var onDone: () -> Void
-
- var body: some View {
- let pool = spec.strs("pool")
- let rows = spec.arr("rows")
- let meanings = spec.strs("meanings")
- let type = spec.str("type")
- if type == "verb_sections" {
- let sections = spec.arr("sections")
- let allRows = sections.flatMap { $0.arr("rows") }
- ComboRows(vm: vm, title: title, sub: sub, pool: spec.strs("pool"), n: allRows.count, onDone: onDone, revealMean: spec.bool("transUpfront")) { picks, expected, revealed in
- ForEach(Array(verbSectionLines().enumerated()), id: \.offset) { idx, line in
- if let title = line.title {
- Text(title).font(.subheadline.weight(.semibold)).foregroundStyle(.secondary)
- }
- ComboLine(
- vm: vm, pool: spec.strs("pool"), picks: picks, expected: expected, idx: idx,
- answer: line.answer, prefix: line.before, suffix: line.after,
- meaning: line.meaning,
- revealed: revealed || spec.bool("transUpfront")
- )
- }
- }
- } else if type == "ordne" {
- ComboRows(vm: vm, title: title, sub: sub, pool: [], n: rows.count * 2, onDone: onDone) { picks, expected, revealed in
- ForEach(rows.indices, id: \.self) { i in
- ComboLine(vm: vm, pool: spec.strs("fwPool"), picks: picks, expected: expected, idx: i * 2, answer: rows[i].str("fw"), prefix: "\(i + 1). ", suffix: rows[i].str("mid"), revealed: revealed)
- ComboLine(vm: vm, pool: spec.strs("ansPool"), picks: picks, expected: expected, idx: i * 2 + 1, answer: rows[i].str("ans"), prefix: "→ ", meaning: i < meanings.count ? meanings[i] : nil, revealed: revealed)
- }
- }
- } else if type == "fill" {
- let blanks = rows.reduce(0) { $0 + $1.answerList().count }
- ComboRows(vm: vm, title: title, sub: sub, pool: pool, n: blanks, onDone: onDone, revealMean: spec.bool("preMeaning")) { picks, expected, revealed in
- if let sample = spec.strOrNull("sample") { Hint(text: sample) }
- ForEach(Array(fillRows().enumerated()), id: \.offset) { _, block in
- FlowLayout(spacing: 4) {
- if let num = block.num {
- Text(num).font(.body.weight(.semibold)).foregroundStyle(.primary)
- }
- ForEach(Array(block.parts.enumerated()), id: \.offset) { _, part in
- switch part {
- case .text(let s):
- Text(s).foregroundStyle(.primary)
- case .blank(let idx, let ans):
- ComboLine(vm: vm, pool: pool, picks: picks, expected: expected, idx: idx, answer: ans, revealed: revealed, inline: true)
- }
- }
- if let mean = block.meaning {
- Text(vm.tr(mean)).font(.caption).foregroundStyle(.secondary)
- }
- }
- .padding(.bottom, 6)
- if revealed || spec.bool("preMeaning") || spec.bool("showGerman"), let mean = block.rowMean {
- Meaning(text: vm.tr(mean), palette: vm.palette, visible: true)
- }
- }
- }
- } else {
- ComboRows(vm: vm, title: title, sub: sub, pool: pool, n: rows.count, onDone: onDone, revealMean: spec.bool("preMeaning")) { picks, expected, revealed in
- if let sample = spec.strOrNull("sample") { Hint(text: sample) }
- ForEach(rows.indices, id: \.self) { i in
- let r = rows[i]
- let prefix: String = {
- switch type {
- case "number": return "\(r.str("digits")) = "
- case "count": return "\(Array(repeating: r.str("emoji"), count: r.int("count")).joined(separator: " ")) \(r.str("noun")) "
- case "seq": return r.str("before")
- case "verbclue": return "\(r.str("emoji")) \(r.str("before"))"
- default: return r.str("before")
- }
- }()
- let suffix: String = {
- switch type {
- case "seq", "verbclue", "verb": return r.str("after")
- default: return ""
- }
- }()
- ComboLine(
- vm: vm, pool: pool, picks: picks, expected: expected, idx: i, answer: r.str("answer"),
- prefix: prefix, suffix: suffix,
- meaning: i < meanings.count ? meanings[i] : r.strOrNull("mean"),
- revealed: revealed || spec.bool("preMeaning")
- )
- }
- }
- }
- }
-
- private func verbSectionLines() -> [(title: String?, before: String, answer: String, after: String, meaning: String?)] {
- var out: [(title: String?, before: String, answer: String, after: String, meaning: String?)] = []
- for sec in spec.arr("sections") {
- var header: String? = sec.str("title")
- let meanings = sec.strs("meanings")
- for (ri, row) in sec.arr("rows").enumerated() {
- out.append((
- header,
- row.str("before"),
- row.str("answer"),
- row.str("after"),
- ri < meanings.count ? meanings[ri] : nil
- ))
- header = nil
- }
- }
- return out
- }
-
- private enum FillPart {
- case text(String)
- case blank(Int, String)
- }
-
- private func fillRows() -> [(num: String?, parts: [FillPart], meaning: String?, rowMean: String?)] {
- let meanings = spec.strs("meanings")
- var i = 0
- return spec.arr("rows").enumerated().map { ri, r in
- var parts: [FillPart] = []
- let segs = r.strsOrEmpty("segs")
- let ans = r.answerList()
- for si in segs.indices {
- if let seg = segs[si], !seg.isEmpty { parts.append(.text(seg)) }
- if si < ans.count {
- parts.append(.blank(i, ans[si]))
- i += 1
- }
- }
- return (
- r.strOrNull("num"),
- parts,
- ri < meanings.count ? meanings[ri] : nil,
- r.strOrNull("mean")
- )
- }
- }
-}
-
-private struct ComboRows: View {
- @ObservedObject var vm: AppModel
- let title: String
- var sub: String?
- let pool: [String]
- let n: Int
- var onDone: () -> Void
- var revealMean = false
- @ViewBuilder var bodyContent: (Binding<[String]>, Binding<[String]>, Bool) -> Body
- @State private var picks: [String]
- @State private var expected: [String]
- @State private var fb = ("", "")
- @State private var revealed: Bool
-
- init(
- vm: AppModel,
- title: String,
- sub: String?,
- pool: [String],
- n: Int,
- onDone: @escaping () -> Void,
- revealMean: Bool = false,
- @ViewBuilder bodyContent: @escaping (Binding<[String]>, Binding<[String]>, Bool) -> Body
- ) {
- self.vm = vm
- self.title = title
- self.sub = sub
- self.pool = pool
- self.n = n
- self.onDone = onDone
- self.revealMean = revealMean
- self.bodyContent = bodyContent
- _picks = State(initialValue: Array(repeating: "", count: n))
- _expected = State(initialValue: Array(repeating: "", count: n))
- _revealed = State(initialValue: revealMean)
- }
-
- var body: some View {
- ExerciseScaffold(vm: vm, title: title, sub: sub, action: vm.tr("check"), onAction: {
- let ok = expected.indices.allSatisfy { i in
- let exp = expected[i]
- return exp.isEmpty || answerAccepts(normalizeAnswer(picks[i]), exp)
- }
- if ok { fb = (vm.tr("feedback_ok"), "ok"); onDone() }
- else { fb = (vm.tr("feedback_retry"), "err") }
- revealed = true
- }, feedback: fb.0, kind: fb.1) {
- bodyContent($picks, $expected, revealed)
- }
- }
-}
-
-private struct ComboLine: View {
- @ObservedObject var vm: AppModel
- let pool: [String]
- @Binding var picks: [String]
- @Binding var expected: [String]
- let idx: Int
- let answer: String
- var prefix: String = ""
- var suffix: String = ""
- var meaning: String?
- var revealed: Bool
- var inline = false
- @State private var open = false
-
- var body: some View {
- let p = vm.palette
- let pick = idx < picks.count ? picks[idx] : ""
- let mark: Bool? = {
- if revealed && !pick.isEmpty { return answerAccepts(normalizeAnswer(pick), answer) }
- return nil
- }()
- let chip = Button {
- open = true
- } label: {
- HStack(spacing: 4) {
- Text(pick.isEmpty ? "…" : pick)
- .foregroundStyle(pick.isEmpty ? Color.secondary : Color.primary)
- .frame(minWidth: 36)
- Image(systemName: mark == true ? "checkmark.circle.fill" : mark == false ? "xmark.circle.fill" : "chevron.up.chevron.down")
- .font(.caption2)
- .foregroundStyle(mark == true ? p.success : mark == false ? p.error : Color.secondary)
- }
- }
- .buttonStyle(.bordered)
- .controlSize(.small)
- .tint(mark == true ? p.success : mark == false ? p.error : .secondary)
-
- Group {
- if inline {
- chip
- } else {
- FlowLayout(spacing: 6) {
- if !prefix.isEmpty { Text(prefix) }
- chip
- if !suffix.isEmpty { Text(suffix) }
- }
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding(.bottom, 8)
- }
- }
- .onAppear {
- if idx >= 0 && idx < expected.count { expected[idx] = answer }
- }
- .onChange(of: answer) { _, new in
- if idx >= 0 && idx < expected.count { expected[idx] = new }
- }
- .sheet(isPresented: $open) {
- WordPicker(palette: p, pool: pool) { word in
- if let word, idx < picks.count { picks[idx] = word }
- open = false
- }
- .sheetDetents()
- }
- if revealed, let meaning, !meaning.isEmpty {
- Meaning(text: vm.tr(meaning), palette: p, visible: true)
- }
- }
-}
-
-struct WordPicker: View {
- let palette: Palette
- let pool: [String]
- var onPick: (String?) -> Void
-
- var body: some View {
- NavigationStack {
- List {
- ForEach(Array(pool.enumerated()), id: \.offset) { _, w in
- Button {
- onPick(w)
- } label: {
- Text(w)
- .foregroundStyle(.primary)
- .frame(maxWidth: .infinity, alignment: .leading)
- .contentShape(Rectangle())
- }
- .buttonStyle(PressScaleStyle())
- }
- }
- .listStyle(.plain)
- .scrollContentBackground(.visible)
- .navigationTitle("Vyberte slovo")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .cancellationAction) {
- Button(role: .cancel) { onPick(nil) } label: { Image(systemName: "xmark") }
- }
- }
- }
- .sheetDetents()
- }
-}
-
-struct Ex2Ex: View {
- @ObservedObject var vm: AppModel
- let title: String
- var sub: String?
- let spec: J
- var onDone: () -> Void
-
- var body: some View {
- let items = spec.arr("items")
- let pool = spec.strs("pool")
- let blanks = items.reduce(0) { acc, item in
- acc + item.arr("rows").reduce(0) { $0 + $1.strs("answers").count }
- }
- ComboRows(vm: vm, title: title, sub: sub, pool: pool, n: blanks, onDone: onDone) { picks, expected, revealed in
- ForEach(Array(ex2Blocks().enumerated()), id: \.offset) { _, block in
- Meaning(text: block.czech, palette: vm.palette, visible: true)
- ForEach(Array(block.rows.enumerated()), id: \.offset) { _, row in
- FlowLayout(spacing: 4) {
- if let num = row.num {
- Text(num).foregroundStyle(.primary).padding(.trailing, 6)
- }
- ForEach(Array(row.parts.enumerated()), id: \.offset) { _, part in
- switch part {
- case .text(let s):
- Text(s).foregroundStyle(.primary)
- case .blank(let idx, let ans):
- ComboLine(vm: vm, pool: pool, picks: picks, expected: expected, idx: idx, answer: ans, revealed: revealed, inline: true)
- }
- }
- }
- }
- }
- }
- }
-
- private enum Part {
- case text(String)
- case blank(Int, String)
- }
-
- private func ex2Blocks() -> [(czech: String, rows: [(num: String?, parts: [Part])])] {
- var i = 0
- return spec.arr("items").map { item in
- let rows = item.arr("rows").map { row -> (num: String?, parts: [Part]) in
- var parts: [Part] = []
- let segs = row.strsOrEmpty("segs")
- let ans = row.strs("answers")
- for si in segs.indices {
- if let seg = segs[si], !seg.isEmpty { parts.append(.text(seg)) }
- if si < ans.count {
- parts.append(.blank(i, ans[si]))
- i += 1
- }
- }
- return (row.strOrNull("num"), parts)
- }
- return (item.str("czech"), rows)
- }
- }
-}
-
-struct LettersEx: View {
- @ObservedObject var vm: AppModel
- let title: String
- var sub: String?
- let spec: J
- var onDone: () -> Void
- @State private var fields: [String]
- @State private var fb = ("", "")
- @State private var revealed = false
-
- init(vm: AppModel, title: String, sub: String?, spec: J, onDone: @escaping () -> Void) {
- self.vm = vm
- self.title = title
- self.sub = sub
- self.spec = spec
- self.onDone = onDone
- let n = spec.arr("rows").flatMap { $0.strs("answers") }.count
- _fields = State(initialValue: Array(repeating: "", count: n))
- }
-
- var body: some View {
- let rows = spec.arr("rows")
- ExerciseScaffold(vm: vm, title: title, sub: sub, action: vm.tr("check"), onAction: {
- var i = 0, ok = 0, total = 0
- for r in rows {
- for ans in r.strs("answers") {
- total += 1
- if i < fields.count && answerAccepts(normalizeAnswer(fields[i]), ans) { ok += 1 }
- i += 1
- }
- }
- if ok == total { fb = (vm.tr("feedback_ok"), "ok"); onDone() }
- else { fb = (vm.tr("feedback_retry"), "err") }
- revealed = true
- }, feedback: fb.0, kind: fb.1) {
- if rows.contains(where: { $0.strs("answers").contains { hasUmlaut($0) } }) {
- Hint(text: vm.tr("hint_umlauts"))
- }
- LettersRows(vm: vm, rows: rows, fields: $fields, revealed: revealed)
- }
- }
-}
-
-private struct LettersRows: View {
- @ObservedObject var vm: AppModel
- let rows: [J]
- @Binding var fields: [String]
- let revealed: Bool
-
- var body: some View {
- let p = vm.palette
- let pairs = build()
- VStack(alignment: .leading, spacing: 8) {
- ForEach(pairs.indices, id: \.self) { ri in
- let row = pairs[ri]
- if let num = row.num { Text(num).foregroundStyle(.secondary) }
- FlowLayout {
- ForEach(row.parts.indices, id: \.self) { pi in
- switch row.parts[pi] {
- case .text(let s):
- Text(s).foregroundStyle(.primary)
- case .field(let idx, let ans):
- WordField(
- value: Binding(get: { idx < fields.count ? fields[idx] : "" }, set: { if idx < fields.count { fields[idx] = $0 } }),
- placeholder: "",
- mark: revealed ? answerAccepts(normalizeAnswer(idx < fields.count ? fields[idx] : ""), ans) : nil,
- palette: p
- )
- .frame(width: 72)
- }
- }
- }
- if revealed, let czech = row.czech { Meaning(text: czech, palette: p, visible: true) }
- if revealed, let mean = row.mean { Meaning(text: mean, palette: p, visible: true) }
- }
- }
- }
-
- private enum Part {
- case text(String)
- case field(Int, String)
- }
-
- private struct RowModel {
- var num: String?
- var parts: [Part]
- var czech: String?
- var mean: String?
- }
-
- private func build() -> [RowModel] {
- var i = 0
- return rows.map { r in
- var parts: [Part] = []
- let segs = r.strsOrEmpty("segs")
- let ans = r.strs("answers")
- for si in segs.indices {
- if let seg = segs[si], !seg.isEmpty { parts.append(.text(seg)) }
- if si < ans.count {
- parts.append(.field(i, ans[si]))
- i += 1
- }
- }
- return RowModel(num: r.strOrNull("num"), parts: parts, czech: r.strOrNull("czech"), mean: r.strOrNull("mean"))
- }
- }
-}
-
-struct TableEx: View {
- @ObservedObject var vm: AppModel
- let title: String
- var sub: String?
- let spec: J
- var onDone: () -> Void
- @State private var fields: [String]
- @State private var fb = ("", "")
- @State private var revealed = false
-
- init(vm: AppModel, title: String, sub: String?, spec: J, onDone: @escaping () -> Void) {
- self.vm = vm
- self.title = title
- self.sub = sub
- self.spec = spec
- self.onDone = onDone
- _fields = State(initialValue: Array(repeating: "", count: spec.strs("nouns").count * spec.strs("persons").count))
- }
-
- var body: some View {
- let persons = spec.strs("persons")
- let nouns = spec.strs("nouns")
- let answers = spec.intRows("answers")
- let p = vm.palette
- ExerciseScaffold(vm: vm, title: title, sub: sub, action: vm.tr("check"), onAction: {
- var ok = 0
- for (r, row) in answers.enumerated() {
- for (c, ans) in row.enumerated() {
- let i = r * persons.count + c
- if i < fields.count && normalizeAnswer(fields[i]) == normalizeAnswer(ans) { ok += 1 }
- }
- }
- if ok == fields.count { fb = (vm.tr("feedback_ok"), "ok"); onDone() }
- else { fb = (vm.tr("feedback_retry"), "err") }
- revealed = true
- }, feedback: fb.0, kind: fb.1) {
- ForEach(nouns.indices, id: \.self) { r in
- Text(nouns[r]).font(.body.weight(.bold)).foregroundStyle(.primary)
- ForEach(persons.indices, id: \.self) { c in
- let i = r * persons.count + c
- Text(persons[c]).font(.caption).foregroundStyle(.secondary)
- WordField(value: Binding(get: { i < fields.count ? fields[i] : "" }, set: { if i < fields.count { fields[i] = $0 } }), placeholder: "", palette: p)
- }
- Spacer().frame(height: 8)
- }
- }
- }
-}
-
-struct RevealEx: View {
- @ObservedObject var vm: AppModel
- let title: String
- var sub: String?
- let spec: J
- var onDone: () -> Void
- @State private var shown: [Bool]
- @State private var fb = ("", "")
-
- init(vm: AppModel, title: String, sub: String?, spec: J, onDone: @escaping () -> Void) {
- self.vm = vm
- self.title = title
- self.sub = sub
- self.spec = spec
- self.onDone = onDone
- _shown = State(initialValue: Array(repeating: false, count: spec.arr("items").count))
- }
-
- var body: some View {
- let items = spec.arr("items")
- let p = vm.palette
- ExerciseScaffold(vm: vm, title: title, sub: sub, action: vm.tr("finish"), onAction: {
- shown = shown.map { _ in true }
- fb = (vm.tr("feedback_ok"), "ok")
- onDone()
- }, feedback: fb.0, kind: fb.1) {
- if let note = spec.strOrNull("note") { Hint(text: note) }
- ForEach(items.indices, id: \.self) { i in
- Prompt(text: items[i].str("prompt"))
- PillButton(label: vm.tr("show_sample")) { shown[i].toggle() }
- .padding(.vertical, 6)
- if shown[i] { Meaning(text: items[i].str("solution"), palette: p, visible: true) }
- Spacer().frame(height: 8)
- }
- }
- }
-}
-
-struct NetQuizScreen: View {
- @ObservedObject var vm: AppModel
- let id: Int
-
- var body: some View {
- if let lesson = vm.content.netLesson(id) {
- if id == 1 {
- VlsmEx(vm: vm, lesson: lesson)
- } else {
- let spec = J([
- "type": "choice",
- "title": vm.tr(lesson.str("exTitleKey")),
- "sub": "net_quiz_intro",
- "questions": lesson.rawArray("quiz"),
- ])
- DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) {
- vm.markNet(id)
- }
- }
- }
- }
-}
-
-struct SciQuizScreen: View {
- @ObservedObject var vm: AppModel
- let area: String
- let id: Int
-
- var body: some View {
- let lesson = area == "bio" ? vm.content.bioLesson(id) : vm.content.chemLesson(id)
- if let lesson {
- let spec = J([
- "type": "choice",
- "title": vm.tr(lesson.str("exTitleKey")),
- "sub": lesson.str("quizHeadKey"),
- "questions": lesson.rawArray("quiz"),
- ])
- DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) {
- if area == "bio" { vm.markBio(id) } else { vm.markChem(id) }
- }
- }
- }
-}
-
-private func mathLesson(_ vm: AppModel, year: Int, id: Int) -> J? {
- switch year {
- case 0: return vm.content.mat0Lesson(id)
- case 2: return vm.content.mat2Lesson(id)
- case 3: return vm.content.mat3Lesson(id)
- case 4: return vm.content.mat4Lesson(id)
- default: return vm.content.matLesson(id)
- }
-}
-
-private func markMathLesson(_ vm: AppModel, year: Int, id: Int) {
- switch year {
- case 0: vm.markMat0(id)
- case 2: vm.markMat2(id)
- case 3: vm.markMat3(id)
- case 4: vm.markMat4(id)
- default: vm.markMat(id)
- }
-}
-
-struct MathPracticeScreen: View {
- @ObservedObject var vm: AppModel
- let year: Int
- let id: Int
- @State private var text: [Int: String] = [:]
- @State private var points: [Int: [(Int, Int)]] = [:]
- @State private var marks: [Int: Bool] = [:]
- @State private var revealed = false
- @State private var feedback = ""
- @State private var kind = ""
-
- var body: some View {
- if let lesson = mathLesson(vm, year: year, id: id) {
- let problems = lesson.arr("problems")
- ExerciseScaffold(
- vm: vm,
- title: vm.tr(lesson.str("exTitleKey")),
- sub: vm.tr(lesson.str("quizHeadKey")),
- action: vm.tr("check"),
- onAction: { check(problems) },
- feedback: feedback,
- kind: kind
- ) {
- Text(vm.tr("math_quiz_intro"))
- .font(.subheadline)
- .foregroundStyle(.secondary)
- .padding(.bottom, 4)
- ForEach(problems.indices, id: \.self) { i in
- mathCard(problems[i], index: i)
- }
- }
- .onChange(of: year) { _, _ in reset() }
- .onChange(of: id) { _, _ in reset() }
- }
- }
-
- private func reset() {
- text = [:]
- points = [:]
- marks = [:]
- revealed = false
- feedback = ""
- kind = ""
- }
-
- private func check(_ problems: [J]) {
- var all = !problems.isEmpty
- var next: [Int: Bool] = [:]
- for i in problems.indices {
- let spec = problems[i].str("answer")
- let good = mathIsDraw(spec)
- ? mathDrawOk(spec, points[i] ?? [])
- : mathAnswerOk(spec, text[i] ?? "")
- next[i] = good
- if !good { all = false }
- }
- marks = next
- revealed = true
- if all {
- feedback = vm.tr("feedback_ok")
- kind = "ok"
- markMathLesson(vm, year: year, id: id)
- } else {
- feedback = vm.tr("feedback_retry_short")
- kind = "bad"
- }
- }
-
- @ViewBuilder
- private func mathCard(_ problem: J, index i: Int) -> some View {
- let spec = problem.str("answer")
- let p = vm.palette
- let border: Color = {
- guard let mark = marks[i] else { return p.surface2 }
- return mark ? p.success : p.error
- }()
- VStack(alignment: .leading, spacing: 8) {
- Text(problem.str("prompt"))
- .frame(maxWidth: .infinity, alignment: .leading)
- if mathIsDraw(spec) {
- Text(vm.tr("math_draw_hint"))
- .font(.caption)
- .foregroundStyle(.secondary)
- MathGrid(
- given: mathDrawGiven(spec),
- user: points[i] ?? [],
- need: mathDrawNeed(spec),
- mark: marks[i],
- palette: p
- ) { next in
- points[i] = next
- }
- HStack {
- Button(vm.tr("math_undo")) {
- var pts = points[i] ?? []
- if !pts.isEmpty {
- pts.removeLast()
- points[i] = pts
- }
- }
- Button(vm.tr("math_clear")) { points[i] = [] }
- }
- .buttonStyle(.bordered)
- } else {
- TextField(vm.tr("math_answer_ph"), text: Binding(
- get: { text[i] ?? "" },
- set: { text[i] = $0 }
- ))
- .textFieldStyle(.roundedBorder)
- #if os(iOS)
- .textInputAutocapitalization(.never)
- .keyboardType(.numbersAndPunctuation)
- #endif
- .autocorrectionDisabled()
- }
- if revealed {
- Text(problem.str("hint"))
- .font(.footnote)
- .foregroundStyle(.secondary)
- }
- }
- .padding(12)
- .background(p.surface0, in: RoundedRectangle(cornerRadius: 12))
- .overlay(RoundedRectangle(cornerRadius: 12).stroke(border, lineWidth: 1.5))
- }
-}
-
-private struct MathGrid: View {
- let given: [(Int, Int)]
- let user: [(Int, Int)]
- let need: Int
- let mark: Bool?
- let palette: Palette
- var onChange: ([(Int, Int)]) -> Void
-
- private let cell: CGFloat = 28
- private let pad: CGFloat = 18
- private let gx = 10
- private let gy = 8
- private var width: CGFloat { pad * 2 + CGFloat(gx) * cell }
- private var height: CGFloat { pad * 2 + CGFloat(gy) * cell }
-
- private func screen(_ x: Int, _ y: Int) -> CGPoint {
- CGPoint(x: pad + CGFloat(x) * cell, y: height - pad - CGFloat(y) * cell)
- }
-
- private func snap(_ loc: CGPoint) -> (Int, Int)? {
- let cx = (loc.x - pad) / cell
- let cy = (height - pad - loc.y) / cell
- let ix = Int(cx.rounded())
- let iy = Int(cy.rounded())
- if ix < 0 || ix > gx || iy < 0 || iy > gy { return nil }
- if abs(cx - CGFloat(ix)) > 0.45 || abs(cy - CGFloat(iy)) > 0.45 { return nil }
- return (ix, iy)
- }
-
- var body: some View {
- Canvas { ctx, _ in
- let rect = CGRect(x: 0, y: 0, width: width, height: height)
- ctx.fill(Path(rect), with: .color(palette.surface1))
- var grid = Path()
- for i in 0...gx {
- let x = pad + CGFloat(i) * cell
- grid.move(to: CGPoint(x: x, y: pad))
- grid.addLine(to: CGPoint(x: x, y: height - pad))
- }
- for i in 0...gy {
- let y = pad + CGFloat(i) * cell
- grid.move(to: CGPoint(x: pad, y: y))
- grid.addLine(to: CGPoint(x: width - pad, y: y))
- }
- ctx.stroke(grid, with: .color(palette.subtext.opacity(0.55)), lineWidth: 1)
- var axes = Path()
- axes.move(to: CGPoint(x: pad, y: height - pad))
- axes.addLine(to: CGPoint(x: width - pad, y: height - pad))
- axes.move(to: CGPoint(x: pad, y: height - pad))
- axes.addLine(to: CGPoint(x: pad, y: pad))
- ctx.stroke(axes, with: .color(palette.text), lineWidth: 1.6)
- if given.count >= 2 {
- var seg = Path()
- seg.move(to: screen(given[0].0, given[0].1))
- seg.addLine(to: screen(given[1].0, given[1].1))
- if given.count >= 3 {
- seg.move(to: screen(given[0].0, given[0].1))
- seg.addLine(to: screen(given[2].0, given[2].1))
- }
- ctx.stroke(seg, with: .color(palette.accent), lineWidth: 2)
- }
- let names = ["A", "B", "C", "D"]
- for (i, g) in given.enumerated() where i < names.count {
- let pt = screen(g.0, g.1)
- ctx.fill(Path(ellipseIn: CGRect(x: pt.x - 5.5, y: pt.y - 5.5, width: 11, height: 11)), with: .color(palette.accent))
- ctx.draw(
- Text(names[i]).font(.system(size: 13, weight: .semibold)).foregroundStyle(palette.text),
- at: CGPoint(x: pt.x + 8, y: pt.y - 8),
- anchor: .bottomLeading
- )
- }
- let dot = mark == false ? palette.error : palette.success
- for (i, u) in user.enumerated() {
- let pt = screen(u.0, u.1)
- ctx.fill(Path(ellipseIn: CGRect(x: pt.x - 5.5, y: pt.y - 5.5, width: 11, height: 11)), with: .color(dot))
- ctx.draw(
- Text("\(i + 1)").font(.system(size: 12, weight: .bold)).foregroundStyle(palette.text),
- at: CGPoint(x: pt.x + 8, y: pt.y - 8),
- anchor: .bottomLeading
- )
- }
- }
- .frame(width: width, height: height)
- .contentShape(Rectangle())
- .gesture(
- DragGesture(minimumDistance: 0).onEnded { value in
- guard let hit = snap(value.location) else { return }
- if given.contains(where: { $0.0 == hit.0 && $0.1 == hit.1 }) { return }
- var next = user
- if let idx = next.firstIndex(where: { $0.0 == hit.0 && $0.1 == hit.1 }) {
- next.remove(at: idx)
- } else if next.count < need {
- next.append(hit)
- } else {
- return
- }
- onChange(next)
- }
- )
- }
-}
-
-struct ItYearList: View {
- @ObservedObject var vm: AppModel
- let course: String
- let titleKey: String
- let subKey: String
- let lesson: (Int) -> Route
-
- var body: some View {
- LessonListScreen(
- vm: vm,
- title: vm.tr(titleKey),
- subtitle: vm.tr(subKey),
- rows: vm.content.itLessons(course).map { l in
- let id = l.int("id")
- return LessonRow(
- id: "\(course)\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.courseDone(course, id),
- destination: lesson(id)
- )
- }
- )
- }
-}
-
-struct ItSlides: View {
- @ObservedObject var vm: AppModel
- let course: String
- let id: Int
- let next: Route
-
- var body: some View {
- if let l = vm.content.itLesson(course, id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: next)
- }
- }
-}
-
-struct ChoiceLessonScreen: View {
- @ObservedObject var vm: AppModel
- let lesson: J?
- let onDone: () -> Void
-
- var body: some View {
- if let lesson {
- let spec = J([
- "type": "choice",
- "title": vm.tr(lesson.str("exTitleKey")),
- "sub": lesson.str("quizHeadKey"),
- "questions": lesson.rawArray("quiz"),
- ])
- DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey")), onDone: onDone)
- }
- }
-}
-
-struct TgQuizScreen: View {
- @ObservedObject var vm: AppModel
- let year: Int
- let id: Int
-
- var body: some View {
- let lesson: J? = {
- switch year {
- case 2: return vm.content.tg2Lesson(id)
- case 3: return vm.content.tg3Lesson(id)
- case 4: return vm.content.tg4Lesson(id)
- default: return vm.content.tgLesson(id)
- }
- }()
- if let lesson {
- let spec = J([
- "type": "choice",
- "title": vm.tr(lesson.str("exTitleKey")),
- "sub": lesson.str("quizHeadKey"),
- "questions": lesson.rawArray("quiz"),
- ])
- DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) {
- switch year {
- case 2: vm.markTg2(id)
- case 3: vm.markTg3(id)
- case 4: vm.markTg4(id)
- default: vm.markTg(id)
- }
- }
- }
- }
-}
-
-struct PgQuizScreen: View {
- @ObservedObject var vm: AppModel
- let year: Int
- let id: Int
-
- var body: some View {
- let lesson: J? = {
- switch year {
- case 2: return vm.content.pg2Lesson(id)
- case 3: return vm.content.pg3Lesson(id)
- case 4: return vm.content.pg4Lesson(id)
- default: return vm.content.pgLesson(id)
- }
- }()
- if let lesson {
- let spec = J([
- "type": "choice",
- "title": vm.tr(lesson.str("exTitleKey")),
- "sub": lesson.str("quizHeadKey"),
- "questions": lesson.rawArray("quiz"),
- ])
- DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) {
- switch year {
- case 2: vm.markPg2(id)
- case 3: vm.markPg3(id)
- case 4: vm.markPg4(id)
- default: vm.markPg(id)
- }
- }
- }
- }
-}
-
-struct SwQuizScreen: View {
- @ObservedObject var vm: AppModel
- let year: Int
- let id: Int
-
- var body: some View {
- let lesson: J? = {
- switch year {
- case 2: return vm.content.sw2Lesson(id)
- case 3: return vm.content.sw3Lesson(id)
- case 4: return vm.content.sw4Lesson(id)
- default: return vm.content.swLesson(id)
- }
- }()
- if let lesson {
- let spec = J([
- "type": "choice",
- "title": vm.tr(lesson.str("exTitleKey")),
- "sub": lesson.str("quizHeadKey"),
- "questions": lesson.rawArray("quiz"),
- ])
- DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) {
- switch year {
- case 2: vm.markSw2(id)
- case 3: vm.markSw3(id)
- case 4: vm.markSw4(id)
- default: vm.markSw(id)
- }
- }
- }
- }
-}
-
-struct FyzQuizScreen: View {
- @ObservedObject var vm: AppModel
- let year: Int
- let id: Int
-
- var body: some View {
- let lesson: J? = {
- switch year {
- case 2: return vm.content.fyz2Lesson(id)
- case 3: return vm.content.fyz3Lesson(id)
- case 4: return vm.content.fyz4Lesson(id)
- default: return vm.content.fyzLesson(id)
- }
- }()
- if let lesson {
- let spec = J([
- "type": "choice",
- "title": vm.tr(lesson.str("exTitleKey")),
- "sub": lesson.str("quizHeadKey"),
- "questions": lesson.rawArray("quiz"),
- ])
- DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) {
- switch year {
- case 2: vm.markFyz2(id)
- case 3: vm.markFyz3(id)
- case 4: vm.markFyz4(id)
- default: vm.markFyz(id)
- }
- }
- }
- }
-}
-
-struct LitYearQuizScreen: View {
- @ObservedObject var vm: AppModel
- let year: Int
- let id: Int
-
- var body: some View {
- let lesson: J? = {
- switch year {
- case 2: return vm.content.lit2Lesson(id)
- case 3: return vm.content.lit3Lesson(id)
- case 4: return vm.content.lit4Lesson(id)
- default: return vm.content.litLesson(id)
- }
- }()
- if let lesson {
- let spec = J([
- "type": "choice",
- "title": vm.tr(lesson.str("exTitleKey")),
- "sub": lesson.str("quizHeadKey"),
- "questions": lesson.rawArray("quiz"),
- ])
- DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) {
- switch year {
- case 2: vm.markLit2(id)
- case 3: vm.markLit3(id)
- case 4: vm.markLit4(id)
- default: vm.markLit(id)
- }
- }
- }
- }
-}
-
-struct OnQuizScreen: View {
- @ObservedObject var vm: AppModel
- let id: Int
-
- var body: some View {
- if let lesson = vm.content.onLesson(id) {
- let spec = J([
- "type": "choice",
- "title": vm.tr(lesson.str("exTitleKey")),
- "sub": lesson.str("quizHeadKey"),
- "questions": lesson.rawArray("quiz"),
- ])
- DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) {
- vm.markOn(id)
- }
- }
- }
-}
-
-struct On2QuizScreen: View {
- @ObservedObject var vm: AppModel
- let id: Int
-
- var body: some View {
- if let lesson = vm.content.on2Lesson(id) {
- let spec = J([
- "type": "choice",
- "title": vm.tr(lesson.str("exTitleKey")),
- "sub": lesson.str("quizHeadKey"),
- "questions": lesson.rawArray("quiz"),
- ])
- DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) {
- vm.markOn2(id)
- }
- }
- }
-}
-
-struct On3QuizScreen: View {
- @ObservedObject var vm: AppModel
- let id: Int
-
- var body: some View {
- if let lesson = vm.content.on3Lesson(id) {
- let spec = J([
- "type": "choice",
- "title": vm.tr(lesson.str("exTitleKey")),
- "sub": lesson.str("quizHeadKey"),
- "questions": lesson.rawArray("quiz"),
- ])
- DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) {
- vm.markOn3(id)
- }
- }
- }
-}
-
-struct On4QuizScreen: View {
- @ObservedObject var vm: AppModel
- let id: Int
-
- var body: some View {
- if let lesson = vm.content.on4Lesson(id) {
- let spec = J([
- "type": "choice",
- "title": vm.tr(lesson.str("exTitleKey")),
- "sub": lesson.str("quizHeadKey"),
- "questions": lesson.rawArray("quiz"),
- ])
- DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) {
- vm.markOn4(id)
- }
- }
- }
-}
-
-struct HwQuizScreen: View {
- @ObservedObject var vm: AppModel
- let id: Int
-
- var body: some View {
- if let lesson = vm.content.hwLesson(id) {
- let spec = J([
- "type": "choice",
- "title": vm.tr(lesson.str("exTitleKey")),
- "sub": lesson.str("quizHeadKey"),
- "questions": lesson.rawArray("quiz"),
- ])
- DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(lesson.str("exTitleKey"))) {
- vm.markHw(id)
- }
- }
- }
-}
-
-private struct VlsmRow {
- var pfx = ""
- var net = ""
- var bc = ""
- var lo = ""
- var hi = ""
-}
-
-private struct VlsmEx: View {
- @ObservedObject var vm: AppModel
- let lesson: J
- @State private var done: [Bool]
- @State private var rows: [[VlsmRow]]
- @State private var fb = ("", "")
-
- init(vm: AppModel, lesson: J) {
- self.vm = vm
- self.lesson = lesson
- let answers = vm.content.netAnswers
- _done = State(initialValue: Array(repeating: false, count: vm.content.netTasks.count))
- _rows = State(initialValue: answers.map { block in Array(repeating: VlsmRow(), count: block.count) })
- }
-
- var body: some View {
- let tasks = vm.content.netTasks
- let answers = vm.content.netAnswers
- let p = vm.palette
- ScrollView {
- VStack(alignment: .leading, spacing: 12) {
- ForEach(tasks.indices, id: \.self) { ti in
- let task = tasks[ti]
- let block = ti < answers.count ? answers[ti] : []
- let infeasible = block.isEmpty || block.allSatisfy { $0.int("pfx") <= 0 }
- GlassCard {
- Label {
- Text(task.str("prompt"))
- } icon: {
- Image(systemName: done[ti] ? "checkmark.circle.fill" : "\(ti + 1).circle")
- .foregroundStyle(done[ti] ? p.success : .secondary)
- }
- }
- .padding(.vertical, 4)
- if infeasible {
- Hint(text: vm.tr("net_infeasible"))
- } else {
- ForEach(block.indices, id: \.self) { si in
- Text("Podsíť \(String(UnicodeScalar(65 + si)!))")
- .font(.subheadline.weight(.semibold))
- .foregroundStyle(.secondary)
- .padding(.top, 6)
- PrefixMenu(value: Binding(
- get: { rows[ti][si].pfx },
- set: { rows[ti][si].pfx = $0 }
- ), palette: p)
- WordField(value: Binding(get: { rows[ti][si].net }, set: { rows[ti][si].net = $0 }), placeholder: "síť", palette: p)
- .keyboardType(.decimalPad)
- WordField(value: Binding(get: { rows[ti][si].bc }, set: { rows[ti][si].bc = $0 }), placeholder: "broadcast", palette: p)
- .keyboardType(.decimalPad)
- WordField(value: Binding(get: { rows[ti][si].lo }, set: { rows[ti][si].lo = $0 }), placeholder: "první uzel", palette: p)
- .keyboardType(.decimalPad)
- WordField(value: Binding(get: { rows[ti][si].hi }, set: { rows[ti][si].hi = $0 }), placeholder: "poslední uzel", palette: p)
- .keyboardType(.decimalPad)
- }
- PillButton(label: vm.tr("check")) {
- let ok = block.indices.allSatisfy { si in
- let a = block[si]
- let field = rows[ti][si]
- return Int(field.pfx.replacingOccurrences(of: "/", with: "")) == a.int("pfx")
- && netTxtEq(field.net, a.str("net"))
- && netTxtEq(field.bc, a.str("bcast"))
- && netTxtEq(field.lo, a.str("lo"))
- && netTxtEq(field.hi, a.str("hi"))
- }
- if ok {
- done[ti] = true
- fb = (vm.tr("feedback_ok"), "ok")
- maybeFinish()
- } else {
- fb = (vm.tr("feedback_retry"), "err")
- }
- }
- .padding(.vertical, 6)
- }
- PillButton(label: vm.tr("net_solution")) {
- if !infeasible {
- for si in block.indices {
- rows[ti][si].pfx = "/\(block[si].int("pfx"))"
- rows[ti][si].net = block[si].str("net")
- rows[ti][si].bc = block[si].str("bcast")
- rows[ti][si].lo = block[si].str("lo")
- rows[ti][si].hi = block[si].str("hi")
- }
- }
- done[ti] = true
- fb = ("", "")
- maybeFinish()
- }
- if done[ti] { Meaning(text: task.str("solution"), palette: p, visible: true) }
- Divider().padding(.vertical, 8)
- }
- }
- .exerciseContent()
- }
- .scrollDismissesKeyboard(.interactively)
- .detailScreen(vm.tr("net_ex_title"))
- .glassBottomBar {
- if !fb.0.isEmpty {
- FeedbackLine(text: fb.0, kind: fb.1, palette: p)
- .padding(.horizontal)
- .padding(.vertical, 8)
- .frame(maxWidth: .infinity, alignment: .leading)
- .transition(.opacity.combined(with: .scale(scale: 0.96, anchor: .leading)))
- }
- }
- .animation(Motion.feedback, value: fb.0)
- }
-
- private func maybeFinish() {
- if !done.isEmpty && done.allSatisfy({ $0 }) { vm.markNet(1) }
- }
-}
-
-private struct PrefixMenu: View {
- @Binding var value: String
- let palette: Palette
- @State private var open = false
-
- var body: some View {
- Button { open = true } label: {
- HStack {
- Text("Prefix").foregroundStyle(.secondary)
- Spacer()
- Text(value.isEmpty ? "/…" : value)
- .foregroundStyle(value.isEmpty ? Color.secondary : Color.primary)
- .monospacedDigit()
- Image(systemName: "chevron.up.chevron.down").font(.caption2).foregroundStyle(.secondary)
- }
- }
- .buttonStyle(.bordered)
- .tint(.secondary)
- .sheet(isPresented: $open) {
- WordPicker(palette: palette, pool: (8...30).map { "/\($0)" }) { word in
- if let word { value = word }
- open = false
- }
- .sheetDetents()
- }
- }
-}
-
-struct LitQuizScreen: View {
- @ObservedObject var vm: AppModel
- let id: String
- @State private var idx = 0
- @State private var score = 0
- @State private var answered = false
- @State private var finished = false
- @State private var picked = -1
-
- var body: some View {
- if let book = vm.content.book(id) {
- let qs = book.arr("quiz")
- let p = vm.palette
- ScrollView {
- VStack(alignment: .leading, spacing: 14) {
- if finished {
- GlassCard {
- VStack(alignment: .leading, spacing: 10) {
- Label(vm.tr("lit_finished"), systemImage: "rosette")
- #if os(macOS)
- .font(.largeTitle.weight(.bold))
- #else
- .font(.title2.weight(.bold))
- #endif
- Text(vm.tr("lit_finished_text"))
- #if os(macOS)
- .font(.title3)
- #endif
- .foregroundStyle(.secondary)
- Text(vm.fmt("lit_score_fmt", score, qs.count))
- #if os(macOS)
- .font(.title2.weight(.semibold))
- #else
- .font(.headline)
- #endif
- }
- }
- } else if idx < qs.count {
- let q = qs[idx]
- ProgressView(value: Double(idx + (answered ? 1 : 0)), total: Double(qs.count))
- HStack {
- Text(vm.fmt("lit_question_fmt", idx + 1, qs.count))
- Spacer()
- Text(vm.fmt("lit_score_fmt", score, qs.count))
- }
- #if os(macOS)
- .font(.body.weight(.semibold))
- #else
- .font(.footnote)
- #endif
- .foregroundStyle(.secondary)
- Text(q.str("prompt"))
- #if os(macOS)
- .font(.title.weight(.semibold))
- #else
- .font(.title3.weight(.semibold))
- #endif
- .padding(.vertical, 12)
- #if os(macOS)
- LazyVGrid(
- columns: [
- GridItem(.flexible(), spacing: 12, alignment: .top),
- GridItem(.flexible(), spacing: 12, alignment: .top),
- ],
- spacing: 8
- ) {
- bookQuizOptions(q)
- }
- #else
- bookQuizOptions(q)
- #endif
- if answered {
- Meaning(text: q.str("expl"), palette: p, visible: true)
- }
- }
- }
- .exerciseContent()
- }
- .detailScreen(vm.tr(book.str("quizTitle")), subtitle: vm.tr(book.str("quizSub")))
- .glassBottomBar {
- if finished {
- BottomAction(label: vm.tr("lit_again"), action: {
- idx = 0; score = 0; answered = false; finished = false; picked = -1
- }) { EmptyView() }
- } else if answered {
- BottomAction(label: idx + 1 >= qs.count ? vm.tr("lit_show_result") : vm.tr("lit_next"), action: {
- if idx + 1 >= qs.count {
- finished = true
- vm.markBookQuiz(id)
- } else {
- idx += 1
- answered = false
- picked = -1
- }
- }) { EmptyView() }
- }
- }
- .animation(Motion.settle, value: answered)
- }
- }
-
- @ViewBuilder
- private func bookQuizOptions(_ q: J) -> some View {
- ForEach(Array(q.strs("options").enumerated()), id: \.offset) { i, opt in
- let correct = i == q.int("correct")
- let chosen = answered && picked == i
- OptionChip(
- text: opt,
- selected: chosen,
- mark: answered ? (correct ? true : (chosen ? false : nil)) : nil
- ) {
- guard !answered else { return }
- picked = i
- answered = true
- if correct { score += 1 }
- }
- }
- }
-}
-
-struct PlotScreen: View {
- @ObservedObject var vm: AppModel
- let id: String
-
- var body: some View {
- if let book = vm.content.book(id) {
- let spec = J([
- "type": "assembly",
- "title": vm.tr(book.str("plotTitle")),
- "sub": book.str("plotSub"),
- "items": book.rawArray("plot"),
- "meanings": book.rawArray("plotMeaning"),
- ])
- DispatchExercise(vm: vm, spec: spec, titleFallback: vm.tr(book.str("plotTitle"))) {
- vm.markBookPlot(id)
- }
- }
- }
-}
-
-private final class EnSpeaker: NSObject, ObservableObject, AVSpeechSynthesizerDelegate, @unchecked Sendable {
- enum Phase { case idle, playing, paused }
-
- private let synth = AVSpeechSynthesizer()
- private var text = ""
- private var base = 0
- private var cursor = 0
- private var restarting = false
- private var settingsDirty = false
-
- @Published var rate: Float = 0.9
- @Published var volume: Float = 1
- @Published var phase: Phase = .idle
- var language = "en-GB"
- var onFinish: (() -> Void)?
-
- override init() {
- super.init()
- synth.delegate = self
- }
-
- func toggle(_ full: String) {
- if text != full {
- text = full
- cursor = 0
- base = 0
- synth.stopSpeaking(at: .immediate)
- phase = .idle
- }
- switch phase {
- case .idle:
- cursor = 0
- start(from: 0)
- case .playing:
- synth.pauseSpeaking(at: .immediate)
- phase = .paused
- case .paused:
- if settingsDirty || !synth.isPaused {
- start(from: cursor)
- } else {
- synth.continueSpeaking()
- phase = .playing
- }
- settingsDirty = false
- }
- }
-
- func applySettings() {
- settingsDirty = true
- guard phase == .playing else { return }
- restarting = true
- synth.stopSpeaking(at: .immediate)
- start(from: cursor)
- settingsDirty = false
- }
-
- private func start(from index: Int) {
- let ns = text as NSString
- let start = min(max(0, index), ns.length)
- base = start
- let rest = ns.substring(from: start)
- let utterance = AVSpeechUtterance(string: rest.isEmpty ? text : rest)
- if rest.isEmpty { base = 0 }
- utterance.voice = AVSpeechSynthesisVoice(language: language)
- ?? AVSpeechSynthesisVoice(language: language.hasPrefix("de") ? "de-DE" : "en-US")
- utterance.rate = AVSpeechUtteranceDefaultSpeechRate * rate
- utterance.volume = volume
- synth.speak(utterance)
- phase = .playing
- }
-
- func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) {
- cursor = base + characterRange.location
- }
-
- func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
- DispatchQueue.main.async {
- self.phase = .idle
- self.cursor = 0
- self.onFinish?()
- }
- }
-
- func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
- if restarting {
- restarting = false
- return
- }
- DispatchQueue.main.async {
- if self.phase == .playing { self.phase = .idle }
- }
- }
-}
-
-struct EngExScreen: View {
- @ObservedObject var vm: AppModel
- let year: Int
- let id: Int
- let voice: String
- let lessonOf: (AppModel, Int, Int) -> J?
- let mark: (AppModel, Int, Int) -> Void
- @StateObject private var speaker: EnSpeaker
- @State private var readPicks: [Int]
- @State private var listenPicks: [Int]
- @State private var quizPicks: [Int]
- @State private var gaps: [String]
- @State private var essay = ""
- @State private var played = false
- @State private var revealed = false
- @State private var showModel = false
- @State private var fb = ("", "")
-
- init(vm: AppModel, year: Int, id: Int, voice: String = "en-GB",
- lesson: @escaping (AppModel, Int, Int) -> J? = { $0.content.enLesson($1, $2) },
- mark: @escaping (AppModel, Int, Int) -> Void = { $0.markEn($1, $2) }) {
- self.vm = vm
- self.year = year
- self.id = id
- self.voice = voice
- self.lessonOf = lesson
- self.mark = mark
- let row = lesson(vm, year, id)
- _readPicks = State(initialValue: Array(repeating: -1, count: row?.arr("readQuiz").count ?? 0))
- _listenPicks = State(initialValue: Array(repeating: -1, count: row?.arr("listenQuiz").count ?? 0))
- _quizPicks = State(initialValue: Array(repeating: -1, count: row?.arr("quiz").count ?? 0))
- _gaps = State(initialValue: Array(repeating: "", count: row?.arr("gaps").count ?? 0))
- let speaker = EnSpeaker()
- speaker.language = voice
- _speaker = StateObject(wrappedValue: speaker)
- }
-
- var body: some View {
- if let lesson = lessonOf(vm, year, id) {
- let listening = lesson.strOrNull("listening")
- let pinListen = listening != nil && lesson.strOrNull("reading") == nil
- if pinListen, let listening {
- scaffold(lesson) {
- exerciseBody(lesson, inlineListening: nil)
- }
- .safeAreaInset(edge: .top, spacing: 0) {
- listenBlock(listening)
- .padding(.horizontal, pagePad)
- .padding(.top, 4)
- .padding(.bottom, 8)
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(.background)
- }
- } else {
- scaffold(lesson) {
- exerciseBody(lesson, inlineListening: listening)
- }
- }
- }
- }
-
- private func scaffold(_ lesson: J, @ViewBuilder content: @escaping () -> Content) -> some View {
- ExerciseScaffold(
- vm: vm,
- title: vm.tr(lesson.str("titleKey")),
- sub: nil,
- action: vm.tr("check"),
- onAction: { check(lesson) },
- feedback: fb.0,
- kind: fb.1,
- content: content
- )
- }
-
- @ViewBuilder
- private func exerciseBody(_ lesson: J, inlineListening: String?) -> some View {
- let p = vm.palette
- if let reading = lesson.strOrNull("reading") {
- section("en_sec_read")
- Text(reading).textSelection(.enabled)
- choiceBlock(lesson.arr("readQuiz"), picks: $readPicks)
- }
- if let inlineListening {
- listenBlock(inlineListening)
- choiceBlock(lesson.arr("listenQuiz"), picks: $listenPicks)
- } else if lesson.strOrNull("listening") != nil {
- choiceBlock(lesson.arr("listenQuiz"), picks: $listenPicks)
- }
- let gapRows = lesson.arr("gaps")
- if !gapRows.isEmpty {
- section("en_sec_gap")
- ForEach(gapRows.indices, id: \.self) { i in
- Text("\(i + 1). \(gapRows[i].str("prompt"))")
- .font(.body.weight(.semibold))
- WordField(
- value: Binding(get: { gaps[i] }, set: { gaps[i] = $0 }),
- placeholder: "",
- mark: revealed ? answerAccepts(normalizeAnswer(gaps[i]), gapRows[i].str("answers")) : nil,
- palette: p
- )
- if revealed, let hint = gapRows[i].strOrNull("meaning") {
- Meaning(text: hint, palette: p, visible: true)
- }
- }
- }
- if let writing = lesson.obj("writing") {
- section("en_sec_write")
- Text(writing.str("prompt"))
- TextEditor(text: $essay)
- .frame(minHeight: 140)
- .padding(8)
- .background(Color.primary.opacity(0.04), in: RoundedRectangle(cornerRadius: 12))
- if showModel {
- Text(vm.tr("en_model"))
- .font(.subheadline.weight(.semibold))
- Text(writing.str("model")).textSelection(.enabled)
- }
- }
- let quiz = lesson.arr("quiz")
- if !quiz.isEmpty {
- section("en_sec_quiz")
- choiceBlock(quiz, picks: $quizPicks)
- }
- }
-
- private var pagePad: CGFloat {
- #if os(macOS)
- MacChrome.pageInset
- #else
- 16
- #endif
- }
-
- private func listenBlock(_ text: String) -> some View {
- VStack(alignment: .leading, spacing: 8) {
- section("en_sec_listen")
- Text(vm.tr("en_listen_note"))
- .font(.subheadline)
- .foregroundStyle(.secondary)
- GlassCard {
- #if os(macOS)
- HStack(alignment: .top, spacing: 22) {
- listenControls(text)
- listenScript(text)
- }
- #else
- VStack(alignment: .leading, spacing: 14) {
- listenScript(text)
- listenControls(text)
- }
- #endif
- }
- }
- }
-
- private func listenScript(_ text: String) -> some View {
- Text(text)
- .font(.body)
- .foregroundStyle(.primary)
- .multilineTextAlignment(.leading)
- .textSelection(.enabled)
- .frame(maxWidth: .infinity, alignment: .leading)
- }
-
- private func section(_ key: String) -> some View {
- Text(vm.tr(key))
- .font(.title3.weight(.bold))
- .padding(.top, 8)
- }
-
- private func listenControls(_ text: String) -> some View {
- let label: String = switch speaker.phase {
- case .playing: vm.tr("en_pause")
- case .paused: vm.tr("en_resume")
- case .idle: vm.tr("en_play")
- }
- return VStack(alignment: .leading, spacing: 12) {
- Button(label) {
- played = true
- speaker.toggle(text)
- }
- .buttonStyle(.borderedProminent)
- .controlSize(.large)
- .frame(maxWidth: .infinity)
- listenSlider(vm.tr("en_speed"), value: Double(speaker.rate), range: 0.6...1.5, caption: String(format: "%.1f×", speaker.rate)) { next in
- speaker.rate = Float(next)
- }
- listenSlider(vm.tr("en_volume"), value: Double(speaker.volume), range: 0...1, caption: "\(Int(speaker.volume * 100)) %") { next in
- speaker.volume = Float(next)
- }
- }
- .frame(width: 228)
- }
-
- private func listenSlider(_ title: String, value: Double, range: ClosedRange, caption: String, set: @escaping (Double) -> Void) -> some View {
- VStack(alignment: .leading, spacing: 2) {
- Text(title)
- .font(.caption.weight(.semibold))
- .foregroundStyle(.secondary)
- HStack(spacing: 8) {
- Slider(
- value: Binding(get: { value }, set: set),
- in: range,
- onEditingChanged: { editing in
- if !editing { speaker.applySettings() }
- }
- )
- .controlSize(.small)
- Text(caption)
- .font(.caption.weight(.semibold))
- .monospacedDigit()
- .foregroundStyle(.secondary)
- .frame(width: 46, alignment: .trailing)
- }
- }
- }
-
- private func choiceBlock(_ questions: [J], picks: Binding<[Int]>) -> some View {
- let p = vm.palette
- return ForEach(questions.indices, id: \.self) { i in
- let q = questions[i]
- VStack(alignment: .leading, spacing: 6) {
- Text("\(i + 1). \(q.str("prompt"))")
- .font(.body.weight(.semibold))
- ForEach(Array(q.strs("options").enumerated()), id: \.offset) { o, opt in
- let selected = picks.wrappedValue[i] == o
- let mark: Bool? = revealed && selected ? picks.wrappedValue[i] == q.int("correct") : nil
- OptionChip(text: opt, selected: selected, mark: mark) {
- picks.wrappedValue[i] = o
- }
- }
- if revealed, let expl = q.strOrNull("expl") {
- Meaning(text: expl, palette: p, visible: true)
- }
- }
- .padding(.bottom, 8)
- }
- }
-
- private func check(_ lesson: J) {
- if lesson.strOrNull("listening") != nil && !played {
- fb = (vm.tr("en_listen_first"), "err")
- return
- }
- let readOk = score(lesson.arr("readQuiz"), readPicks)
- let listenOk = score(lesson.arr("listenQuiz"), listenPicks)
- let quizOk = score(lesson.arr("quiz"), quizPicks)
- let gapRows = lesson.arr("gaps")
- let gapOk = gapRows.indices.filter {
- answerAccepts(normalizeAnswer(gaps[$0]), gapRows[$0].str("answers"))
- }.count
- var writeOk = true
- if let writing = lesson.obj("writing") {
- let keys = writing.str("keys").split(separator: "|").map(String.init)
- let enough = wordCount(essay) >= writing.int("minWords", 20)
- writeOk = enough && keys.allSatisfy { hasWord(essay, $0) }
- }
- revealed = true
- let all = readOk && listenOk && quizOk && gapOk == gapRows.count && writeOk
- if all {
- fb = (vm.tr("feedback_ok"), "ok")
- showModel = true
- mark(vm, year, id)
- } else if lesson.obj("writing") != nil && !writeOk && readOk && listenOk && quizOk && gapOk == gapRows.count {
- fb = (vm.tr("en_write_short"), "err")
- } else {
- fb = (vm.tr("feedback_retry_short"), "err")
- }
- }
-
- private func score(_ questions: [J], _ picks: [Int]) -> Bool {
- questions.indices.allSatisfy { picks[$0] == questions[$0].int("correct") }
- }
-
- private func wordCount(_ text: String) -> Int {
- let n = normalizeAnswer(text)
- return n.isEmpty ? 0 : n.split(separator: " ").count
- }
-
- private func hasWord(_ text: String, _ word: String) -> Bool {
- let n = " \(normalizeAnswer(text)) "
- let w = normalizeAnswer(word)
- return !w.isEmpty && n.contains(" \(w) ")
- }
-}
-
diff --git a/ios/Graduately/UI/Icons.swift b/ios/Graduately/UI/Icons.swift
deleted file mode 100644
index 6434277..0000000
--- a/ios/Graduately/UI/Icons.swift
+++ /dev/null
@@ -1,59 +0,0 @@
-import SwiftUI
-
-// Circular flags used as subject glyphs; everything else is an SF Symbol.
-
-struct GermanFlag: View {
- var size: CGFloat = 48
- var body: some View {
- Canvas { ctx, sz in
- let band = sz.height / 3
- ctx.clip(to: Path(ellipseIn: CGRect(origin: .zero, size: sz)))
- ctx.fill(Path(CGRect(x: 0, y: 0, width: sz.width, height: band)), with: .color(Color(red: 0.18, green: 0.18, blue: 0.19)))
- ctx.fill(Path(CGRect(x: 0, y: band, width: sz.width, height: band)), with: .color(Color(red: 0.72, green: 0.27, blue: 0.24)))
- ctx.fill(Path(CGRect(x: 0, y: 2 * band, width: sz.width, height: band)), with: .color(Color(red: 0.84, green: 0.70, blue: 0.32)))
- }
- .frame(width: size, height: size)
- }
-}
-
-struct CzechFlag: View {
- var size: CGFloat = 48
- var body: some View {
- Canvas { ctx, sz in
- ctx.clip(to: Path(ellipseIn: CGRect(origin: .zero, size: sz)))
- ctx.fill(Path(CGRect(origin: .zero, size: sz)), with: .color(Color(white: 0.98)))
- ctx.fill(Path(CGRect(x: 0, y: sz.height / 2, width: sz.width, height: sz.height / 2)), with: .color(Color(red: 0.83, green: 0.16, blue: 0.18)))
- var tri = Path()
- tri.move(to: .zero)
- tri.addLine(to: CGPoint(x: sz.width * 0.55, y: sz.height / 2))
- tri.addLine(to: CGPoint(x: 0, y: sz.height))
- tri.closeSubpath()
- ctx.fill(tri, with: .color(Color(red: 0.07, green: 0.24, blue: 0.55)))
- }
- .frame(width: size, height: size)
- }
-}
-
-struct UkFlag: View {
- var size: CGFloat = 48
- var body: some View {
- Canvas { ctx, sz in
- let w = sz.width
- let h = sz.height
- ctx.clip(to: Path(ellipseIn: CGRect(origin: .zero, size: sz)))
- ctx.fill(Path(CGRect(origin: .zero, size: sz)), with: .color(Color(red: 0.01, green: 0.14, blue: 0.40)))
- var saltire = Path()
- saltire.move(to: .zero)
- saltire.addLine(to: CGPoint(x: w, y: h))
- saltire.move(to: CGPoint(x: w, y: 0))
- saltire.addLine(to: CGPoint(x: 0, y: h))
- ctx.stroke(saltire, with: .color(.white), lineWidth: w * 0.16)
- ctx.stroke(saltire, with: .color(Color(red: 0.80, green: 0.05, blue: 0.15)), lineWidth: w * 0.055)
- ctx.fill(Path(CGRect(x: 0, y: h * 0.38, width: w, height: h * 0.24)), with: .color(.white))
- ctx.fill(Path(CGRect(x: w * 0.38, y: 0, width: w * 0.24, height: h)), with: .color(.white))
- ctx.fill(Path(CGRect(x: 0, y: h * 0.43, width: w, height: h * 0.14)), with: .color(Color(red: 0.80, green: 0.05, blue: 0.15)))
- ctx.fill(Path(CGRect(x: w * 0.43, y: 0, width: w * 0.14, height: h)), with: .color(Color(red: 0.80, green: 0.05, blue: 0.15)))
- }
- .frame(width: size, height: size)
- }
-}
diff --git a/ios/Graduately/UI/PathMap.swift b/ios/Graduately/UI/PathMap.swift
deleted file mode 100644
index 5c7e4e4..0000000
--- a/ios/Graduately/UI/PathMap.swift
+++ /dev/null
@@ -1,376 +0,0 @@
-import SwiftUI
-
-struct MapNode: Identifiable {
- let id: String
- let label: String
- let locked: Bool
- let done: Bool
- let current: Bool
- var finish: Bool = false
- var destination: Route?
- /// Glyph for a node that has no number, such as a side branch.
- var symbol: String? = nil
-}
-
-private struct NodePos {
- var x: CGFloat
- var y: CGFloat
- var row: Int
-}
-
-private struct Serpentine {
- let points: [NodePos]
- let width: CGFloat
- let height: CGFloat
- let spacing: CGFloat
-}
-
-/// Keep enough room under/above each node for a two-line caption.
-#if os(macOS)
-private let labelBand: CGFloat = 56
-#else
-private let labelBand: CGFloat = 44
-#endif
-
-private func serpentine(
- count n: Int,
- avail: CGFloat,
- mx: CGFloat,
- my: CGFloat,
- spac: CGFloat,
- gap: CGFloat,
- wave: CGFloat,
- nodeSize: CGFloat,
- lift: CGFloat = 0
-) -> Serpentine {
- guard n > 0 else {
- return Serpentine(points: [], width: avail, height: my * 2, spacing: spac)
- }
-
- // Pick the fewest rows that fit the preferred spacing in the available width.
- // Do NOT pre-scale as if every node were on one row — that crushed tall maps.
- var rows = 1
- var cols = n
- while rows <= n {
- cols = max(1, (n + rows - 1) / rows)
- if 2 * mx + CGFloat(cols - 1) * spac <= avail + 1 { break }
- rows += 1
- }
- rows = min(rows, n)
- cols = max(1, (n + rows - 1) / rows)
-
- let needed = 2 * mx + CGFloat(cols - 1) * spac
- let scale = needed > avail ? max(0.72, avail / needed) : 1
-
- let smx = mx * scale
- let smy = max(my * scale, nodeSize / 2 + labelBand + 8)
- let sspac = max(spac * scale, nodeSize + 36)
- // Gap must clear labels that sit between rows (outward placement still
- // needs space when wave lifts nodes toward each other).
- let sgap = max(gap * scale, nodeSize + labelBand * 2 + 28)
- let swave = min(wave * scale, sgap * 0.12)
- let slift = lift * scale
-
- let cw = 2 * smx + CGFloat(cols - 1) * sspac
- let ch = 2 * smy + CGFloat(rows - 1) * sgap + slift + labelBand
- let phase = CGFloat(2 * Double.pi * 1.7 / Double(max(n - 1, 1)))
-
- var pts = Array(repeating: NodePos(x: 0, y: 0, row: 0), count: n)
- for r in 0.. NodePos {
- let n = pts.count
- guard n > 1 else { return pts.first ?? NodePos(x: 0, y: 0, row: 0) }
- var k = Int(t)
- var u = t - CGFloat(k)
- if k < 0 { k = 0; u = 0 }
- if k >= n - 1 { k = n - 2; u = 1 }
- let p1 = pts[k]
- let p2 = pts[k + 1]
- let p0 = k - 1 >= 0
- ? pts[k - 1]
- : NodePos(x: p1.x - (p2.x - p1.x), y: p1.y - (p2.y - p1.y), row: p1.row)
- let p3 = k + 2 < n
- ? pts[k + 2]
- : NodePos(x: p2.x + (p2.x - p1.x), y: p2.y + (p2.y - p1.y), row: p2.row)
- let u2 = u * u
- let u3 = u2 * u
- let x = 0.5 * (2 * p1.x + (-p0.x + p2.x) * u
- + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * u2
- + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * u3)
- let y = 0.5 * (2 * p1.y + (-p0.y + p2.y) * u
- + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * u2
- + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * u3)
- return NodePos(x: x, y: y, row: p1.row)
-}
-
-struct PathMap: View {
- let nodes: [MapNode]
- let palette: Palette
- var litUntil: CGFloat = 0
- var nodeSize: CGFloat = 64
- var mx: CGFloat = 90
- var my: CGFloat = 110
- var spac: CGFloat = 170
- var gap: CGFloat = 220
- var wave: CGFloat = 18
- var branch: (index: Int, node: MapNode)? = nil
-
- var body: some View {
- GeometryReader { geo in
- let layout = serpentine(
- count: nodes.count,
- avail: max(geo.size.width - 24, 480),
- mx: mx,
- my: my,
- spac: spac,
- gap: gap,
- wave: wave,
- nodeSize: nodeSize,
- lift: branch == nil ? 0 : 56
- )
- let labelWidth = min(140, max(88, layout.spacing - 16))
- let canvasH = layout.height + labelBand + 24
- ScrollView([.horizontal, .vertical]) {
- ZStack(alignment: .topLeading) {
- railCanvas(points: layout.points)
- .frame(width: layout.width, height: canvasH)
-
- ForEach(Array(nodes.enumerated()), id: \.element.id) { i, node in
- let pt = layout.points[i]
- let labelBelow = pt.row % 2 == 0
- nodeView(node, index: i)
- .position(x: pt.x, y: pt.y)
- Text(node.label)
- #if os(macOS)
- .font(.callout.weight(.semibold))
- #else
- .font(.caption2.weight(.semibold))
- #endif
- .foregroundStyle(node.locked ? palette.overlay : palette.text)
- .multilineTextAlignment(.center)
- .lineLimit(2)
- .minimumScaleFactor(0.85)
- .frame(width: labelWidth, height: labelBand, alignment: labelBelow ? .top : .bottom)
- .position(
- x: pt.x,
- y: labelBelow
- ? pt.y + nodeSize / 2 + labelBand / 2 + 6
- : pt.y - nodeSize / 2 - labelBand / 2 - 6
- )
- }
-
- if let branch, branch.index < layout.points.count {
- let base = layout.points[branch.index]
- let tip = NodePos(x: base.x + 64, y: base.y - 128, row: base.row)
- Path { p in
- p.move(to: CGPoint(x: base.x, y: base.y - nodeSize / 2))
- p.addQuadCurve(
- to: CGPoint(x: tip.x, y: tip.y + nodeSize / 2),
- control: CGPoint(x: base.x + 14, y: tip.y + 28)
- )
- }
- .stroke(palette.rail, style: StrokeStyle(lineWidth: 10, lineCap: .round))
- nodeView(branch.node, index: nil)
- .position(x: tip.x, y: tip.y)
- Text(branch.node.label)
- #if os(macOS)
- .font(.callout.weight(.semibold))
- #else
- .font(.caption2.weight(.semibold))
- #endif
- .foregroundStyle(palette.text)
- .multilineTextAlignment(.center)
- .lineLimit(2)
- .frame(width: labelWidth, height: labelBand, alignment: .bottom)
- .position(x: tip.x, y: tip.y - nodeSize / 2 - labelBand / 2 - 6)
- }
- }
- .frame(width: layout.width + 24, height: canvasH, alignment: .topLeading)
- .frame(
- minWidth: geo.size.width,
- minHeight: max(canvasH, geo.size.height),
- alignment: Alignment(
- horizontal: layout.width + 24 < geo.size.width ? .center : .leading,
- vertical: canvasH < geo.size.height ? .center : .top
- )
- )
- }
- }
- #if os(macOS)
- .frame(minHeight: 520)
- #else
- .frame(minHeight: 420)
- #endif
- .softAppear()
- }
-
- @ViewBuilder
- private func railCanvas(points: [NodePos]) -> some View {
- Canvas { ctx, _ in
- guard points.count >= 2 else { return }
- func stroke(_ t0: CGFloat, _ t1: CGFloat, color: Color, width: CGFloat, dashed: Bool = false) {
- var path = Path()
- let steps = max(1, Int((t1 - t0) * 48))
- let start = catmull(points, t0)
- path.move(to: CGPoint(x: start.x, y: start.y))
- for s in 1...steps {
- let t = t0 + (t1 - t0) * CGFloat(s) / CGFloat(steps)
- let pt = catmull(points, t)
- path.addLine(to: CGPoint(x: pt.x, y: pt.y))
- }
- var style = StrokeStyle(lineWidth: width, lineCap: .round, lineJoin: .round)
- if dashed { style.dash = [1, 18] }
- ctx.stroke(path, with: .color(color), style: style)
- }
- let end = CGFloat(points.count - 1)
- stroke(0, end, color: palette.rail, width: 10)
- if litUntil > 0 {
- stroke(0, min(litUntil, end), color: palette.accent, width: 10)
- }
- let mutedStart = litUntil > 0 ? litUntil : 0
- if mutedStart < end {
- stroke(mutedStart, end, color: palette.subtext.opacity(0.22), width: 10, dashed: true)
- }
- }
- }
-
- @ViewBuilder
- private func nodeView(_ node: MapNode, index: Int?) -> some View {
- let content = ZStack {
- Circle()
- .fill(nodeFill(node))
- .overlay {
- if node.locked {
- Circle().strokeBorder(palette.lockedBorder, lineWidth: 1)
- } else if !node.current && !node.done {
- Circle().strokeBorder(palette.accent.opacity(0.55), lineWidth: 2.5)
- }
- }
- if node.finish {
- VStack(spacing: 2) {
- if let index {
- Text("\(index + 1)")
- #if os(macOS)
- .font(.title2.weight(.black))
- #else
- .font(.headline.weight(.black))
- #endif
- }
- FinishFlagBadge(palette: palette)
- }
- .foregroundStyle(palette.overlay)
- } else if node.locked {
- VStack(spacing: 2) {
- if let index {
- Text("\(index + 1)")
- #if os(macOS)
- .font(.title2.weight(.black))
- #else
- .font(.headline.weight(.black))
- #endif
- }
- Image(systemName: "lock.fill")
- #if os(macOS)
- .font(.caption.weight(.bold))
- #else
- .font(.caption2.weight(.bold))
- #endif
- }
- .foregroundStyle(palette.overlay)
- } else if node.done {
- VStack(spacing: 2) {
- if let index {
- Text("\(index + 1)")
- #if os(macOS)
- .font(.title2.weight(.black))
- #else
- .font(.headline.weight(.black))
- #endif
- } else if let symbol = node.symbol {
- Image(systemName: symbol)
- #if os(macOS)
- .font(.title3.weight(.semibold))
- #else
- .font(.body.weight(.semibold))
- #endif
- }
- Image(systemName: "checkmark")
- #if os(macOS)
- .font(.caption.weight(.bold))
- #else
- .font(.caption2.weight(.bold))
- #endif
- }
- .foregroundStyle(palette.onAccent)
- } else if let index {
- Text("\(index + 1)")
- #if os(macOS)
- .font(.title2.weight(.black))
- #else
- .font(.headline.weight(.black))
- #endif
- .foregroundStyle(node.current ? palette.onAccent : palette.text)
- } else if let symbol = node.symbol {
- Image(systemName: symbol)
- #if os(macOS)
- .font(.title2.weight(.semibold))
- #else
- .font(.headline.weight(.semibold))
- #endif
- .foregroundStyle(node.current ? palette.onAccent : palette.text)
- }
- }
- .frame(width: nodeSize, height: nodeSize)
- .shadow(color: .black.opacity(node.locked ? 0 : 0.12), radius: 6, y: 2)
-
- if let dest = node.destination, !node.locked {
- NavigationLink(value: dest) { content }
- .buttonStyle(PressScaleStyle())
- } else {
- content.opacity(node.locked ? 0.55 : 1)
- }
- }
-
- private func nodeFill(_ node: MapNode) -> Color {
- if node.locked { return palette.lockedBg }
- if node.current || node.done { return palette.accent }
- return palette.node
- }
-}
-
-private struct FinishFlagBadge: View {
- let palette: Palette
-
- var body: some View {
- let cell: CGFloat = 4
- Canvas { ctx, _ in
- for iy in 0..<2 {
- for ix in 0..<3 {
- let dark = ((ix + iy) & 1) == 1
- let rect = CGRect(x: CGFloat(ix) * cell, y: CGFloat(iy) * cell, width: cell, height: cell)
- ctx.fill(
- Path(rect),
- with: .color(dark ? palette.finishLight : palette.finishStroke)
- )
- }
- }
- }
- .frame(width: cell * 3, height: cell * 2)
- }
-}
diff --git a/ios/Graduately/UI/Screens.swift b/ios/Graduately/UI/Screens.swift
deleted file mode 100644
index d4535b2..0000000
--- a/ios/Graduately/UI/Screens.swift
+++ /dev/null
@@ -1,3918 +0,0 @@
-import SwiftUI
-#if os(macOS)
-import AppKit
-#endif
-
-struct PracticeHome: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- macHome.softAppear()
- #else
- iosHome.softAppear()
- #endif
- }
-
- #if os(macOS)
- private var macHome: some View {
- let sum = summarize(vm.content, vm.progress)
- let pct = sum.totalEx == 0 ? 0 : Double(sum.doneEx) / Double(sum.totalEx)
- let open = vm.content.subjects.filter { $0.bool("open") }
- let soon = vm.content.subjects.filter { !$0.bool("open") }
- return ScrollView {
- VStack(alignment: .leading, spacing: 28) {
- if vm.showChangelog {
- ChangelogCard(vm: vm)
- }
- welcomeCard(sum: sum, pct: pct)
- VStack(alignment: .leading, spacing: 14) {
- Text(vm.tr("subjects_title"))
- .font(.title2.weight(.semibold))
- LazyVGrid(
- columns: [GridItem(.adaptive(minimum: MacChrome.homeTileMin), spacing: 16, alignment: .top)],
- alignment: .leading,
- spacing: 16
- ) {
- ForEach(Array(open.enumerated()), id: \.offset) { _, s in
- subjectTile(s)
- }
- }
- }
- if !soon.isEmpty {
- VStack(alignment: .leading, spacing: 12) {
- Text(vm.tr("subjects_soon"))
- .font(.headline)
- .foregroundStyle(.secondary)
- FlowLayout(spacing: 10) {
- ForEach(Array(soon.enumerated()), id: \.offset) { _, s in
- Label(vm.tr(s.str("key")), systemImage: "lock.fill")
- .font(.body)
- .foregroundStyle(.secondary)
- .padding(.horizontal, 14)
- .padding(.vertical, 9)
- .background(Color.primary.opacity(0.05), in: Capsule())
- }
- }
- }
- }
- }
- .padding(MacChrome.pageInset)
- .macPage()
- }
- .navigationTitle("Graduately")
- }
-
- private func welcomeCard(sum: ProgressSum, pct: Double) -> some View {
- GlassCard {
- HStack(alignment: .center, spacing: 32) {
- VStack(alignment: .leading, spacing: 12) {
- Text(vm.tr("welcome_title"))
- .font(.system(size: 34, weight: .bold))
- .fixedSize(horizontal: false, vertical: true)
- Text(vm.tr(Self.welcomeBodyKey))
- .font(.title3)
- .foregroundStyle(.secondary)
- .fixedSize(horizontal: false, vertical: true)
- }
- .frame(maxWidth: .infinity, alignment: .leading)
- ProgressRing(value: pct) {
- VStack(spacing: 2) {
- Text(vm.fmt("stats_pct_fmt", Int((pct * 100).rounded(.down))))
- .font(.system(size: 26, weight: .bold, design: .rounded))
- .monospacedDigit()
- Text(vm.fmt("stats_ex_fmt", sum.doneEx, sum.totalEx))
- .font(.subheadline.monospacedDigit())
- .foregroundStyle(.secondary)
- }
- }
- .frame(width: 132, height: 132)
- }
- .padding(.vertical, 6)
- }
- }
-
- @ViewBuilder
- private func subjectTile(_ s: J) -> some View {
- let dest = s.strOrNull("target").flatMap(routeFromPage)
- if let dest, dest != .subjects, dest != .welcome {
- NavigationLink(value: dest) {
- subjectTileBody(s)
- }
- .buttonStyle(PressScaleStyle())
- } else {
- subjectTileBody(s)
- }
- }
-
- private func subjectTileBody(_ s: J) -> some View {
- let part = subjectSum(vm, s)
- let progress = part.totalEx > 0 ? Double(part.doneEx) / Double(part.totalEx) : 0
- return VStack(alignment: .leading, spacing: 14) {
- HStack(alignment: .center) {
- SubjectIcon(icon: s.str("icon"), open: true, size: 40)
- Spacer(minLength: 0)
- Image(systemName: "chevron.right")
- .font(.body.weight(.semibold))
- .foregroundStyle(.tertiary)
- }
- Text(vm.tr(s.str("key")))
- .font(.title3.weight(.semibold))
- .foregroundStyle(.primary)
- .multilineTextAlignment(.leading)
- .lineLimit(2)
- Spacer(minLength: 0)
- ProgressBar(value: progress)
- HStack {
- Text(vm.fmt("stats_ex_fmt", part.doneEx, part.totalEx))
- Spacer(minLength: 0)
- Text(vm.fmt("stats_pct_fmt", Int((progress * 100).rounded(.down))))
- }
- .font(.subheadline.monospacedDigit())
- .foregroundStyle(.secondary)
- }
- .padding(20)
- .frame(maxWidth: .infinity, alignment: .topLeading)
- .frame(height: MacChrome.homeTileHeight)
- .softSurface(cornerRadius: 22)
- .contentShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
- }
- #endif
-
- #if os(iOS)
- private var iosHome: some View {
- let sum = summarize(vm.content, vm.progress)
- let pct = sum.totalEx == 0 ? 0 : Double(sum.doneEx) / Double(sum.totalEx)
- return List {
- if vm.showChangelog {
- Section {
- ChangelogCard(vm: vm)
- .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
- .listRowBackground(Color.clear)
- .listRowSeparator(.hidden)
- }
- }
- Section {
- GlassCard {
- VStack(alignment: .leading, spacing: 10) {
- Text(vm.tr("welcome_title"))
- .font(.title.bold())
- Text(vm.tr(Self.welcomeBodyKey))
- .font(.subheadline)
- .foregroundStyle(.secondary)
- ProgressView(value: pct)
- .animation(Motion.settle, value: pct)
- Text(vm.fmt("stats_ex_fmt", sum.doneEx, sum.totalEx))
- .font(.footnote)
- .foregroundStyle(.secondary)
- }
- }
- .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
- .listRowBackground(Color.clear)
- .listRowSeparator(.hidden)
- }
-
- Section(vm.tr("subjects_title")) {
- ForEach(Array(vm.content.subjects.filter { $0.bool("open") }.enumerated()), id: \.offset) { _, s in
- let dest = s.strOrNull("target").flatMap(routeFromPage)
- if let dest, dest != .subjects, dest != .welcome {
- NavigationLink(value: dest) {
- subjectLabel(s)
- }
- } else {
- subjectLabel(s)
- }
- }
- }
-
- let soon = vm.content.subjects.filter { !$0.bool("open") }
- if !soon.isEmpty {
- Section(vm.tr("subjects_soon")) {
- ForEach(Array(soon.enumerated()), id: \.offset) { _, s in
- Label {
- Text(vm.tr(s.str("key")))
- } icon: {
- SubjectIcon(icon: s.str("icon"), open: false)
- }
- .foregroundStyle(.secondary)
- }
- }
- }
- }
- .navigationTitle("Graduately")
- .navigationBarTitleDisplayMode(.large)
- }
-
- private func subjectLabel(_ s: J) -> some View {
- let part = subjectSum(vm, s)
- return Label {
- VStack(alignment: .leading, spacing: 6) {
- Text(vm.tr(s.str("key")))
- if part.totalEx > 0 {
- ProgressLine(done: part.doneEx, total: part.totalEx)
- }
- }
- .padding(.vertical, 2)
- } icon: {
- SubjectIcon(icon: s.str("icon"), open: true)
- }
- }
- #endif
-
- private static var welcomeBodyKey: String {
- #if os(macOS)
- "welcome_body_macos"
- #else
- "welcome_body_ios"
- #endif
- }
-}
-
-struct ChangelogCard: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- let langKey = vm.lang == .en ? "en" : "cs"
- let blocks = Array(vm.content.changelog.prefix(3))
- GlassCard {
- VStack(alignment: .leading, spacing: 10) {
- HStack {
- Label(vm.tr("changelog_title"), systemImage: "sparkles")
- .font(.headline)
- Spacer()
- if let date = blocks.first?.strOrNull("date") {
- Text(date)
- .font(.caption.weight(.semibold))
- .foregroundStyle(.secondary)
- }
- }
- ForEach(Array(blocks.enumerated()), id: \.offset) { index, block in
- ChangelogLines(index: index, lines: changelogLines(block, langKey))
- }
- Button(vm.tr("changelog_dismiss")) { vm.dismissChangelog() }
- #if os(macOS)
- .buttonStyle(.borderedProminent)
- .controlSize(.large)
- #else
- .buttonStyle(.glassProminent)
- .controlSize(.small)
- #endif
- }
- }
- }
-}
-
-private func changelogLines(_ block: J, _ langKey: String) -> [String] {
- let preferred = block.strs(langKey)
- return preferred.isEmpty ? block.strs("cs") : preferred
-}
-
-private struct ChangelogLines: View {
- let index: Int
- let lines: [String]
-
- var body: some View {
- VStack(alignment: .leading, spacing: 4) {
- if index > 0 { Spacer().frame(height: 4) }
- VStack(alignment: .leading, spacing: 4) { lineList }
- }
- }
-
- @ViewBuilder
- private var lineList: some View {
- ForEach(Array(lines.enumerated()), id: \.offset) { _, line in
- Text("• \(line)")
- .font(.subheadline)
- .foregroundStyle(.secondary)
- .fixedSize(horizontal: false, vertical: true)
- .frame(maxWidth: .infinity, alignment: .leading)
- }
- }
-}
-
-struct SettingsScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- macSettings
- #else
- settingsForm
- #endif
- }
-
- #if os(iOS)
- private var settingsForm: some View {
- Form {
- Section(vm.tr("mode")) {
- Picker(vm.tr("mode"), selection: modeBinding) {
- Text(vm.tr("mode_dark")).tag(ColorMode.dark)
- Text(vm.tr("mode_light")).tag(ColorMode.light)
- }
- .pickerStyle(.segmented)
- #if os(macOS)
- .controlSize(.large)
- #endif
- }
- Section(vm.tr("theme")) {
- ForEach(Array(ThemeId.allCases), id: \.self) { id in
- Button {
- vm.setTheme(id)
- } label: {
- HStack(spacing: 12) {
- Circle().fill(themePalette(id, vm.mode).tint)
- .frame(width: {
- #if os(macOS)
- 18
- #else
- 14
- #endif
- }(), height: {
- #if os(macOS)
- 18
- #else
- 14
- #endif
- }())
- Text(themeNames[id.rawValue])
- #if os(macOS)
- .font(.title3)
- #endif
- .foregroundStyle(.primary)
- Spacer()
- if vm.themeId == id {
- Image(systemName: "checkmark").fontWeight(.semibold)
- }
- }
- #if os(macOS)
- .padding(.vertical, 4)
- #endif
- }
- }
- }
- Section(vm.tr("language")) {
- Picker(vm.tr("language"), selection: langBinding) {
- Text("Čeština").tag(UiLang.cs)
- Text("English").tag(UiLang.en)
- }
- .pickerStyle(.segmented)
- #if os(macOS)
- .controlSize(.large)
- #endif
- }
- Section(vm.tr("updates")) {
- Text(updateText)
- #if os(macOS)
- .font(.body)
- #endif
- .foregroundStyle(.secondary)
- Button(vm.tr("update_check")) { vm.checkUpdate(true) }
- #if os(macOS)
- .controlSize(.large)
- #endif
- if vm.update.canInstall {
- Button(vm.tr("update_install")) { vm.installUpdate() }
- #if os(macOS)
- .controlSize(.large)
- #endif
- }
- LabeledContent("Build", value: "\(AppConfig.versionName) · \(String(AppConfig.commit.prefix(7)))")
- }
- }
- .navigationTitle(vm.tr("settings_title"))
- .appFormStyle()
- }
- #endif
-
- #if os(macOS)
- private var macSettings: some View {
- let themes = Array(ThemeId.allCases)
- return VStack(alignment: .leading, spacing: 20) {
- HStack(alignment: .top, spacing: 16) {
- settingsCard(vm.tr("mode")) {
- Picker(vm.tr("mode"), selection: modeBinding) {
- Text(vm.tr("mode_dark")).tag(ColorMode.dark)
- Text(vm.tr("mode_light")).tag(ColorMode.light)
- }
- .pickerStyle(.segmented)
- .labelsHidden()
- .controlSize(.large)
- }
- settingsCard(vm.tr("language")) {
- Picker(vm.tr("language"), selection: langBinding) {
- Text("Čeština").tag(UiLang.cs)
- Text("English").tag(UiLang.en)
- }
- .pickerStyle(.segmented)
- .labelsHidden()
- .controlSize(.large)
- }
- settingsCard(vm.tr("updates")) {
- Text(updateText)
- .font(.body)
- .foregroundStyle(.secondary)
- .fixedSize(horizontal: false, vertical: true)
- Button(vm.tr("update_check")) { vm.checkUpdate(true) }
- .controlSize(.large)
- if vm.update.canInstall {
- Button(vm.tr("update_install")) { vm.installUpdate() }
- .controlSize(.large)
- }
- LabeledContent("Build", value: "\(AppConfig.versionName) · \(String(AppConfig.commit.prefix(7)))")
- }
- }
- .layoutPriority(1)
- Text(vm.tr("theme"))
- .font(.title2.weight(.semibold))
- .layoutPriority(1)
- LazyVGrid(columns: [GridItem(.adaptive(minimum: 280), spacing: 12)], spacing: 12) {
- ForEach(themes, id: \.self) { id in
- Button {
- vm.setTheme(id)
- } label: {
- HStack(spacing: 12) {
- Circle().fill(themePalette(id, vm.mode).tint)
- .frame(width: 18, height: 18)
- Text(themeNames[id.rawValue])
- .font(.title3)
- .foregroundStyle(.primary)
- Spacer(minLength: 0)
- if vm.themeId == id {
- Image(systemName: "checkmark").fontWeight(.semibold)
- }
- }
- .padding(16)
- .frame(maxWidth: .infinity, minHeight: 64, alignment: .leading)
- .softSurface(cornerRadius: 16)
- }
- .buttonStyle(PressScaleStyle())
- }
- }
- }
- .padding(MacChrome.pageInset)
- .frame(maxWidth: MacChrome.contentWidth, maxHeight: .infinity, alignment: .topLeading)
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
- .navigationTitle(vm.tr("settings_title"))
- }
-
- private func settingsCard(_ title: String, @ViewBuilder content: @escaping () -> Content) -> some View {
- GlassCard {
- VStack(alignment: .leading, spacing: 12) {
- Text(title).font(.headline)
- content()
- }
- .frame(maxWidth: .infinity, alignment: .leading)
- }
- }
- #endif
-
- private var modeBinding: Binding {
- Binding(get: { vm.mode }, set: { vm.applyMode($0) })
- }
-
- private var langBinding: Binding {
- Binding(get: { vm.lang }, set: { vm.applyLang($0) })
- }
-
- private var updateText: String {
- if let arg = vm.update.messageArg {
- return vm.fmt(vm.update.messageKey, arg)
- }
- return vm.tr(vm.update.messageKey)
- }
-}
-
-struct StatsScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- macStats
- #else
- iosStats
- #endif
- }
-
- #if os(iOS)
- private var iosStats: some View {
- let sum = summarize(vm.content, vm.progress)
- let pct = sum.totalEx == 0 ? 0.0 : Double(sum.doneEx) / Double(sum.totalEx)
- return List {
- Section {
- GlassCard {
- VStack(alignment: .leading, spacing: 14) {
- HStack(alignment: .top) {
- stat(vm.tr("stats_ex_label"), vm.fmt("stats_ex_fmt", sum.doneEx, sum.totalEx))
- stat(vm.tr("stats_pct_label"), vm.fmt("stats_pct_fmt", Int(pct * 100)))
- stat(vm.tr("stats_units_label"), vm.fmt("stats_units_fmt", sum.doneUnits, sum.openUnits))
- }
- ProgressView(value: pct)
- .animation(Motion.settle, value: pct)
- }
- }
- .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
- .listRowBackground(Color.clear)
- .listRowSeparator(.hidden)
- }
- Section(vm.tr("stats_section")) {
- ForEach(Array(vm.content.subjects.enumerated()), id: \.offset) { _, s in
- let open = s.bool("open")
- let part = subjectSum(vm, s)
- Label {
- VStack(alignment: .leading, spacing: 6) {
- HStack {
- Text(vm.tr(s.str("key")))
- #if os(macOS)
- .font(.title3)
- #endif
- Spacer()
- Text(open ? vm.fmt("stats_ex_fmt", part.doneEx, part.totalEx) : vm.tr("stats_locked"))
- #if os(macOS)
- .font(.body)
- #else
- .font(.caption)
- #endif
- .foregroundStyle(.secondary)
- .monospacedDigit()
- }
- if open && part.totalEx > 0 {
- ProgressView(value: Double(part.doneEx) / Double(part.totalEx))
- .animation(Motion.settle, value: part.doneEx)
- }
- }
- } icon: {
- SubjectIcon(icon: s.str("icon"), open: open, size: {
- #if os(macOS)
- 32
- #else
- 26
- #endif
- }())
- }
- .foregroundStyle(open ? .primary : .secondary)
- }
- }
- }
- .navigationTitle(vm.tr("stats_title"))
- .appListStyle()
- }
- #endif
-
- #if os(macOS)
- private var macStats: some View {
- let sum = summarize(vm.content, vm.progress)
- let pct = sum.totalEx == 0 ? 0.0 : Double(sum.doneEx) / Double(sum.totalEx)
- let ranked = rankedSubjects()
- return VStack(alignment: .leading, spacing: 22) {
- HStack(spacing: 16) {
- statMetric(vm.tr("stats_ex_label"), vm.fmt("stats_ex_fmt", sum.doneEx, sum.totalEx))
- statMetric(vm.tr("stats_pct_label"), vm.fmt("stats_pct_fmt", Int(pct * 100)))
- statMetric(vm.tr("stats_units_label"), vm.fmt("stats_units_fmt", sum.doneUnits, sum.openUnits))
- }
- Text(vm.tr("stats_section"))
- .font(.title2.weight(.semibold))
- GeometryReader { geo in
- let count = max(ranked.count, 1)
- let minRow: CGFloat = 54
- let floorH = minRow * CGFloat(ranked.count)
- let rowH = min(76, max(minRow, geo.size.height / CGFloat(count)))
- let table = VStack(spacing: 0) {
- ForEach(Array(ranked.enumerated()), id: \.offset) { index, item in
- statRow(item.subject, part: item.part)
- .frame(height: floorH > geo.size.height + 1 ? minRow : rowH)
- if index < ranked.count - 1 {
- Divider().padding(.leading, 68)
- }
- }
- }
- .softSurface(cornerRadius: 18)
- if floorH > geo.size.height + 1 {
- ScrollView { table }
- } else {
- table.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
- }
- }
- }
- .padding(MacChrome.pageInset)
- .frame(maxWidth: MacChrome.contentWidth, maxHeight: .infinity, alignment: .topLeading)
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
- .navigationTitle(vm.tr("stats_title"))
- }
-
- private func rankedSubjects() -> [(subject: J, part: ProgressSum)] {
- let rows = vm.content.subjects.map { subject in
- (subject: subject, part: subjectSum(vm, subject))
- }
- let open = rows.filter { $0.subject.bool("open") }.sorted { lhs, rhs in
- share(lhs.part) > share(rhs.part)
- }
- let locked = rows.filter { !$0.subject.bool("open") }
- return open + locked
- }
-
- private func share(_ part: ProgressSum) -> Double {
- part.totalEx == 0 ? 0 : Double(part.doneEx) / Double(part.totalEx)
- }
-
- private func statMetric(_ label: String, _ value: String) -> some View {
- VStack(alignment: .leading, spacing: 6) {
- Text(value)
- .font(.system(size: 40, weight: .bold, design: .rounded))
- .monospacedDigit()
- .minimumScaleFactor(0.6)
- .lineLimit(1)
- Text(label)
- .font(.headline)
- .foregroundStyle(.secondary)
- }
- .padding(22)
- .frame(maxWidth: .infinity, alignment: .leading)
- .softSurface(cornerRadius: 18)
- }
-
- private func statRow(_ s: J, part: ProgressSum) -> some View {
- let open = s.bool("open")
- let progress = share(part)
- return HStack(spacing: 16) {
- SubjectIcon(icon: s.str("icon"), open: open, size: 28)
- .frame(width: 36)
- Text(vm.tr(s.str("key")))
- .font(.title3.weight(.semibold))
- .foregroundStyle(open ? .primary : .secondary)
- .lineLimit(1)
- .frame(width: 240, alignment: .leading)
- if open && part.totalEx > 0 {
- statTrack(progress)
- Text(vm.fmt("stats_ex_fmt", part.doneEx, part.totalEx))
- .font(.title3.monospacedDigit())
- .foregroundStyle(.secondary)
- .frame(width: 88, alignment: .trailing)
- } else {
- statTrack(0)
- Text(vm.tr("stats_locked"))
- .font(.body)
- .foregroundStyle(.tertiary)
- .frame(width: 88, alignment: .trailing)
- }
- }
- .padding(.horizontal, 18)
- }
-
- private func statTrack(_ progress: Double) -> some View {
- GeometryReader { geo in
- ZStack(alignment: .leading) {
- Capsule().fill(Color.primary.opacity(0.08))
- Capsule()
- .fill(Color.accentColor)
- .frame(width: max(0, geo.size.width * progress))
- }
- }
- .frame(height: 12)
- .frame(maxWidth: .infinity)
- }
- #endif
-
- private func stat(_ label: String, _ value: String) -> some View {
- VStack(alignment: .leading, spacing: 2) {
- Text(label)
- #if os(macOS)
- .font(.subheadline)
- #else
- .font(.caption)
- #endif
- .foregroundStyle(.secondary)
- Text(value)
- #if os(macOS)
- .font(.title2.bold())
- #else
- .font(.title3.bold())
- #endif
- .monospacedDigit()
- }
- .frame(maxWidth: .infinity, alignment: .leading)
- }
-}
-
-/// Done and total exercises behind one card of a years screen, or nil when there is no count.
-func routeProgress(_ vm: AppModel, _ r: Route) -> (done: Int, total: Int)? {
- let c = vm.content
- let p = vm.progress
- func tally(_ lessons: [J], _ done: (Int) -> Bool) -> (done: Int, total: Int) {
- (lessons.filter { done($0.int("id")) }.count, lessons.count)
- }
- switch r {
- case .netMap: return tally(c.netLessons, p.netDone)
- case .net2Map: return tally(c.itLessons("net2")) { p.courseDone("net2", $0) }
- case .net3Map: return tally(c.itLessons("net3")) { p.courseDone("net3", $0) }
- case .net4Map: return tally(c.itLessons("net4")) { p.courseDone("net4", $0) }
- case .hwMap: return tally(c.hw, p.hwDone)
- case .hw2Map: return tally(c.itLessons("hw2")) { p.courseDone("hw2", $0) }
- case .hw3Map: return tally(c.itLessons("hw3")) { p.courseDone("hw3", $0) }
- case .hw4Map: return tally(c.itLessons("hw4")) { p.courseDone("hw4", $0) }
- case .onMap: return tally(c.on, p.onDone)
- case .on2Map: return tally(c.on2, p.on2Done)
- case .on3Map: return tally(c.on3, p.on3Done)
- case .on4Map: return tally(c.on4, p.on4Done)
- case .enMap(let year): return tally(c.enYear(year)) { p.enDone(year, $0) }
- case .deMap(let year): return tally(c.deYear(year)) { p.deDone(year, $0) }
- case .roadmap:
- var done = 0
- var total = 0
- for u in c.german where u.bool("unlocked") {
- let n = u.strs("names").count
- total += n
- done += (0.. ProgressSum {
- var sum = ProgressSum()
- switch s.str("target") {
- case "dehome":
- for u in vm.content.german where u.bool("unlocked") {
- let n = u.strs("names").count
- sum.totalEx += n
- sum.doneEx += (1...n).filter { vm.progress.germanDone(u.int("id"), $0) }.count
- }
- sum.totalEx += (1...4).reduce(0) { $0 + vm.content.deYear($1).count }
- sum.doneEx += (1...4).reduce(0) { acc, year in
- acc + vm.content.deYear(year).filter { vm.progress.deDone(year, $0.int("id")) }.count
- }
- case "netyears":
- sum.totalEx = vm.content.netLessons.count
- + vm.content.net2.count + vm.content.net3.count + vm.content.net4.count
- sum.doneEx = vm.content.netLessons.filter { vm.progress.netDone($0.int("id")) }.count
- + ["net2", "net3", "net4"].reduce(0) { acc, course in
- acc + vm.content.itLessons(course).filter { vm.progress.courseDone(course, $0.int("id")) }.count
- }
- case "hwyears", "hwmap":
- sum.totalEx = vm.content.hw.count
- + vm.content.hw2.count + vm.content.hw3.count + vm.content.hw4.count
- sum.doneEx = vm.content.hw.filter { vm.progress.hwDone($0.int("id")) }.count
- + ["hw2", "hw3", "hw4"].reduce(0) { acc, course in
- acc + vm.content.itLessons(course).filter { vm.progress.courseDone(course, $0.int("id")) }.count
- }
- case "enyears", "en1map", "en2map", "en3map", "en4map":
- sum.totalEx = (1...4).reduce(0) { $0 + vm.content.enYear($1).count }
- sum.doneEx = (1...4).reduce(0) { acc, year in
- acc + vm.content.enYear(year).filter { vm.progress.enDone(year, $0.int("id")) }.count
- }
- case "onyears", "onmap", "on2map", "on3map", "on4map":
- sum.totalEx = vm.content.on.count + vm.content.on2.count + vm.content.on3.count + vm.content.on4.count
- sum.doneEx = vm.content.on.filter { vm.progress.onDone($0.int("id")) }.count
- + vm.content.on2.filter { vm.progress.on2Done($0.int("id")) }.count
- + vm.content.on3.filter { vm.progress.on3Done($0.int("id")) }.count
- + vm.content.on4.filter { vm.progress.on4Done($0.int("id")) }.count
- case "scimap":
- sum.totalEx = vm.content.chem.count + vm.content.bio.count
- sum.doneEx = vm.content.chem.filter { vm.progress.chemDone($0.int("id")) }.count
- + vm.content.bio.filter { vm.progress.bioDone($0.int("id")) }.count
- case "matyears", "mat0map", "matmap", "mat2map", "mat3map", "mat4map":
- sum.totalEx = vm.content.mat0.count + vm.content.mat.count + vm.content.mat2.count + vm.content.mat3.count + vm.content.mat4.count
- sum.doneEx = vm.content.mat0.filter { vm.progress.mat0Done($0.int("id")) }.count
- + vm.content.mat.filter { vm.progress.matDone($0.int("id")) }.count
- + vm.content.mat2.filter { vm.progress.mat2Done($0.int("id")) }.count
- + vm.content.mat3.filter { vm.progress.mat3Done($0.int("id")) }.count
- + vm.content.mat4.filter { vm.progress.mat4Done($0.int("id")) }.count
- case "csyears", "csmap", "cs2map", "cs3map", "cs4map":
- sum.totalEx = vm.content.cs.count + vm.content.cs2.count + vm.content.cs3.count + vm.content.cs4.count
- sum.doneEx = vm.content.cs.filter { vm.progress.csDone($0.int("id")) }.count
- + vm.content.cs2.filter { vm.progress.cs2Done($0.int("id")) }.count
- + vm.content.cs3.filter { vm.progress.cs3Done($0.int("id")) }.count
- + vm.content.cs4.filter { vm.progress.cs4Done($0.int("id")) }.count
- case "pgyears", "pgmap", "pg2map", "pg3map", "pg4map":
- sum.totalEx = vm.content.pg.count + vm.content.pg2.count + vm.content.pg3.count + vm.content.pg4.count
- sum.doneEx = vm.content.pg.filter { vm.progress.pgDone($0.int("id")) }.count
- + vm.content.pg2.filter { vm.progress.pg2Done($0.int("id")) }.count
- + vm.content.pg3.filter { vm.progress.pg3Done($0.int("id")) }.count
- + vm.content.pg4.filter { vm.progress.pg4Done($0.int("id")) }.count
- case "swyears", "swmap", "sw2map", "sw3map", "sw4map":
- sum.totalEx = vm.content.sw.count + vm.content.sw2.count + vm.content.sw3.count + vm.content.sw4.count
- sum.doneEx = vm.content.sw.filter { vm.progress.swDone($0.int("id")) }.count
- + vm.content.sw2.filter { vm.progress.sw2Done($0.int("id")) }.count
- + vm.content.sw3.filter { vm.progress.sw3Done($0.int("id")) }.count
- + vm.content.sw4.filter { vm.progress.sw4Done($0.int("id")) }.count
- case "tgyears", "tgmap", "tg2map", "tg3map", "tg4map":
- sum.totalEx = vm.content.tg.count + vm.content.tg2.count + vm.content.tg3.count + vm.content.tg4.count
- sum.doneEx = vm.content.tg.filter { vm.progress.tgDone($0.int("id")) }.count
- + vm.content.tg2.filter { vm.progress.tg2Done($0.int("id")) }.count
- + vm.content.tg3.filter { vm.progress.tg3Done($0.int("id")) }.count
- + vm.content.tg4.filter { vm.progress.tg4Done($0.int("id")) }.count
- case "fyzyears", "fyzmap", "fyz2map", "fyz3map", "fyz4map":
- sum.totalEx = vm.content.fyz.count + vm.content.fyz2.count + vm.content.fyz3.count + vm.content.fyz4.count
- sum.doneEx = vm.content.fyz.filter { vm.progress.fyzDone($0.int("id")) }.count
- + vm.content.fyz2.filter { vm.progress.fyz2Done($0.int("id")) }.count
- + vm.content.fyz3.filter { vm.progress.fyz3Done($0.int("id")) }.count
- + vm.content.fyz4.filter { vm.progress.fyz4Done($0.int("id")) }.count
- case "czechmap":
- let litCount = vm.content.lit.count + vm.content.lit2.count + vm.content.lit3.count + vm.content.lit4.count
- let litDone = vm.content.lit.filter { vm.progress.litDone($0.int("id")) }.count
- + vm.content.lit2.filter { vm.progress.lit2Done($0.int("id")) }.count
- + vm.content.lit3.filter { vm.progress.lit3Done($0.int("id")) }.count
- + vm.content.lit4.filter { vm.progress.lit4Done($0.int("id")) }.count
- sum.totalEx = vm.content.mluvnice.count + litCount
- sum.doneEx = vm.content.mluvnice.filter { vm.progress.mluvDone($0.int("id")) }.count + litDone
- default:
- break
- }
- return sum
-}
-
-struct SearchScreen: View {
- @ObservedObject var vm: AppModel
- @State private var query = ""
- #if os(iOS)
- @FocusState private var fieldFocused: Bool
- #endif
-
- var body: some View {
- let q = normalizeAnswer(query)
- let hits = buildSearch(vm).filter { q.isEmpty || normalizeAnswer($0.hay).contains(q) }
- #if os(macOS)
- return macSearch(hits)
- #else
- return iosSearch(hits)
- #endif
- }
-
- #if os(macOS)
- private func macSearch(_ hits: [Hit]) -> some View {
- ScrollView {
- if hits.isEmpty {
- ContentUnavailableView.search
- .frame(maxWidth: .infinity, minHeight: 360)
- } else {
- LazyVGrid(columns: [GridItem(.adaptive(minimum: 340), spacing: 12)], spacing: 12) {
- ForEach(hits) { hit in
- Button {
- if !hit.locked { vm.openFromSearch(hit.target) }
- } label: {
- VStack(alignment: .leading, spacing: 6) {
- Text(hit.title)
- .font(.title3.weight(.semibold))
- .foregroundStyle(hit.locked ? .secondary : .primary)
- .multilineTextAlignment(.leading)
- if !hit.sub.isEmpty {
- Text(hit.sub)
- .font(.body)
- .foregroundStyle(.secondary)
- .multilineTextAlignment(.leading)
- }
- }
- .padding(16)
- .frame(maxWidth: .infinity, minHeight: 84, alignment: .leading)
- .softSurface(cornerRadius: 16)
- }
- .buttonStyle(PressScaleStyle())
- .disabled(hit.locked)
- }
- }
- .padding(MacChrome.pageInset)
- .macPage()
- }
- }
- .navigationTitle(vm.tr("search"))
- .searchable(text: $vm.searchQuery, prompt: vm.tr("search_placeholder"))
- .onAppear { query = vm.searchQuery }
- .onChange(of: vm.searchQuery) { _, q in query = q }
- }
- #endif
-
- #if os(iOS)
- private func iosSearch(_ hits: [Hit]) -> some View {
- List {
- if hits.isEmpty {
- ContentUnavailableView.search
- .listRowBackground(Color.clear)
- .listRowSeparator(.hidden)
- } else {
- ForEach(hits) { hit in
- Button {
- #if os(iOS)
- fieldFocused = false
- #endif
- if !hit.locked { vm.openFromSearch(hit.target) }
- } label: {
- ListRowLabel(title: hit.title, subtitle: hit.sub)
- }
- .disabled(hit.locked)
- .foregroundStyle(hit.locked ? .secondary : .primary)
- }
- }
- }
- .navigationTitle(vm.tr("search"))
- .navigationBarTitleDisplayMode(.inline)
- .safeAreaBar(edge: .top) {
- searchField
- }
- .onAppear {
- query = vm.searchQuery
- DispatchQueue.main.async {
- fieldFocused = true
- }
- }
- .onChange(of: query) { _, q in
- vm.searchQuery = q
- }
- .onChange(of: vm.searchQuery) { _, q in
- if q != query { query = q }
- }
- .appListStyle()
- }
- #endif
-
- #if os(iOS)
- private var searchField: some View {
- HStack(spacing: 8) {
- Image(systemName: "magnifyingglass")
- .foregroundStyle(.secondary)
- TextField(vm.tr("search_placeholder"), text: $query)
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
- .submitLabel(.search)
- .focused($fieldFocused)
- if !query.isEmpty {
- Button {
- query = ""
- fieldFocused = true
- } label: {
- Image(systemName: "xmark.circle.fill")
- .foregroundStyle(.tertiary)
- }
- .buttonStyle(PressScaleStyle())
- .accessibilityLabel("Clear")
- }
- }
- .padding(.horizontal, 12)
- .padding(.vertical, 10)
- .background(Color(.tertiarySystemFill), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
- .padding(.horizontal, 16)
- .padding(.vertical, 8)
- }
- #endif
-}
-
-private struct Hit: Identifiable {
- var id: String { target + "|" + title }
- let title: String
- let sub: String
- let target: String
- let locked: Bool
- let hay: String
-}
-
-private func buildSearch(_ vm: AppModel) -> [Hit] {
- var out: [Hit] = []
- func add(_ title: String, _ sub: String, _ target: String, locked: Bool = false, extra: String = "") {
- out.append(Hit(title: title, sub: sub, target: target, locked: locked, hay: "\(title) \(sub) \(target) \(extra)"))
- }
- add(vm.tr("subjects_title"), vm.tr("search_page"), "subjects", extra: "predmety")
- add(vm.tr("stats"), vm.tr("search_page"), "stats")
- add(vm.tr("search_home"), vm.tr("search_page"), "welcome")
- for s in vm.content.subjects {
- add(vm.tr(s.str("key")), vm.tr("search_subject"), s.strOrNull("target") ?? "subjects", locked: !s.bool("open"))
- }
- for u in vm.content.german {
- let unlocked = u.bool("unlocked")
- add(u.str("title"), vm.tr("search_unit"), u.strOrNull("page") ?? "roadmap", locked: !unlocked, extra: "deutsch")
- if unlocked {
- for (i, name) in u.strs("names").enumerated() {
- add(name, u.str("title"), "u\(u.int("id") + 1)e\(i + 1)")
- }
- if u.has("branch") { add("Vokabeltraining", u.str("title"), u.str("branch"), extra: "vokabel") }
- }
- }
- for l in vm.content.netLessons {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "netunit\(l.int("id"))", extra: "site")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "netex\(l.int("id"))")
- }
- for course in ["net2", "net3", "net4"] {
- add(vm.tr("net_year\(course.dropFirst(3))"), vm.tr("search_lesson"), "\(course)map", extra: "rocnik site")
- for l in vm.content.itLessons(course) {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "\(course)unit\(l.int("id"))", extra: "site")
- }
- }
- add(vm.tr("hw_year1"), vm.tr("search_lesson"), "hwmap", extra: "rocnik")
- for l in vm.content.hw {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "hwunit\(l.int("id"))", extra: "hardware")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "hwex\(l.int("id"))")
- }
- for course in ["hw2", "hw3", "hw4"] {
- add(vm.tr("hw_year\(course.dropFirst(2))"), vm.tr("search_lesson"), "\(course)map", extra: "rocnik hardware")
- for l in vm.content.itLessons(course) {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "\(course)unit\(l.int("id"))", extra: "hardware")
- }
- }
- add(vm.tr("on_year1"), vm.tr("search_lesson"), "onmap", extra: "rocnik")
- add(vm.tr("on_year2"), vm.tr("search_lesson"), "on2map", extra: "rocnik")
- add(vm.tr("on_year3"), vm.tr("search_lesson"), "on3map", extra: "rocnik")
- add(vm.tr("on_year4"), vm.tr("search_lesson"), "on4map", extra: "rocnik")
- for year in 1...4 {
- add(vm.tr("en_year\(year)"), vm.tr("search_lesson"), "en\(year)map", extra: "anglictina english rocnik")
- for l in vm.content.enYear(year) {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "en\(year)unit\(l.int("id"))", extra: "anglictina english")
- }
- add(vm.tr("de_year\(year)"), vm.tr("search_lesson"), "de\(year)map", extra: "nemcina deutsch rocnik")
- for l in vm.content.deYear(year) {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "de\(year)unit\(l.int("id"))", extra: "nemcina deutsch")
- }
- }
- for l in vm.content.on {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "onunit\(l.int("id"))", extra: "obcanka")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "onex\(l.int("id"))")
- }
- for l in vm.content.on2 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on2unit\(l.int("id"))", extra: "obcanka")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on2ex\(l.int("id"))")
- }
- for l in vm.content.on3 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on3unit\(l.int("id"))", extra: "obcanka")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on3ex\(l.int("id"))")
- }
- for l in vm.content.on4 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "on4unit\(l.int("id"))", extra: "obcanka")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "on4ex\(l.int("id"))")
- }
- add(vm.tr("Literatura"), vm.tr("search_lesson"), "lityears", extra: "literatura")
- add(vm.tr("lit_year1"), vm.tr("search_lesson"), "litmap", extra: "rocnik")
- add(vm.tr("lit_year2"), vm.tr("search_lesson"), "lit2map", extra: "rocnik")
- add(vm.tr("lit_year3"), vm.tr("search_lesson"), "lit3map", extra: "rocnik")
- add(vm.tr("lit_year4"), vm.tr("search_lesson"), "lit4map", extra: "rocnik")
- for l in vm.content.lit {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "litunit\(l.int("id"))", extra: "literatura")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "litex\(l.int("id"))")
- }
- for l in vm.content.lit2 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "lit2unit\(l.int("id"))", extra: "literatura")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "lit2ex\(l.int("id"))")
- }
- for l in vm.content.lit3 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "lit3unit\(l.int("id"))", extra: "literatura")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "lit3ex\(l.int("id"))")
- }
- for l in vm.content.lit4 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "lit4unit\(l.int("id"))", extra: "literatura")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "lit4ex\(l.int("id"))")
- }
- add(vm.tr("Matematika"), vm.tr("search_lesson"), "matyears", extra: "matematika")
- add(vm.tr("mat0_year"), vm.tr("search_lesson"), "mat0map", extra: "zakladka")
- add(vm.tr("mat_year1"), vm.tr("search_lesson"), "matmap", extra: "rocnik")
- add(vm.tr("mat_year2"), vm.tr("search_lesson"), "mat2map", extra: "rocnik")
- add(vm.tr("mat_year3"), vm.tr("search_lesson"), "mat3map", extra: "rocnik")
- add(vm.tr("mat_year4"), vm.tr("search_lesson"), "mat4map", extra: "rocnik")
- for l in vm.content.mat0 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat0unit\(l.int("id"))", extra: "matematika")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat0ex\(l.int("id"))")
- }
- for l in vm.content.mat {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "matunit\(l.int("id"))", extra: "matematika")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "matex\(l.int("id"))")
- }
- for l in vm.content.mat2 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat2unit\(l.int("id"))", extra: "matematika")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat2ex\(l.int("id"))")
- }
- for l in vm.content.mat3 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat3unit\(l.int("id"))", extra: "matematika")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat3ex\(l.int("id"))")
- }
- for l in vm.content.mat4 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "mat4unit\(l.int("id"))", extra: "matematika")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "mat4ex\(l.int("id"))")
- }
- add(vm.tr("Fyzika"), vm.tr("search_lesson"), "fyzyears", extra: "fyzika")
- add(vm.tr("fyz_year1"), vm.tr("search_lesson"), "fyzmap", extra: "rocnik")
- add(vm.tr("fyz_year2"), vm.tr("search_lesson"), "fyz2map", extra: "rocnik")
- add(vm.tr("fyz_year3"), vm.tr("search_lesson"), "fyz3map", extra: "rocnik")
- add(vm.tr("fyz_year4"), vm.tr("search_lesson"), "fyz4map", extra: "rocnik")
- for l in vm.content.fyz {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "fyzunit\(l.int("id"))", extra: "fyzika")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "fyzex\(l.int("id"))")
- }
- for l in vm.content.fyz2 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "fyz2unit\(l.int("id"))", extra: "fyzika")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "fyz2ex\(l.int("id"))")
- }
- for l in vm.content.fyz3 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "fyz3unit\(l.int("id"))", extra: "fyzika")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "fyz3ex\(l.int("id"))")
- }
- for l in vm.content.fyz4 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "fyz4unit\(l.int("id"))", extra: "fyzika")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "fyz4ex\(l.int("id"))")
- }
- add(vm.tr("Programové vybavení"), vm.tr("search_lesson"), "swyears", extra: "office windows excel mysql")
- add(vm.tr("sw_year1"), vm.tr("search_lesson"), "swmap", extra: "rocnik")
- add(vm.tr("sw_year2"), vm.tr("search_lesson"), "sw2map", extra: "rocnik")
- add(vm.tr("sw_year3"), vm.tr("search_lesson"), "sw3map", extra: "rocnik")
- add(vm.tr("sw_year4"), vm.tr("search_lesson"), "sw4map", extra: "rocnik")
- for l in vm.content.sw {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "swunit\(l.int("id"))", extra: "windows")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "swex\(l.int("id"))")
- }
- for l in vm.content.sw2 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "sw2unit\(l.int("id"))", extra: "word excel")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "sw2ex\(l.int("id"))")
- }
- for l in vm.content.sw3 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "sw3unit\(l.int("id"))", extra: "access mysql")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "sw3ex\(l.int("id"))")
- }
- for l in vm.content.sw4 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "sw4unit\(l.int("id"))", extra: "wordpress")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "sw4ex\(l.int("id"))")
- }
- add(vm.tr("Prezentační grafika"), vm.tr("search_lesson"), "pgyears", extra: "photoshop premiere illustrator")
- add(vm.tr("pg_year1"), vm.tr("search_lesson"), "pgmap", extra: "rocnik")
- add(vm.tr("pg_year2"), vm.tr("search_lesson"), "pg2map", extra: "rocnik")
- add(vm.tr("pg_year3"), vm.tr("search_lesson"), "pg3map", extra: "rocnik")
- add(vm.tr("pg_year4"), vm.tr("search_lesson"), "pg4map", extra: "rocnik")
- for l in vm.content.pg {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "pgunit\(l.int("id"))", extra: "photoshop")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "pgex\(l.int("id"))")
- }
- for l in vm.content.pg2 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "pg2unit\(l.int("id"))", extra: "photoshop")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "pg2ex\(l.int("id"))")
- }
- for l in vm.content.pg3 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "pg3unit\(l.int("id"))", extra: "premiere")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "pg3ex\(l.int("id"))")
- }
- for l in vm.content.pg4 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "pg4unit\(l.int("id"))", extra: "illustrator")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "pg4ex\(l.int("id"))")
- }
- add(vm.tr("Technická grafika"), vm.tr("search_lesson"), "tgyears", extra: "autocad grafika")
- add(vm.tr("tg_year1"), vm.tr("search_lesson"), "tgmap", extra: "rocnik")
- add(vm.tr("tg_year2"), vm.tr("search_lesson"), "tg2map", extra: "rocnik")
- add(vm.tr("tg_year3"), vm.tr("search_lesson"), "tg3map", extra: "rocnik")
- add(vm.tr("tg_year4"), vm.tr("search_lesson"), "tg4map", extra: "rocnik")
- for l in vm.content.tg {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "tgunit\(l.int("id"))", extra: "autocad")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "tgex\(l.int("id"))")
- }
- for l in vm.content.tg2 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "tg2unit\(l.int("id"))", extra: "autocad")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "tg2ex\(l.int("id"))")
- }
- for l in vm.content.tg3 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "tg3unit\(l.int("id"))", extra: "autocad")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "tg3ex\(l.int("id"))")
- }
- for l in vm.content.tg4 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "tg4unit\(l.int("id"))", extra: "autocad")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "tg4ex\(l.int("id"))")
- }
- add(vm.tr("Programování"), vm.tr("search_lesson"), "csyears", extra: "csharp c#")
- add(vm.tr("cs_year1"), vm.tr("search_lesson"), "csmap", extra: "rocnik")
- add(vm.tr("cs_year2"), vm.tr("search_lesson"), "cs2map", extra: "rocnik")
- add(vm.tr("cs_year3"), vm.tr("search_lesson"), "cs3map", extra: "rocnik")
- add(vm.tr("cs_year4"), vm.tr("search_lesson"), "cs4map", extra: "rocnik")
- for l in vm.content.cs {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "csunit\(l.int("id"))", extra: "csharp")
- }
- for l in vm.content.cs2 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "cs2unit\(l.int("id"))", extra: "csharp")
- }
- for l in vm.content.cs3 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "cs3unit\(l.int("id"))", extra: "csharp")
- }
- for l in vm.content.cs4 {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "cs4unit\(l.int("id"))", extra: "csharp")
- }
- add(vm.tr("Chemie"), vm.tr("search_lesson"), "chemmap", extra: "chemie")
- add(vm.tr("Biologie"), vm.tr("search_lesson"), "biomap", extra: "biologie")
- for l in vm.content.chem {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "chemunit\(l.int("id"))", extra: "chemie")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "chemex\(l.int("id"))")
- }
- for l in vm.content.bio {
- add(vm.tr(l.str("titleKey")), vm.tr("search_lesson"), "biounit\(l.int("id"))", extra: "biologie")
- add(vm.tr(l.str("titleKey")), vm.tr("search_exercise"), "bioex\(l.int("id"))")
- }
- add(vm.tr("Mluvnice"), vm.tr("search_lesson"), "mluvnice")
- add(vm.tr("Maturitní četba"), vm.tr("search_book"), "readinglist")
- for b in vm.content.books {
- add(b.str("title"), vm.tr("search_book"), b.str("page"))
- }
- return out
-}
-
-struct RoadmapScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- roadmapPath
- #else
- roadmapList
- #endif
- }
-
- #if os(macOS)
- private var roadmapPath: some View {
- let current = vm.content.german.firstIndex { u in
- u.bool("unlocked") && u.strs("names").indices.contains { !vm.progress.germanDone(u.int("id"), $0 + 1) }
- }
- let lit = CGFloat(max((current ?? vm.content.german.count) - 1, 0))
- return PathMap(
- nodes: vm.content.german.enumerated().map { i, u in
- let unlocked = u.bool("unlocked")
- let names = u.strs("names")
- let done = unlocked && !names.isEmpty && names.indices.allSatisfy {
- vm.progress.germanDone(u.int("id"), $0 + 1)
- }
- return MapNode(
- id: "unit-\(i)",
- label: u.str("title"),
- locked: !unlocked,
- done: done,
- current: i == current,
- finish: i == vm.content.german.count - 1,
- destination: unlocked ? .unitMap(u.int("id")) : nil
- )
- },
- palette: vm.palette,
- litUntil: lit,
- nodeSize: MacChrome.pathNode,
- mx: 100,
- my: 140,
- spac: 196,
- gap: 280,
- wave: 18
- )
- .padding(28)
- .navigationTitle(vm.tr("roadmap_title"))
- .navigationSubtitle(vm.tr("roadmap_sub"))
- }
- #endif
-
- #if os(iOS)
- private var roadmapList: some View {
- List {
- ForEach(Array(vm.content.german.enumerated()), id: \.offset) { _, u in
- let unlocked = u.bool("unlocked")
- let names = u.strs("names")
- let done = unlocked && !names.isEmpty && names.indices.allSatisfy { vm.progress.germanDone(u.int("id"), $0 + 1) }
- if unlocked {
- NavigationLink(value: Route.unitMap(u.int("id"))) {
- Label {
- Text(u.str("title"))
- } icon: {
- Image(systemName: done ? "checkmark.circle.fill" : "circle")
- }
- }
- } else {
- Label(u.str("title"), systemImage: "lock.fill")
- .foregroundStyle(.secondary)
- }
- }
- }
- .navigationTitle(vm.tr("roadmap_title"))
- .navigationSubtitle(vm.tr("roadmap_sub"))
- .appListStyle()
- }
- #endif
-}
-
-struct UnitMapScreen: View {
- @ObservedObject var vm: AppModel
- let unitId: Int
-
- var body: some View {
- if let u = vm.content.germanUnit(unitId) {
- #if os(macOS)
- unitPath(u)
- #else
- unitList(u)
- #endif
- }
- }
-
- #if os(macOS)
- private func unitPath(_ u: J) -> some View {
- let names = u.strs("names")
- let next = names.indices.first { !vm.progress.germanDone(unitId, $0 + 1) }
- let lit = CGFloat(next ?? names.count)
- let branch: (index: Int, node: MapNode)? = u.has("branch")
- ? (
- 1,
- MapNode(
- id: "vocab-\(unitId)",
- label: vm.tr("Vokabeltraining"),
- locked: false,
- done: vm.progress.vocabDone(unitId),
- current: false,
- destination: .vocab(unitId),
- symbol: "character.book.closed.fill"
- )
- )
- : nil
- return PathMap(
- nodes: names.enumerated().map { i, name in
- MapNode(
- id: "e\(i + 1)",
- label: name,
- locked: false,
- done: vm.progress.germanDone(unitId, i + 1),
- current: i == next,
- destination: .germanEx(unitId, i + 1)
- )
- },
- palette: vm.palette,
- litUntil: lit,
- nodeSize: MacChrome.pathNode,
- mx: 100,
- my: 136,
- spac: 190,
- gap: 268,
- wave: 16,
- branch: branch
- )
- .padding(28)
- .navigationTitle(u.str("title"))
- .navigationSubtitle(vm.tr(u.str("sub")))
- }
- #endif
-
- #if os(iOS)
- private func unitList(_ u: J) -> some View {
- let names = u.strs("names")
- return List {
- ForEach(names.indices, id: \.self) { i in
- NavigationLink(value: Route.germanEx(unitId, i + 1)) {
- Label {
- Text(names[i])
- } icon: {
- Image(systemName: vm.progress.germanDone(unitId, i + 1) ? "checkmark.circle.fill" : "circle")
- }
- }
- }
- if u.has("branch") {
- NavigationLink(value: Route.vocab(unitId)) {
- Label(vm.tr("Vokabeltraining"), systemImage: "character.book.closed")
- }
- }
- }
- .navigationTitle(u.str("title"))
- .navigationSubtitle(vm.tr(u.str("sub")))
- .appListStyle()
- }
- #endif
-}
-
-struct NetYearsScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- let years: [(String, String, Route)] = [
- ("net_year1", "net_sub", .netMap),
- ("net_year2", "net2_sub", .net2Map),
- ("net_year3", "net3_sub", .net3Map),
- ("net_year4", "net4_sub", .net4Map),
- ]
- MacFillGrid(count: years.count, minWidth: 260, minHeight: 200) { i in
- NavigationLink(value: years[i].2) {
- MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).", progress: routeProgress(vm, years[i].2))
- }
- .buttonStyle(PressScaleStyle())
- }
- .navigationTitle(vm.tr("net_years_title"))
- .navigationSubtitle(vm.tr("net_years_sub"))
- #else
- List {
- NavigationLink(value: Route.netMap) {
- ListRowLabel(title: vm.tr("net_year1"), subtitle: vm.tr("net_sub"), progress: routeProgress(vm, Route.netMap))
- }
- NavigationLink(value: Route.net2Map) {
- ListRowLabel(title: vm.tr("net_year2"), subtitle: vm.tr("net2_sub"), progress: routeProgress(vm, Route.net2Map))
- }
- NavigationLink(value: Route.net3Map) {
- ListRowLabel(title: vm.tr("net_year3"), subtitle: vm.tr("net3_sub"), progress: routeProgress(vm, Route.net3Map))
- }
- NavigationLink(value: Route.net4Map) {
- ListRowLabel(title: vm.tr("net_year4"), subtitle: vm.tr("net4_sub"), progress: routeProgress(vm, Route.net4Map))
- }
- }
- .navigationTitle(vm.tr("net_years_title"))
- .navigationSubtitle(vm.tr("net_years_sub"))
- .appListStyle()
- #endif
- }
-}
-
-struct HwYearsScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- let years: [(String, String, Route)] = [
- ("hw_year1", "hw_sub", .hwMap),
- ("hw_year2", "hw2_sub", .hw2Map),
- ("hw_year3", "hw3_sub", .hw3Map),
- ("hw_year4", "hw4_sub", .hw4Map),
- ]
- MacFillGrid(count: years.count, minWidth: 260, minHeight: 200) { i in
- NavigationLink(value: years[i].2) {
- MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).", progress: routeProgress(vm, years[i].2))
- }
- .buttonStyle(PressScaleStyle())
- }
- .navigationTitle(vm.tr("Technické vybavení"))
- .navigationSubtitle(vm.tr("hw_years_sub"))
- #else
- List {
- NavigationLink(value: Route.hwMap) {
- ListRowLabel(title: vm.tr("hw_year1"), subtitle: vm.tr("hw_sub"), progress: routeProgress(vm, Route.hwMap))
- }
- NavigationLink(value: Route.hw2Map) {
- ListRowLabel(title: vm.tr("hw_year2"), subtitle: vm.tr("hw2_sub"), progress: routeProgress(vm, Route.hw2Map))
- }
- NavigationLink(value: Route.hw3Map) {
- ListRowLabel(title: vm.tr("hw_year3"), subtitle: vm.tr("hw3_sub"), progress: routeProgress(vm, Route.hw3Map))
- }
- NavigationLink(value: Route.hw4Map) {
- ListRowLabel(title: vm.tr("hw_year4"), subtitle: vm.tr("hw4_sub"), progress: routeProgress(vm, Route.hw4Map))
- }
- }
- .navigationTitle(vm.tr("Technické vybavení"))
- .navigationSubtitle(vm.tr("hw_years_sub"))
- .appListStyle()
- #endif
- }
-}
-
-struct DeHomeScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- let rows: [(String, String, String, Route)] = [
- ("U", "de_book_title", "de_book_sub", .roadmap),
- ("1", "de_year1", "de_y1_sub", .deMap(1)),
- ("2", "de_year2", "de_y2_sub", .deMap(2)),
- ("3", "de_year3", "de_y3_sub", .deMap(3)),
- ("4", "de_year4", "de_y4_sub", .deMap(4)),
- ]
- MacFillGrid(count: rows.count, minWidth: 260, minHeight: 200) { i in
- NavigationLink(value: rows[i].3) {
- MacLinkCard(title: vm.tr(rows[i].1), subtitle: vm.tr(rows[i].2), badge: rows[i].0, progress: routeProgress(vm, rows[i].3))
- }
- .buttonStyle(PressScaleStyle())
- }
- .navigationTitle(vm.tr("Deutsch"))
- .navigationSubtitle(vm.tr("de_home_sub"))
- #else
- List {
- NavigationLink(value: Route.roadmap) {
- ListRowLabel(title: vm.tr("de_book_title"), subtitle: vm.tr("de_book_sub"), progress: routeProgress(vm, Route.roadmap))
- }
- ForEach(1...4, id: \.self) { year in
- NavigationLink(value: Route.deMap(year)) {
- ListRowLabel(title: vm.tr("de_year\(year)"), subtitle: vm.tr("de_y\(year)_sub"), progress: routeProgress(vm, Route.deMap(year)))
- }
- }
- }
- .navigationTitle(vm.tr("Deutsch"))
- .navigationSubtitle(vm.tr("de_home_sub"))
- #endif
- }
-}
-
-struct EnYearsScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- let years: [(String, String, Route)] = [
- ("en_year1", "en_y1_sub", .enMap(1)),
- ("en_year2", "en_y2_sub", .enMap(2)),
- ("en_year3", "en_y3_sub", .enMap(3)),
- ("en_year4", "en_y4_sub", .enMap(4)),
- ]
- MacFillGrid(count: years.count, minWidth: 260, minHeight: 200) { i in
- NavigationLink(value: years[i].2) {
- MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).", progress: routeProgress(vm, years[i].2))
- }
- .buttonStyle(PressScaleStyle())
- }
- .navigationTitle(vm.tr("English"))
- .navigationSubtitle(vm.tr("en_years_sub"))
- #else
- List {
- ForEach(1...4, id: \.self) { year in
- NavigationLink(value: Route.enMap(year)) {
- ListRowLabel(title: vm.tr("en_year\(year)"), subtitle: vm.tr("en_y\(year)_sub"), progress: routeProgress(vm, Route.enMap(year)))
- }
- }
- }
- .navigationTitle(vm.tr("English"))
- .navigationSubtitle(vm.tr("en_years_sub"))
- .appListStyle()
- #endif
- }
-}
-
-struct OnYearsScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- let years: [(String, String, Route)] = [
- ("on_year1", "on_sub", .onMap),
- ("on_year2", "on2_sub", .on2Map),
- ("on_year3", "on3_sub", .on3Map),
- ("on_year4", "on4_sub", .on4Map),
- ]
- MacFillGrid(count: years.count, minWidth: 260, minHeight: 200) { i in
- NavigationLink(value: years[i].2) {
- MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).", progress: routeProgress(vm, years[i].2))
- }
- .buttonStyle(PressScaleStyle())
- }
- .navigationTitle(vm.tr("Občanská nauka"))
- .navigationSubtitle(vm.tr("on_years_sub"))
- #else
- List {
- NavigationLink(value: Route.onMap) {
- ListRowLabel(title: vm.tr("on_year1"), subtitle: vm.tr("on_sub"), progress: routeProgress(vm, Route.onMap))
- }
- NavigationLink(value: Route.on2Map) {
- ListRowLabel(title: vm.tr("on_year2"), subtitle: vm.tr("on2_sub"), progress: routeProgress(vm, Route.on2Map))
- }
- NavigationLink(value: Route.on3Map) {
- ListRowLabel(title: vm.tr("on_year3"), subtitle: vm.tr("on3_sub"), progress: routeProgress(vm, Route.on3Map))
- }
- NavigationLink(value: Route.on4Map) {
- ListRowLabel(title: vm.tr("on_year4"), subtitle: vm.tr("on4_sub"), progress: routeProgress(vm, Route.on4Map))
- }
- }
- .navigationTitle(vm.tr("Občanská nauka"))
- .navigationSubtitle(vm.tr("on_years_sub"))
- .appListStyle()
- #endif
- }
-}
-
-struct LessonRow: Identifiable {
- let id: String
- let title: String
- let done: Bool
- let destination: Route
-}
-
-struct LessonListScreen: View {
- @ObservedObject var vm: AppModel
- let title: String
- let subtitle: String
- let rows: [LessonRow]
-
- var body: some View {
- #if os(macOS)
- lessonPath
- #else
- lessonList
- #endif
- }
-
- #if os(macOS)
- private var lessonPath: some View {
- let next = rows.firstIndex { !$0.done }
- return PathMap(
- nodes: rows.enumerated().map { i, row in
- MapNode(
- id: row.id,
- label: row.title,
- locked: false,
- done: row.done,
- current: i == next,
- destination: row.destination
- )
- },
- palette: vm.palette,
- litUntil: CGFloat(next ?? rows.count),
- nodeSize: MacChrome.pathNode,
- mx: 98,
- my: 134,
- spac: 186,
- gap: 272,
- wave: 16
- )
- .padding(28)
- .navigationTitle(title)
- .navigationSubtitle(subtitle)
- }
- #endif
-
- #if os(iOS)
- private var lessonList: some View {
- List(rows) { row in
- NavigationLink(value: row.destination) {
- Label {
- Text(row.title)
- } icon: {
- Image(systemName: row.done ? "checkmark.circle.fill" : "circle")
- }
- }
- }
- .navigationTitle(title)
- .navigationSubtitle(subtitle)
- .appListStyle()
- }
- #endif
-}
-
-struct SlidesScreen: View {
- @ObservedObject var vm: AppModel
- let title: String
- let subtitle: String
- let slides: [J]
- let next: Route
- @State private var idx = 0
-
- var body: some View {
- Group {
- #if os(iOS)
- TabView(selection: $idx) {
- ForEach(slides.indices, id: \.self) { i in
- slidePage(slides[i]).tag(i)
- }
- }
- .tabViewStyle(.page(indexDisplayMode: .never))
- #else
- if slides.indices.contains(idx) {
- slidePage(slides[idx])
- .id(idx)
- .transition(.opacity)
- }
- #endif
- }
- #if os(macOS)
- .animation(Motion.settle, value: idx)
- #endif
- .detailScreen(title, subtitle: subtitle)
- .glassBottomBar {
- VStack(spacing: 10) {
- HStack(spacing: 6) {
- ForEach(slides.indices, id: \.self) { i in
- Capsule()
- .fill(i == idx ? Color.accentColor : Color(.tertiarySystemFill))
- .frame(width: i == idx ? 18 : 6, height: 6)
- }
- }
- .animation(Motion.settle, value: idx)
- HStack(spacing: 10) {
- if idx > 0 {
- Button {
- withAnimation { idx -= 1 }
- } label: {
- Image(systemName: "chevron.left")
- .frame(minWidth: 24)
- }
- #if os(macOS)
- .buttonStyle(.bordered)
- #else
- .buttonStyle(.glass)
- #endif
- .controlSize(.large)
- }
- if idx < slides.count - 1 {
- PrimaryButton(label: vm.tr("net_slide_next")) {
- withAnimation { idx += 1 }
- }
- } else {
- PrimaryButton(label: vm.tr("net_slide_start")) {
- vm.go(next)
- }
- }
- }
- }
- .padding(.horizontal)
- .padding(.top, 8)
- .padding(.bottom, 4)
- #if os(macOS)
- .macPage(MacChrome.actionWidth)
- #endif
- }
- }
-
- private func slidePage(_ slide: J) -> some View {
- ScrollView {
- SlideBody(slide: slide)
- .padding(.horizontal, slideInset)
- .padding(.top, 22)
- .padding(.bottom, 28)
- .frame(maxWidth: 860)
- .frame(maxWidth: .infinity, alignment: .top)
- }
- }
-
- private var slideInset: CGFloat {
- #if os(macOS)
- MacChrome.pageInset
- #else
- 16
- #endif
- }
-}
-
-/// One explanation slide: label, title, tip and numbered points.
-struct SlideBody: View {
- let slide: J
-
- var body: some View {
- slideStack(slide)
- }
-
- private func slideStack(_ slide: J) -> some View {
- VStack(alignment: .leading, spacing: 18) {
- Text(slide.str("kicker"))
- #if os(macOS)
- .font(.subheadline.weight(.semibold))
- #else
- .font(.caption.weight(.semibold))
- #endif
- .foregroundStyle(Color.accentColor)
- Text(slide.str("title"))
- #if os(macOS)
- .font(.title.bold())
- #else
- .font(.title2.bold())
- #endif
- .fixedSize(horizontal: false, vertical: true)
- if let tip = slide.strOrNull("tip") {
- HStack(alignment: .top, spacing: 12) {
- Image(systemName: "lightbulb.fill")
- .foregroundStyle(Color.accentColor)
- .padding(.top, 2)
- Text(tip)
- #if os(macOS)
- .font(.body)
- #else
- .font(.subheadline)
- #endif
- .fixedSize(horizontal: false, vertical: true)
- .frame(maxWidth: .infinity, alignment: .leading)
- }
- .padding(16)
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(Color.accentColor.opacity(0.12), in: RoundedRectangle(cornerRadius: 16, style: .continuous))
- }
- slideLines(slide.strs("lines"))
- }
- .frame(maxWidth: .infinity, alignment: .leading)
- }
-
- private func slideLines(_ lines: [String]) -> some View {
- let points = studyPoints(lines)
- return VStack(alignment: .leading, spacing: 10) {
- ForEach(Array(points.enumerated()), id: \.offset) { _, point in
- HStack(alignment: .top, spacing: 14) {
- Text(point.0)
- .font(.callout.weight(.bold))
- .foregroundStyle(Color.accentColor)
- .frame(minWidth: 28, minHeight: 28)
- .background(Color.accentColor.opacity(0.14), in: Circle())
- Text(point.1)
- #if os(macOS)
- .font(.title3)
- #else
- .font(.body)
- #endif
- .fixedSize(horizontal: false, vertical: true)
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding(.top, 3)
- }
- .padding(.horizontal, 16)
- .padding(.vertical, 14)
- .frame(maxWidth: .infinity, alignment: .leading)
- .background {
- RoundedRectangle(cornerRadius: 16, style: .continuous)
- .fill(Color.primary.opacity(0.045))
- }
- .overlay {
- RoundedRectangle(cornerRadius: 16, style: .continuous)
- .strokeBorder(Color.primary.opacity(0.08), lineWidth: 1)
- }
- }
- }
- }
-
- /// Lines that end in the middle of a sentence stay one point. A leading
- /// "1. " is the point number, not part of the sentence.
- private func studyPoints(_ lines: [String]) -> [(String, String)] {
- var chunks: [String] = []
- var buffer = ""
- func flush() {
- let text = buffer.trimmingCharacters(in: .whitespacesAndNewlines)
- if !text.isEmpty { chunks.append(text) }
- buffer = ""
- }
- for line in lines {
- let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
- if trimmed.isEmpty { continue }
- if buffer.isEmpty {
- buffer = trimmed
- } else if continuesStudyPoint(buffer) {
- buffer += " " + trimmed
- } else {
- flush()
- buffer = trimmed
- }
- }
- flush()
- return chunks.enumerated().map { index, chunk in
- let pattern = #"^(\d+)\.\s+(.*)$"#
- if let regex = try? NSRegularExpression(pattern: pattern),
- let match = regex.firstMatch(in: chunk, range: NSRange(chunk.startIndex..., in: chunk)),
- let number = Range(match.range(at: 1), in: chunk),
- let rest = Range(match.range(at: 2), in: chunk),
- !continuesStudyPoint(String(chunk[rest])) {
- return (String(chunk[number]), String(chunk[rest]))
- }
- return (String(index + 1), chunk)
- }
- }
-
- private func continuesStudyPoint(_ text: String) -> Bool {
- guard let last = text.last else { return false }
- return last == "," || last == ":" || last == ";" || last == "–" || last == "-" || last == "("
- }
-}
-
-struct LitYearsScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- let years: [(String, String, Route)] = [
- ("lit_year1", "lit_sub", .litMap),
- ("lit_year2", "lit2_sub", .lit2Map),
- ("lit_year3", "lit3_sub", .lit3Map),
- ("lit_year4", "lit4_sub", .lit4Map),
- ]
- MacFillGrid(count: years.count, minWidth: 260, minHeight: 200) { i in
- NavigationLink(value: years[i].2) {
- MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).", progress: routeProgress(vm, years[i].2))
- }
- .buttonStyle(PressScaleStyle())
- }
- .navigationTitle(vm.tr("Literatura"))
- .navigationSubtitle(vm.tr("lit_years_sub"))
- #else
- List {
- NavigationLink(value: Route.litMap) {
- ListRowLabel(title: vm.tr("lit_year1"), subtitle: vm.tr("lit_sub"), progress: routeProgress(vm, Route.litMap))
- }
- NavigationLink(value: Route.lit2Map) {
- ListRowLabel(title: vm.tr("lit_year2"), subtitle: vm.tr("lit2_sub"), progress: routeProgress(vm, Route.lit2Map))
- }
- NavigationLink(value: Route.lit3Map) {
- ListRowLabel(title: vm.tr("lit_year3"), subtitle: vm.tr("lit3_sub"), progress: routeProgress(vm, Route.lit3Map))
- }
- NavigationLink(value: Route.lit4Map) {
- ListRowLabel(title: vm.tr("lit_year4"), subtitle: vm.tr("lit4_sub"), progress: routeProgress(vm, Route.lit4Map))
- }
- }
- .navigationTitle(vm.tr("Literatura"))
- .navigationSubtitle(vm.tr("lit_years_sub"))
- .appListStyle()
- #endif
- }
-}
-
-struct MatYearsScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- let years: [(String, String, Route, String)] = [
- ("mat0_year", "mat0_sub", .mat0Map, "ZŠ"),
- ("mat_year1", "mat_sub", .matMap, "1."),
- ("mat_year2", "mat2_sub", .mat2Map, "2."),
- ("mat_year3", "mat3_sub", .mat3Map, "3."),
- ("mat_year4", "mat4_sub", .mat4Map, "4."),
- ]
- MacFillGrid(count: years.count, minWidth: 260, minHeight: 200) { i in
- NavigationLink(value: years[i].2) {
- MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: years[i].3, progress: routeProgress(vm, years[i].2))
- }
- .buttonStyle(PressScaleStyle())
- }
- .navigationTitle(vm.tr("Matematika"))
- .navigationSubtitle(vm.tr("mat_years_sub"))
- #else
- List {
- NavigationLink(value: Route.mat0Map) {
- ListRowLabel(title: vm.tr("mat0_year"), subtitle: vm.tr("mat0_sub"), progress: routeProgress(vm, Route.mat0Map))
- }
- NavigationLink(value: Route.matMap) {
- ListRowLabel(title: vm.tr("mat_year1"), subtitle: vm.tr("mat_sub"), progress: routeProgress(vm, Route.matMap))
- }
- NavigationLink(value: Route.mat2Map) {
- ListRowLabel(title: vm.tr("mat_year2"), subtitle: vm.tr("mat2_sub"), progress: routeProgress(vm, Route.mat2Map))
- }
- NavigationLink(value: Route.mat3Map) {
- ListRowLabel(title: vm.tr("mat_year3"), subtitle: vm.tr("mat3_sub"), progress: routeProgress(vm, Route.mat3Map))
- }
- NavigationLink(value: Route.mat4Map) {
- ListRowLabel(title: vm.tr("mat_year4"), subtitle: vm.tr("mat4_sub"), progress: routeProgress(vm, Route.mat4Map))
- }
- }
- .navigationTitle(vm.tr("Matematika"))
- .navigationSubtitle(vm.tr("mat_years_sub"))
- .appListStyle()
- #endif
- }
-}
-
-struct CsYearsScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- let years: [(String, String, Route)] = [
- ("cs_year1", "cs_sub", .csMap),
- ("cs_year2", "cs2_sub", .cs2Map),
- ("cs_year3", "cs3_sub", .cs3Map),
- ("cs_year4", "cs4_sub", .cs4Map),
- ]
- MacFillGrid(count: years.count, minWidth: 260, minHeight: 200) { i in
- NavigationLink(value: years[i].2) {
- MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).", progress: routeProgress(vm, years[i].2))
- }
- .buttonStyle(PressScaleStyle())
- }
- .navigationTitle(vm.tr("Programování"))
- .navigationSubtitle(vm.tr("cs_years_sub"))
- #else
- List {
- NavigationLink(value: Route.csMap) {
- ListRowLabel(title: vm.tr("cs_year1"), subtitle: vm.tr("cs_sub"), progress: routeProgress(vm, Route.csMap))
- }
- NavigationLink(value: Route.cs2Map) {
- ListRowLabel(title: vm.tr("cs_year2"), subtitle: vm.tr("cs2_sub"), progress: routeProgress(vm, Route.cs2Map))
- }
- NavigationLink(value: Route.cs3Map) {
- ListRowLabel(title: vm.tr("cs_year3"), subtitle: vm.tr("cs3_sub"), progress: routeProgress(vm, Route.cs3Map))
- }
- NavigationLink(value: Route.cs4Map) {
- ListRowLabel(title: vm.tr("cs_year4"), subtitle: vm.tr("cs4_sub"), progress: routeProgress(vm, Route.cs4Map))
- }
- }
- .navigationTitle(vm.tr("Programování"))
- .navigationSubtitle(vm.tr("cs_years_sub"))
- .appListStyle()
- #endif
- }
-}
-
-struct SwYearsScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- let years: [(String, String, Route)] = [
- ("sw_year1", "sw_sub", .swMap),
- ("sw_year2", "sw2_sub", .sw2Map),
- ("sw_year3", "sw3_sub", .sw3Map),
- ("sw_year4", "sw4_sub", .sw4Map),
- ]
- MacFillGrid(count: years.count, minWidth: 260, minHeight: 200) { i in
- NavigationLink(value: years[i].2) {
- MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).", progress: routeProgress(vm, years[i].2))
- }
- .buttonStyle(PressScaleStyle())
- }
- .navigationTitle(vm.tr("Programové vybavení"))
- .navigationSubtitle(vm.tr("sw_years_sub"))
- #else
- List {
- NavigationLink(value: Route.swMap) {
- ListRowLabel(title: vm.tr("sw_year1"), subtitle: vm.tr("sw_sub"), progress: routeProgress(vm, Route.swMap))
- }
- NavigationLink(value: Route.sw2Map) {
- ListRowLabel(title: vm.tr("sw_year2"), subtitle: vm.tr("sw2_sub"), progress: routeProgress(vm, Route.sw2Map))
- }
- NavigationLink(value: Route.sw3Map) {
- ListRowLabel(title: vm.tr("sw_year3"), subtitle: vm.tr("sw3_sub"), progress: routeProgress(vm, Route.sw3Map))
- }
- NavigationLink(value: Route.sw4Map) {
- ListRowLabel(title: vm.tr("sw_year4"), subtitle: vm.tr("sw4_sub"), progress: routeProgress(vm, Route.sw4Map))
- }
- }
- .navigationTitle(vm.tr("Programové vybavení"))
- .navigationSubtitle(vm.tr("sw_years_sub"))
- .appListStyle()
- #endif
- }
-}
-
-struct PgYearsScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- let years: [(String, String, Route)] = [
- ("pg_year1", "pg_sub", .pgMap),
- ("pg_year2", "pg2_sub", .pg2Map),
- ("pg_year3", "pg3_sub", .pg3Map),
- ("pg_year4", "pg4_sub", .pg4Map),
- ]
- MacFillGrid(count: years.count, minWidth: 260, minHeight: 200) { i in
- NavigationLink(value: years[i].2) {
- MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).", progress: routeProgress(vm, years[i].2))
- }
- .buttonStyle(PressScaleStyle())
- }
- .navigationTitle(vm.tr("Prezentační grafika"))
- .navigationSubtitle(vm.tr("pg_years_sub"))
- #else
- List {
- NavigationLink(value: Route.pgMap) {
- ListRowLabel(title: vm.tr("pg_year1"), subtitle: vm.tr("pg_sub"), progress: routeProgress(vm, Route.pgMap))
- }
- NavigationLink(value: Route.pg2Map) {
- ListRowLabel(title: vm.tr("pg_year2"), subtitle: vm.tr("pg2_sub"), progress: routeProgress(vm, Route.pg2Map))
- }
- NavigationLink(value: Route.pg3Map) {
- ListRowLabel(title: vm.tr("pg_year3"), subtitle: vm.tr("pg3_sub"), progress: routeProgress(vm, Route.pg3Map))
- }
- NavigationLink(value: Route.pg4Map) {
- ListRowLabel(title: vm.tr("pg_year4"), subtitle: vm.tr("pg4_sub"), progress: routeProgress(vm, Route.pg4Map))
- }
- }
- .navigationTitle(vm.tr("Prezentační grafika"))
- .navigationSubtitle(vm.tr("pg_years_sub"))
- .appListStyle()
- #endif
- }
-}
-
-struct TgYearsScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- let years: [(String, String, Route)] = [
- ("tg_year1", "tg_sub", .tgMap),
- ("tg_year2", "tg2_sub", .tg2Map),
- ("tg_year3", "tg3_sub", .tg3Map),
- ("tg_year4", "tg4_sub", .tg4Map),
- ]
- MacFillGrid(count: years.count, minWidth: 260, minHeight: 200) { i in
- NavigationLink(value: years[i].2) {
- MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).", progress: routeProgress(vm, years[i].2))
- }
- .buttonStyle(PressScaleStyle())
- }
- .navigationTitle(vm.tr("Technická grafika"))
- .navigationSubtitle(vm.tr("tg_years_sub"))
- #else
- List {
- NavigationLink(value: Route.tgMap) {
- ListRowLabel(title: vm.tr("tg_year1"), subtitle: vm.tr("tg_sub"), progress: routeProgress(vm, Route.tgMap))
- }
- NavigationLink(value: Route.tg2Map) {
- ListRowLabel(title: vm.tr("tg_year2"), subtitle: vm.tr("tg2_sub"), progress: routeProgress(vm, Route.tg2Map))
- }
- NavigationLink(value: Route.tg3Map) {
- ListRowLabel(title: vm.tr("tg_year3"), subtitle: vm.tr("tg3_sub"), progress: routeProgress(vm, Route.tg3Map))
- }
- NavigationLink(value: Route.tg4Map) {
- ListRowLabel(title: vm.tr("tg_year4"), subtitle: vm.tr("tg4_sub"), progress: routeProgress(vm, Route.tg4Map))
- }
- }
- .navigationTitle(vm.tr("Technická grafika"))
- .navigationSubtitle(vm.tr("tg_years_sub"))
- .appListStyle()
- #endif
- }
-}
-
-struct FyzYearsScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- let years: [(String, String, Route)] = [
- ("fyz_year1", "fyz_sub", .fyzMap),
- ("fyz_year2", "fyz2_sub", .fyz2Map),
- ("fyz_year3", "fyz3_sub", .fyz3Map),
- ("fyz_year4", "fyz4_sub", .fyz4Map),
- ]
- MacFillGrid(count: years.count, minWidth: 260, minHeight: 200) { i in
- NavigationLink(value: years[i].2) {
- MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).", progress: routeProgress(vm, years[i].2))
- }
- .buttonStyle(PressScaleStyle())
- }
- .navigationTitle(vm.tr("Fyzika"))
- .navigationSubtitle(vm.tr("fyz_years_sub"))
- #else
- List {
- NavigationLink(value: Route.fyzMap) {
- ListRowLabel(title: vm.tr("fyz_year1"), subtitle: vm.tr("fyz_sub"), progress: routeProgress(vm, Route.fyzMap))
- }
- NavigationLink(value: Route.fyz2Map) {
- ListRowLabel(title: vm.tr("fyz_year2"), subtitle: vm.tr("fyz2_sub"), progress: routeProgress(vm, Route.fyz2Map))
- }
- NavigationLink(value: Route.fyz3Map) {
- ListRowLabel(title: vm.tr("fyz_year3"), subtitle: vm.tr("fyz3_sub"), progress: routeProgress(vm, Route.fyz3Map))
- }
- NavigationLink(value: Route.fyz4Map) {
- ListRowLabel(title: vm.tr("fyz_year4"), subtitle: vm.tr("fyz4_sub"), progress: routeProgress(vm, Route.fyz4Map))
- }
- }
- .navigationTitle(vm.tr("Fyzika"))
- .navigationSubtitle(vm.tr("fyz_years_sub"))
- .appListStyle()
- #endif
- }
-}
-
-struct SciMapScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- MacFillGrid(count: 2, minWidth: 260, minHeight: 200) { i in
- if i == 0 {
- NavigationLink(value: Route.chemMap) {
- MacLinkCard(title: vm.tr("Chemie"), subtitle: vm.tr("chem_sub"), systemImage: "flask.fill", progress: routeProgress(vm, .chemMap))
- }
- .buttonStyle(PressScaleStyle())
- } else {
- NavigationLink(value: Route.bioMap) {
- MacLinkCard(title: vm.tr("Biologie"), subtitle: vm.tr("bio_sub"), systemImage: "leaf.fill", progress: routeProgress(vm, .bioMap))
- }
- .buttonStyle(PressScaleStyle())
- }
- }
- .navigationTitle(vm.tr("Základy přírodních věd"))
- .navigationSubtitle(vm.tr("sci_sub"))
- #else
- List {
- NavigationLink(value: Route.chemMap) {
- Label {
- ListRowLabel(title: vm.tr("Chemie"), subtitle: vm.tr("chem_sub"), progress: routeProgress(vm, .chemMap))
- } icon: {
- Image(systemName: "flask.fill")
- }
- }
- NavigationLink(value: Route.bioMap) {
- Label {
- ListRowLabel(title: vm.tr("Biologie"), subtitle: vm.tr("bio_sub"), progress: routeProgress(vm, .bioMap))
- } icon: {
- Image(systemName: "leaf.fill")
- }
- }
- }
- .navigationTitle(vm.tr("Základy přírodních věd"))
- .navigationSubtitle(vm.tr("sci_sub"))
- .appListStyle()
- #endif
- }
-}
-
-struct CzechMapScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- MacFillGrid(count: 3, minWidth: 260, minHeight: 200) { i in
- switch i {
- case 1:
- NavigationLink(value: Route.mluvnice) {
- MacLinkCard(title: vm.tr("Mluvnice"), subtitle: vm.tr("mluv_sub"), systemImage: "textformat", progress: routeProgress(vm, .mluvnice))
- }
- .buttonStyle(PressScaleStyle())
- case 2:
- NavigationLink(value: Route.readingList) {
- MacLinkCard(title: vm.tr("Maturitní četba"), subtitle: vm.tr("reading_sub"), systemImage: "books.vertical")
- }
- .buttonStyle(PressScaleStyle())
- default:
- NavigationLink(value: Route.litYears) {
- MacLinkCard(title: vm.tr("Literatura"), subtitle: vm.tr("lit_years_sub"), systemImage: "book.closed", progress: routeProgress(vm, .litYears))
- }
- .buttonStyle(PressScaleStyle())
- }
- }
- .navigationTitle(vm.tr("Český jazyk a literatura"))
- .navigationSubtitle(vm.tr("czech_sub"))
- #else
- List {
- NavigationLink(value: Route.litYears) {
- Label {
- ListRowLabel(title: vm.tr("Literatura"), subtitle: vm.tr("lit_years_sub"), progress: routeProgress(vm, .litYears))
- } icon: {
- Image(systemName: "book.closed")
- }
- }
- NavigationLink(value: Route.mluvnice) {
- Label {
- ListRowLabel(title: vm.tr("Mluvnice"), subtitle: vm.tr("mluv_sub"), progress: routeProgress(vm, .mluvnice))
- } icon: {
- Image(systemName: "textformat")
- }
- }
- NavigationLink(value: Route.readingList) {
- Label {
- ListRowLabel(title: vm.tr("Maturitní četba"), subtitle: vm.tr("reading_sub"))
- } icon: {
- Image(systemName: "books.vertical")
- }
- }
- }
- .navigationTitle(vm.tr("Český jazyk a literatura"))
- .navigationSubtitle(vm.tr("czech_sub"))
- .appListStyle()
- #endif
- }
-}
-
-struct BookListScreen: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- #if os(macOS)
- macBooks
- #else
- iosBooks
- #endif
- }
-
- #if os(macOS)
- private var macBooks: some View {
- ScrollView {
- LazyVGrid(columns: [GridItem(.adaptive(minimum: 300), spacing: 16)], spacing: 16) {
- ForEach(Array(vm.content.books.enumerated()), id: \.offset) { _, b in
- let parts = bookTitleParts(b.str("title"))
- let quizDone = !b.arr("quiz").isEmpty && vm.progress.bookQuiz(b.str("id"))
- let plotDone = !b.arr("plot").isEmpty && vm.progress.bookPlot(b.str("id"))
- NavigationLink(value: Route.book(b.str("id"))) {
- VStack(alignment: .leading, spacing: 8) {
- Image(systemName: "book.fill")
- .font(.title2)
- .foregroundStyle(.tint)
- Text(parts.title)
- .font(.title3.weight(.semibold))
- .foregroundStyle(.primary)
- .multilineTextAlignment(.leading)
- if let author = parts.author {
- Text(author)
- .font(.body)
- .foregroundStyle(.secondary)
- }
- Spacer(minLength: 0)
- HStack(spacing: 8) {
- let genre = b.str("genre")
- Text(genre.isEmpty ? vm.tr(b.str("subKey")) : genre)
- .font(.subheadline)
- .foregroundStyle(.secondary)
- Spacer(minLength: 0)
- if quizDone || plotDone {
- Image(systemName: "checkmark.circle.fill")
- .foregroundStyle(.green)
- .accessibilityLabel(vm.tr("book_done"))
- }
- }
- }
- .padding(18)
- .frame(maxWidth: .infinity, minHeight: 168, alignment: .topLeading)
- .softSurface(cornerRadius: 18)
- }
- .buttonStyle(PressScaleStyle())
- }
- }
- .padding(MacChrome.pageInset)
- }
- .navigationTitle(vm.tr("Maturitní četba"))
- .navigationSubtitle(vm.tr("reading_sub"))
- }
- #endif
-
- #if os(iOS)
- private var iosBooks: some View {
- List {
- ForEach(Array(vm.content.books.enumerated()), id: \.offset) { _, b in
- let parts = bookTitleParts(b.str("title"))
- let quizDone = !b.arr("quiz").isEmpty && vm.progress.bookQuiz(b.str("id"))
- let plotDone = !b.arr("plot").isEmpty && vm.progress.bookPlot(b.str("id"))
- NavigationLink(value: Route.book(b.str("id"))) {
- HStack(alignment: .top, spacing: 12) {
- Image(systemName: "book.fill")
- #if os(macOS)
- .font(.title2)
- #else
- .font(.title3)
- #endif
- .foregroundStyle(.tint)
- .frame(width: 28)
- VStack(alignment: .leading, spacing: 4) {
- Text(parts.title)
- #if os(macOS)
- .font(.title3.weight(.semibold))
- #else
- .font(.body.weight(.semibold))
- #endif
- if let author = parts.author {
- Text(author)
- #if os(macOS)
- .font(.body)
- #else
- .font(.subheadline)
- #endif
- .foregroundStyle(.secondary)
- }
- HStack(spacing: 8) {
- let genre = b.str("genre")
- Text(genre.isEmpty ? vm.tr(b.str("subKey")) : genre)
- #if os(macOS)
- .font(.subheadline)
- #else
- .font(.caption)
- #endif
- .foregroundStyle(.secondary)
- if quizDone || plotDone {
- Label(vm.tr("book_done"), systemImage: "checkmark.circle.fill")
- #if os(macOS)
- .font(.subheadline.weight(.semibold))
- #else
- .font(.caption.weight(.semibold))
- #endif
- .foregroundStyle(.green)
- }
- }
- }
- }
- #if os(macOS)
- .padding(.vertical, 4)
- #endif
- }
- }
- }
- .navigationTitle(vm.tr("Maturitní četba"))
- .navigationSubtitle(vm.tr("reading_sub"))
- .appListStyle()
- }
- #endif
-}
-
-struct BookScreen: View {
- @ObservedObject var vm: AppModel
- let id: String
-
- var body: some View {
- if let b = vm.content.book(id) {
- let hasQuiz = !b.arr("quiz").isEmpty
- let hasPlot = !b.arr("plot").isEmpty
- let notes = b.arr("notes")
- let quizDone = vm.progress.bookQuiz(id)
- let plotDone = vm.progress.bookPlot(id)
- let parts = bookTitleParts(b.str("title"))
- ScrollView {
- VStack(alignment: .leading, spacing: 28) {
- GlassCard {
- VStack(alignment: .leading, spacing: 10) {
- if let author = parts.author {
- Text(author)
- #if os(macOS)
- .font(.title3)
- #else
- .font(.subheadline)
- #endif
- .foregroundStyle(.secondary)
- }
- if !b.str("genre").isEmpty {
- Text(b.str("genre"))
- .font(.subheadline.weight(.semibold))
- .foregroundStyle(.tint)
- }
- Text(vm.tr("book_study_hint"))
- #if os(macOS)
- .font(.title3)
- #else
- .font(.body)
- #endif
- .foregroundStyle(.secondary)
- .fixedSize(horizontal: false, vertical: true)
- }
- }
-
- if hasQuiz || hasPlot {
- VStack(alignment: .leading, spacing: 14) {
- Text(vm.tr("book_practice"))
- #if os(macOS)
- .font(.title.weight(.semibold))
- #else
- .font(.title2.weight(.semibold))
- #endif
- #if os(macOS)
- LazyVGrid(columns: [GridItem(.adaptive(minimum: 320), spacing: 16)], spacing: 16) {
- if hasQuiz {
- NavigationLink(value: Route.bookQuiz(id)) {
- BookActionCard(
- title: vm.tr("book_quiz_heading"),
- subtitle: vm.tr("lit_quiz_sub"),
- systemImage: "questionmark.circle.fill",
- done: quizDone,
- doneLabel: vm.tr("book_done")
- )
- }
- .buttonStyle(PressScaleStyle())
- }
- if hasPlot {
- NavigationLink(value: Route.bookPlot(id)) {
- BookActionCard(
- title: vm.tr("lit_plot_title"),
- subtitle: vm.tr("lit_plot_sub"),
- systemImage: "list.number",
- done: plotDone,
- doneLabel: vm.tr("book_done")
- )
- }
- .buttonStyle(PressScaleStyle())
- }
- }
- #else
- if hasQuiz {
- NavigationLink(value: Route.bookQuiz(id)) {
- BookActionCard(
- title: vm.tr("book_quiz_heading"),
- subtitle: vm.tr("lit_quiz_sub"),
- systemImage: "questionmark.circle.fill",
- done: quizDone,
- doneLabel: vm.tr("book_done")
- )
- }
- .buttonStyle(PressScaleStyle())
- }
- if hasPlot {
- NavigationLink(value: Route.bookPlot(id)) {
- BookActionCard(
- title: vm.tr("lit_plot_title"),
- subtitle: vm.tr("lit_plot_sub"),
- systemImage: "list.number",
- done: plotDone,
- doneLabel: vm.tr("book_done")
- )
- }
- .buttonStyle(PressScaleStyle())
- }
- #endif
- }
- }
-
- if !notes.isEmpty {
- VStack(alignment: .leading, spacing: 14) {
- Text(vm.tr("book_notes"))
- #if os(macOS)
- .font(.title.weight(.semibold))
- #else
- .font(.title2.weight(.semibold))
- #endif
- #if os(macOS)
- LazyVGrid(columns: [GridItem(.adaptive(minimum: 360), spacing: 16)], alignment: .leading, spacing: 16) {
- ForEach(Array(notes.enumerated()), id: \.offset) { _, note in
- BookNoteCard(note: note)
- }
- }
- #else
- ForEach(Array(notes.enumerated()), id: \.offset) { _, note in
- BookNoteCard(note: note)
- }
- #endif
- }
- }
- }
- #if os(macOS)
- .padding(MacChrome.pageInset)
- .frame(maxWidth: .infinity, alignment: .leading)
- #else
- .padding()
- .padding(.bottom, 24)
- #endif
- }
- .navigationTitle(parts.title)
- .navigationSubtitle(parts.author ?? vm.tr(b.str("subKey")))
- }
- }
-}
-
-private struct BookActionCard: View {
- let title: String
- let subtitle: String
- let systemImage: String
- let done: Bool
- let doneLabel: String
-
- var body: some View {
- HStack(alignment: .center, spacing: 16) {
- Image(systemName: systemImage)
- #if os(macOS)
- .font(.largeTitle)
- #else
- .font(.title)
- #endif
- .foregroundStyle(.tint)
- .frame(width: 44)
- VStack(alignment: .leading, spacing: 4) {
- Text(title)
- #if os(macOS)
- .font(.title3.weight(.semibold))
- #else
- .font(.headline)
- #endif
- .foregroundStyle(.primary)
- Text(subtitle)
- #if os(macOS)
- .font(.body)
- #else
- .font(.subheadline)
- #endif
- .foregroundStyle(.secondary)
- .fixedSize(horizontal: false, vertical: true)
- }
- Spacer(minLength: 8)
- if done {
- Image(systemName: "checkmark.circle.fill")
- #if os(macOS)
- .font(.title2)
- #else
- .font(.title3)
- #endif
- .foregroundStyle(.green)
- .accessibilityLabel(doneLabel)
- } else {
- Image(systemName: "chevron.right")
- .font(.body.weight(.semibold))
- .foregroundStyle(.tertiary)
- }
- }
- #if os(macOS)
- .padding(20)
- #else
- .padding(16)
- #endif
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
- .softSurface(cornerRadius: 18)
- }
-}
-
-private struct BookNoteCard: View {
- let note: J
-
- var body: some View {
- GlassCard {
- VStack(alignment: .leading, spacing: 12) {
- Label(note.str("title"), systemImage: noteSymbol(note.str("icon")))
- #if os(macOS)
- .font(.title2.weight(.semibold))
- #else
- .font(.headline)
- #endif
- VStack(alignment: .leading, spacing: 10) {
- ForEach(Array(note.strs("lines").enumerated()), id: \.offset) { _, line in
- HStack(alignment: .top, spacing: 10) {
- Text("•")
- .foregroundStyle(.secondary)
- Text(line)
- #if os(macOS)
- .font(.title3)
- #else
- .font(.body)
- #endif
- .fixedSize(horizontal: false, vertical: true)
- }
- }
- }
- }
- }
- }
-}
-
-private func bookTitleParts(_ title: String) -> (author: String?, title: String) {
- let separators = [" – ", " — ", " - "]
- for sep in separators {
- if let range = title.range(of: sep) {
- let author = String(title[.. String {
- switch icon {
- case "book": return "book.fill"
- case "globe": return "globe.europe.africa.fill"
- case "people": return "person.2.fill"
- case "bulb": return "lightbulb.fill"
- default: return "text.alignleft"
- }
-}
-
-struct RouteDestination: View {
- @ObservedObject var vm: AppModel
- let route: Route
-
- var body: some View {
- switch route {
- case .welcome, .subjects:
- PracticeHome(vm: vm)
- case .stats:
- StatsScreen(vm: vm)
- case .deHome:
- DeHomeScreen(vm: vm)
- case .deMap(let year):
- LessonListScreen(
- vm: vm,
- title: vm.tr("de_year\(year)"),
- subtitle: vm.tr("de_y\(year)_sub"),
- rows: vm.content.deYear(year).map { l in
- let id = l.int("id")
- return LessonRow(
- id: "d\(year)\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.deDone(year, id),
- destination: .deLesson(year, id)
- )
- }
- )
- case .deLesson(let year, let id):
- if let l = vm.content.deLesson(year, id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .deEx(year, id))
- }
- case .deEx(let year, let id):
- EngExScreen(vm: vm, year: year, id: id, voice: "de-DE", lesson: { $0.content.deLesson($1, $2) }, mark: { $0.markDe($1, $2) })
- case .roadmap:
- RoadmapScreen(vm: vm)
- case .unitMap(let id):
- UnitMapScreen(vm: vm, unitId: id)
- case .germanEx(let unit, let ex):
- GermanExercise(vm: vm, unitId: unit, ex: ex)
- case .vocab(let id):
- VocabExercise(vm: vm, unitId: id)
- case .netYears:
- NetYearsScreen(vm: vm)
- case .netMap:
- LessonListScreen(
- vm: vm,
- title: vm.tr("net_year1"),
- subtitle: vm.tr("net_sub"),
- rows: vm.content.netLessons.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "n\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.netDone(id),
- destination: .netLesson(id)
- )
- }
- )
- case .netLesson(let id):
- if let l = vm.content.netLesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .netEx(id))
- }
- case .netEx(let id):
- NetQuizScreen(vm: vm, id: id)
- case .net2Map:
- ItYearList(vm: vm, course: "net2", titleKey: "net_year2", subKey: "net2_sub") { .net2Lesson($0) }
- case .net2Lesson(let id):
- ItSlides(vm: vm, course: "net2", id: id, next: .net2Ex(id))
- case .net2Ex(let id):
- ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("net2", id)) { vm.markCourse("net2", id) }
- case .net3Map:
- ItYearList(vm: vm, course: "net3", titleKey: "net_year3", subKey: "net3_sub") { .net3Lesson($0) }
- case .net3Lesson(let id):
- ItSlides(vm: vm, course: "net3", id: id, next: .net3Ex(id))
- case .net3Ex(let id):
- ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("net3", id)) { vm.markCourse("net3", id) }
- case .net4Map:
- ItYearList(vm: vm, course: "net4", titleKey: "net_year4", subKey: "net4_sub") { .net4Lesson($0) }
- case .net4Lesson(let id):
- ItSlides(vm: vm, course: "net4", id: id, next: .net4Ex(id))
- case .net4Ex(let id):
- ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("net4", id)) { vm.markCourse("net4", id) }
- case .hwYears:
- HwYearsScreen(vm: vm)
- case .hwMap:
- LessonListScreen(
- vm: vm,
- title: vm.tr("hw_year1"),
- subtitle: vm.tr("hw_sub"),
- rows: vm.content.hw.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "h\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.hwDone(id),
- destination: .hwLesson(id)
- )
- }
- )
- case .hwLesson(let id):
- if let l = vm.content.hwLesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .hwEx(id))
- }
- case .hwEx(let id):
- HwQuizScreen(vm: vm, id: id)
- case .hw2Map:
- ItYearList(vm: vm, course: "hw2", titleKey: "hw_year2", subKey: "hw2_sub") { .hw2Lesson($0) }
- case .hw2Lesson(let id):
- ItSlides(vm: vm, course: "hw2", id: id, next: .hw2Ex(id))
- case .hw2Ex(let id):
- ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("hw2", id)) { vm.markCourse("hw2", id) }
- case .hw3Map:
- ItYearList(vm: vm, course: "hw3", titleKey: "hw_year3", subKey: "hw3_sub") { .hw3Lesson($0) }
- case .hw3Lesson(let id):
- ItSlides(vm: vm, course: "hw3", id: id, next: .hw3Ex(id))
- case .hw3Ex(let id):
- ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("hw3", id)) { vm.markCourse("hw3", id) }
- case .hw4Map:
- ItYearList(vm: vm, course: "hw4", titleKey: "hw_year4", subKey: "hw4_sub") { .hw4Lesson($0) }
- case .hw4Lesson(let id):
- ItSlides(vm: vm, course: "hw4", id: id, next: .hw4Ex(id))
- case .hw4Ex(let id):
- ChoiceLessonScreen(vm: vm, lesson: vm.content.itLesson("hw4", id)) { vm.markCourse("hw4", id) }
- case .onYears:
- OnYearsScreen(vm: vm)
- case .enYears:
- EnYearsScreen(vm: vm)
- case .enMap(let year):
- LessonListScreen(
- vm: vm,
- title: vm.tr("en_year\(year)"),
- subtitle: vm.tr("en_y\(year)_sub"),
- rows: vm.content.enYear(year).map { l in
- let id = l.int("id")
- return LessonRow(
- id: "e\(year)\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.enDone(year, id),
- destination: .enLesson(year, id)
- )
- }
- )
- case .enLesson(let year, let id):
- if let l = vm.content.enLesson(year, id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .enEx(year, id))
- }
- case .enEx(let year, let id):
- EngExScreen(vm: vm, year: year, id: id)
- case .onMap:
- LessonListScreen(
- vm: vm,
- title: vm.tr("on_year1"),
- subtitle: vm.tr("on_sub"),
- rows: vm.content.on.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "o\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.onDone(id),
- destination: .onLesson(id)
- )
- }
- )
- case .onLesson(let id):
- if let l = vm.content.onLesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .onEx(id))
- }
- case .onEx(let id):
- OnQuizScreen(vm: vm, id: id)
- case .on2Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("on_year2"),
- subtitle: vm.tr("on2_sub"),
- rows: vm.content.on2.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "o2\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.on2Done(id),
- destination: .on2Lesson(id)
- )
- }
- )
- case .on2Lesson(let id):
- if let l = vm.content.on2Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .on2Ex(id))
- }
- case .on2Ex(let id):
- On2QuizScreen(vm: vm, id: id)
- case .on3Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("on_year3"),
- subtitle: vm.tr("on3_sub"),
- rows: vm.content.on3.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "o3\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.on3Done(id),
- destination: .on3Lesson(id)
- )
- }
- )
- case .on3Lesson(let id):
- if let l = vm.content.on3Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .on3Ex(id))
- }
- case .on3Ex(let id):
- On3QuizScreen(vm: vm, id: id)
- case .on4Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("on_year4"),
- subtitle: vm.tr("on4_sub"),
- rows: vm.content.on4.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "o4\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.on4Done(id),
- destination: .on4Lesson(id)
- )
- }
- )
- case .on4Lesson(let id):
- if let l = vm.content.on4Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .on4Ex(id))
- }
- case .on4Ex(let id):
- On4QuizScreen(vm: vm, id: id)
- case .czechMap:
- CzechMapScreen(vm: vm)
- case .litYears:
- LitYearsScreen(vm: vm)
- case .litMap:
- LessonListScreen(
- vm: vm,
- title: vm.tr("lit_year1"),
- subtitle: vm.tr("lit_sub"),
- rows: vm.content.lit.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "l\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.litDone(id),
- destination: .litLesson(id)
- )
- }
- )
- case .litLesson(let id):
- if let l = vm.content.litLesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .litEx(id))
- }
- case .litEx(let id):
- LitYearQuizScreen(vm: vm, year: 1, id: id)
- case .lit2Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("lit_year2"),
- subtitle: vm.tr("lit2_sub"),
- rows: vm.content.lit2.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "l2\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.lit2Done(id),
- destination: .lit2Lesson(id)
- )
- }
- )
- case .lit2Lesson(let id):
- if let l = vm.content.lit2Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .lit2Ex(id))
- }
- case .lit2Ex(let id):
- LitYearQuizScreen(vm: vm, year: 2, id: id)
- case .lit3Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("lit_year3"),
- subtitle: vm.tr("lit3_sub"),
- rows: vm.content.lit3.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "l3\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.lit3Done(id),
- destination: .lit3Lesson(id)
- )
- }
- )
- case .lit3Lesson(let id):
- if let l = vm.content.lit3Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .lit3Ex(id))
- }
- case .lit3Ex(let id):
- LitYearQuizScreen(vm: vm, year: 3, id: id)
- case .lit4Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("lit_year4"),
- subtitle: vm.tr("lit4_sub"),
- rows: vm.content.lit4.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "l4\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.lit4Done(id),
- destination: .lit4Lesson(id)
- )
- }
- )
- case .lit4Lesson(let id):
- if let l = vm.content.lit4Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .lit4Ex(id))
- }
- case .lit4Ex(let id):
- LitYearQuizScreen(vm: vm, year: 4, id: id)
- case .sciMap:
- SciMapScreen(vm: vm)
- case .chemMap:
- LessonListScreen(
- vm: vm,
- title: vm.tr("Chemie"),
- subtitle: vm.tr("chem_sub"),
- rows: vm.content.chem.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "c\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.chemDone(id),
- destination: .chemLesson(id)
- )
- }
- )
- case .chemLesson(let id):
- if let l = vm.content.chemLesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .chemEx(id))
- }
- case .chemEx(let id):
- SciQuizScreen(vm: vm, area: "chem", id: id)
- case .bioMap:
- LessonListScreen(
- vm: vm,
- title: vm.tr("Biologie"),
- subtitle: vm.tr("bio_sub"),
- rows: vm.content.bio.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "b\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.bioDone(id),
- destination: .bioLesson(id)
- )
- }
- )
- case .bioLesson(let id):
- if let l = vm.content.bioLesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .bioEx(id))
- }
- case .bioEx(let id):
- SciQuizScreen(vm: vm, area: "bio", id: id)
- case .matYears:
- MatYearsScreen(vm: vm)
- case .mat0Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("mat0_year"),
- subtitle: vm.tr("mat0_sub"),
- rows: vm.content.mat0.map { l in
- let id = l.int("id")
- return LessonRow(id: "m0\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat0Done(id), destination: .mat0Lesson(id))
- }
- )
- case .mat0Lesson(let id):
- if let l = vm.content.mat0Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat0Ex(id))
- }
- case .mat0Ex(let id):
- MathPracticeScreen(vm: vm, year: 0, id: id)
- case .matMap:
- LessonListScreen(
- vm: vm,
- title: vm.tr("mat_year1"),
- subtitle: vm.tr("mat_sub"),
- rows: vm.content.mat.map { l in
- let id = l.int("id")
- return LessonRow(id: "m1\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.matDone(id), destination: .matLesson(id))
- }
- )
- case .matLesson(let id):
- if let l = vm.content.matLesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .matEx(id))
- }
- case .matEx(let id):
- MathPracticeScreen(vm: vm, year: 1, id: id)
- case .mat2Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("mat_year2"),
- subtitle: vm.tr("mat2_sub"),
- rows: vm.content.mat2.map { l in
- let id = l.int("id")
- return LessonRow(id: "m2\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat2Done(id), destination: .mat2Lesson(id))
- }
- )
- case .mat2Lesson(let id):
- if let l = vm.content.mat2Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat2Ex(id))
- }
- case .mat2Ex(let id):
- MathPracticeScreen(vm: vm, year: 2, id: id)
- case .mat3Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("mat_year3"),
- subtitle: vm.tr("mat3_sub"),
- rows: vm.content.mat3.map { l in
- let id = l.int("id")
- return LessonRow(id: "m3\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat3Done(id), destination: .mat3Lesson(id))
- }
- )
- case .mat3Lesson(let id):
- if let l = vm.content.mat3Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat3Ex(id))
- }
- case .mat3Ex(let id):
- MathPracticeScreen(vm: vm, year: 3, id: id)
- case .mat4Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("mat_year4"),
- subtitle: vm.tr("mat4_sub"),
- rows: vm.content.mat4.map { l in
- let id = l.int("id")
- return LessonRow(id: "m4\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.mat4Done(id), destination: .mat4Lesson(id))
- }
- )
- case .mat4Lesson(let id):
- if let l = vm.content.mat4Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .mat4Ex(id))
- }
- case .mat4Ex(let id):
- MathPracticeScreen(vm: vm, year: 4, id: id)
- case .fyzYears:
- FyzYearsScreen(vm: vm)
- case .fyzMap:
- LessonListScreen(
- vm: vm,
- title: vm.tr("fyz_year1"),
- subtitle: vm.tr("fyz_sub"),
- rows: vm.content.fyz.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "f\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.fyzDone(id),
- destination: .fyzLesson(id)
- )
- }
- )
- case .fyzLesson(let id):
- if let l = vm.content.fyzLesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .fyzEx(id))
- }
- case .fyzEx(let id):
- FyzQuizScreen(vm: vm, year: 1, id: id)
- case .fyz2Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("fyz_year2"),
- subtitle: vm.tr("fyz2_sub"),
- rows: vm.content.fyz2.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "f2\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.fyz2Done(id),
- destination: .fyz2Lesson(id)
- )
- }
- )
- case .fyz2Lesson(let id):
- if let l = vm.content.fyz2Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .fyz2Ex(id))
- }
- case .fyz2Ex(let id):
- FyzQuizScreen(vm: vm, year: 2, id: id)
- case .fyz3Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("fyz_year3"),
- subtitle: vm.tr("fyz3_sub"),
- rows: vm.content.fyz3.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "f3\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.fyz3Done(id),
- destination: .fyz3Lesson(id)
- )
- }
- )
- case .fyz3Lesson(let id):
- if let l = vm.content.fyz3Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .fyz3Ex(id))
- }
- case .fyz3Ex(let id):
- FyzQuizScreen(vm: vm, year: 3, id: id)
- case .fyz4Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("fyz_year4"),
- subtitle: vm.tr("fyz4_sub"),
- rows: vm.content.fyz4.map { l in
- let id = l.int("id")
- return LessonRow(
- id: "f4\(id)",
- title: vm.tr(l.str("titleKey")),
- done: vm.progress.fyz4Done(id),
- destination: .fyz4Lesson(id)
- )
- }
- )
- case .fyz4Lesson(let id):
- if let l = vm.content.fyz4Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .fyz4Ex(id))
- }
- case .fyz4Ex(let id):
- FyzQuizScreen(vm: vm, year: 4, id: id)
- case .csYears:
- CsYearsScreen(vm: vm)
- case .csMap:
- LessonListScreen(
- vm: vm,
- title: vm.tr("cs_year1"),
- subtitle: vm.tr("cs_sub"),
- rows: vm.content.cs.map { l in
- let id = l.int("id")
- return LessonRow(id: "c\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.csDone(id), destination: .csLesson(id))
- }
- )
- case .csLesson(let id):
- if let l = vm.content.csLesson(id) {
- CodeLessonScreen(vm: vm, lesson: l, onSuccess: { vm.markCs(id) })
- }
- case .cs2Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("cs_year2"),
- subtitle: vm.tr("cs2_sub"),
- rows: vm.content.cs2.map { l in
- let id = l.int("id")
- return LessonRow(id: "c2\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.cs2Done(id), destination: .cs2Lesson(id))
- }
- )
- case .cs2Lesson(let id):
- if let l = vm.content.cs2Lesson(id) {
- CodeLessonScreen(vm: vm, lesson: l, onSuccess: { vm.markCs2(id) })
- }
- case .cs3Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("cs_year3"),
- subtitle: vm.tr("cs3_sub"),
- rows: vm.content.cs3.map { l in
- let id = l.int("id")
- return LessonRow(id: "c3\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.cs3Done(id), destination: .cs3Lesson(id))
- }
- )
- case .cs3Lesson(let id):
- if let l = vm.content.cs3Lesson(id) {
- CodeLessonScreen(vm: vm, lesson: l, onSuccess: { vm.markCs3(id) })
- }
- case .cs4Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("cs_year4"),
- subtitle: vm.tr("cs4_sub"),
- rows: vm.content.cs4.map { l in
- let id = l.int("id")
- return LessonRow(id: "c4\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.cs4Done(id), destination: .cs4Lesson(id))
- }
- )
- case .cs4Lesson(let id):
- if let l = vm.content.cs4Lesson(id) {
- CodeLessonScreen(vm: vm, lesson: l, onSuccess: { vm.markCs4(id) })
- }
- case .swYears:
- SwYearsScreen(vm: vm)
- case .swMap:
- LessonListScreen(
- vm: vm,
- title: vm.tr("sw_year1"),
- subtitle: vm.tr("sw_sub"),
- rows: vm.content.sw.map { l in
- let id = l.int("id")
- return LessonRow(id: "s\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.swDone(id), destination: .swLesson(id))
- }
- )
- case .swLesson(let id):
- if let l = vm.content.swLesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .swEx(id))
- }
- case .swEx(let id):
- SwQuizScreen(vm: vm, year: 1, id: id)
- case .sw2Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("sw_year2"),
- subtitle: vm.tr("sw2_sub"),
- rows: vm.content.sw2.map { l in
- let id = l.int("id")
- return LessonRow(id: "s2\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.sw2Done(id), destination: .sw2Lesson(id))
- }
- )
- case .sw2Lesson(let id):
- if let l = vm.content.sw2Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .sw2Ex(id))
- }
- case .sw2Ex(let id):
- SwQuizScreen(vm: vm, year: 2, id: id)
- case .sw3Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("sw_year3"),
- subtitle: vm.tr("sw3_sub"),
- rows: vm.content.sw3.map { l in
- let id = l.int("id")
- return LessonRow(id: "s3\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.sw3Done(id), destination: .sw3Lesson(id))
- }
- )
- case .sw3Lesson(let id):
- if let l = vm.content.sw3Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .sw3Ex(id))
- }
- case .sw3Ex(let id):
- SwQuizScreen(vm: vm, year: 3, id: id)
- case .sw4Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("sw_year4"),
- subtitle: vm.tr("sw4_sub"),
- rows: vm.content.sw4.map { l in
- let id = l.int("id")
- return LessonRow(id: "s4\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.sw4Done(id), destination: .sw4Lesson(id))
- }
- )
- case .sw4Lesson(let id):
- if let l = vm.content.sw4Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .sw4Ex(id))
- }
- case .sw4Ex(let id):
- SwQuizScreen(vm: vm, year: 4, id: id)
- case .pgYears:
- PgYearsScreen(vm: vm)
- case .pgMap:
- LessonListScreen(
- vm: vm,
- title: vm.tr("pg_year1"),
- subtitle: vm.tr("pg_sub"),
- rows: vm.content.pg.map { l in
- let id = l.int("id")
- return LessonRow(id: "p\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.pgDone(id), destination: .pgLesson(id))
- }
- )
- case .pgLesson(let id):
- if let l = vm.content.pgLesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .pgEx(id))
- }
- case .pgEx(let id):
- PgQuizScreen(vm: vm, year: 1, id: id)
- case .pg2Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("pg_year2"),
- subtitle: vm.tr("pg2_sub"),
- rows: vm.content.pg2.map { l in
- let id = l.int("id")
- return LessonRow(id: "p2\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.pg2Done(id), destination: .pg2Lesson(id))
- }
- )
- case .pg2Lesson(let id):
- if let l = vm.content.pg2Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .pg2Ex(id))
- }
- case .pg2Ex(let id):
- PgQuizScreen(vm: vm, year: 2, id: id)
- case .pg3Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("pg_year3"),
- subtitle: vm.tr("pg3_sub"),
- rows: vm.content.pg3.map { l in
- let id = l.int("id")
- return LessonRow(id: "p3\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.pg3Done(id), destination: .pg3Lesson(id))
- }
- )
- case .pg3Lesson(let id):
- if let l = vm.content.pg3Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .pg3Ex(id))
- }
- case .pg3Ex(let id):
- PgQuizScreen(vm: vm, year: 3, id: id)
- case .pg4Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("pg_year4"),
- subtitle: vm.tr("pg4_sub"),
- rows: vm.content.pg4.map { l in
- let id = l.int("id")
- return LessonRow(id: "p4\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.pg4Done(id), destination: .pg4Lesson(id))
- }
- )
- case .pg4Lesson(let id):
- if let l = vm.content.pg4Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .pg4Ex(id))
- }
- case .pg4Ex(let id):
- PgQuizScreen(vm: vm, year: 4, id: id)
- case .tgYears:
- TgYearsScreen(vm: vm)
- case .tgMap:
- LessonListScreen(
- vm: vm,
- title: vm.tr("tg_year1"),
- subtitle: vm.tr("tg_sub"),
- rows: vm.content.tg.map { l in
- let id = l.int("id")
- return LessonRow(id: "t\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.tgDone(id), destination: .tgLesson(id))
- }
- )
- case .tgLesson(let id):
- if let l = vm.content.tgLesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .tgEx(id))
- }
- case .tgEx(let id):
- TgQuizScreen(vm: vm, year: 1, id: id)
- case .tg2Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("tg_year2"),
- subtitle: vm.tr("tg2_sub"),
- rows: vm.content.tg2.map { l in
- let id = l.int("id")
- return LessonRow(id: "t2\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.tg2Done(id), destination: .tg2Lesson(id))
- }
- )
- case .tg2Lesson(let id):
- if let l = vm.content.tg2Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .tg2Ex(id))
- }
- case .tg2Ex(let id):
- TgQuizScreen(vm: vm, year: 2, id: id)
- case .tg3Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("tg_year3"),
- subtitle: vm.tr("tg3_sub"),
- rows: vm.content.tg3.map { l in
- let id = l.int("id")
- return LessonRow(id: "t3\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.tg3Done(id), destination: .tg3Lesson(id))
- }
- )
- case .tg3Lesson(let id):
- if let l = vm.content.tg3Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .tg3Ex(id))
- }
- case .tg3Ex(let id):
- TgQuizScreen(vm: vm, year: 3, id: id)
- case .tg4Map:
- LessonListScreen(
- vm: vm,
- title: vm.tr("tg_year4"),
- subtitle: vm.tr("tg4_sub"),
- rows: vm.content.tg4.map { l in
- let id = l.int("id")
- return LessonRow(id: "t4\(id)", title: vm.tr(l.str("titleKey")), done: vm.progress.tg4Done(id), destination: .tg4Lesson(id))
- }
- )
- case .tg4Lesson(let id):
- if let l = vm.content.tg4Lesson(id) {
- SlidesScreen(vm: vm, title: vm.tr(l.str("titleKey")), subtitle: vm.tr(l.str("subKey")), slides: l.arr("slides"), next: .tg4Ex(id))
- }
- case .tg4Ex(let id):
- TgQuizScreen(vm: vm, year: 4, id: id)
- case .mluvnice:
- LessonListScreen(
- vm: vm,
- title: vm.tr("Mluvnice"),
- subtitle: vm.tr("mluv_sub"),
- rows: vm.content.mluvnice.map { m in
- let n = m.int("id")
- return LessonRow(
- id: "m\(n)",
- title: m.str("name"),
- done: vm.progress.mluvDone(n),
- destination: .mluvEx(n)
- )
- }
- )
- case .mluvEx(let n):
- MluvExercise(vm: vm, n: n)
- case .readingList:
- BookListScreen(vm: vm)
- case .book(let id):
- BookScreen(vm: vm, id: id)
- case .bookQuiz(let id):
- LitQuizScreen(vm: vm, id: id)
- case .bookPlot(let id):
- PlotScreen(vm: vm, id: id)
- }
- }
-}
-
-struct CodeLessonScreen: View {
- @ObservedObject var vm: AppModel
- let lesson: J
- let onSuccess: () -> Void
- @State private var code = ""
- @State private var stdin = ""
- @State private var output = ""
- @State private var status = ""
-
- private var running: Bool { status == "running" }
- private var needsInput: Bool { code.contains("ReadLine") }
-
- var body: some View {
- GeometryReader { geo in
- if geo.size.width >= 900 {
- HStack(alignment: .top, spacing: 28) {
- ScrollView {
- explanation
- .padding(.vertical, 4)
- }
- .frame(width: min(560, geo.size.width * 0.4))
- workspace(height: geo.size.height)
- }
- .padding(.horizontal, 28)
- .padding(.vertical, 20)
- .frame(maxWidth: 1640)
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- } else {
- ScrollView {
- VStack(alignment: .leading, spacing: 20) {
- explanation
- editorCard.frame(height: 320)
- if needsInput { inputField }
- outputCard.frame(height: 220)
- }
- .padding(16)
- }
- }
- }
- .detailScreen(vm.tr(lesson.str("titleKey")), subtitle: vm.tr(lesson.str("subKey")))
- .onAppear {
- if code.isEmpty { code = lesson.str("code") }
- }
- }
-
- private var explanation: some View {
- VStack(alignment: .leading, spacing: 34) {
- ForEach(Array(lesson.arr("slides").enumerated()), id: \.offset) { _, slide in
- SlideBody(slide: slide)
- }
- }
- }
-
- private func workspace(height: CGFloat) -> some View {
- VStack(spacing: 16) {
- editorCard
- .frame(maxHeight: .infinity)
- if needsInput { inputField }
- outputCard
- .frame(height: max(180, height * 0.32))
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- }
-
- private var editorCard: some View {
- VStack(spacing: 0) {
- HStack(spacing: 10) {
- Image(systemName: "chevron.left.forwardslash.chevron.right")
- .foregroundStyle(Color.accentColor)
- Text("Program.cs")
- .font(.headline.monospaced())
- Spacer(minLength: 8)
- Button {
- code = lesson.str("code")
- output = ""
- status = ""
- } label: {
- Label(vm.tr("cs_reset"), systemImage: "arrow.counterclockwise")
- }
- .buttonStyle(.bordered)
- .disabled(running)
- Button { run() } label: {
- Label(vm.tr("cs_run"), systemImage: "play.fill")
- .padding(.horizontal, 4)
- }
- .buttonStyle(.borderedProminent)
- .keyboardShortcut(.return, modifiers: .command)
- .help("⌘↩")
- .disabled(running)
- }
- #if os(macOS)
- .controlSize(.large)
- #endif
- .padding(.horizontal, 14)
- .padding(.vertical, 10)
- Divider().opacity(0.5)
- editorField
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- }
- .background(codeFill, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
- .overlay {
- RoundedRectangle(cornerRadius: 16, style: .continuous)
- .strokeBorder(.separator.opacity(0.6), lineWidth: 0.5)
- }
- }
-
- @ViewBuilder
- private var editorField: some View {
- #if os(macOS)
- CodeEditor(text: $code)
- #else
- TextEditor(text: $code)
- .font(.system(size: 15, design: .monospaced))
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
- .scrollContentBackground(.hidden)
- .padding(8)
- #endif
- }
-
- private var inputField: some View {
- VStack(alignment: .leading, spacing: 6) {
- Label(vm.tr("cs_stdin"), systemImage: "keyboard")
- .font(.subheadline)
- .foregroundStyle(.secondary)
- TextField("", text: $stdin, axis: .vertical)
- .font(.system(size: 15, design: .monospaced))
- .lineLimit(1...4)
- .textFieldStyle(.roundedBorder)
- }
- }
-
- private var outputCard: some View {
- VStack(spacing: 0) {
- HStack(spacing: 10) {
- Image(systemName: "terminal")
- .foregroundStyle(.secondary)
- Text(vm.tr("cs_output"))
- .font(.headline)
- Spacer(minLength: 8)
- statusBadge
- }
- .padding(.horizontal, 14)
- .frame(height: 44)
- Divider().opacity(0.5)
- ScrollView {
- Text(output.isEmpty ? vm.tr("cs_empty") : output)
- .font(.system(size: 15, design: .monospaced))
- .foregroundStyle(output.isEmpty || running ? .secondary : .primary)
- .frame(maxWidth: .infinity, alignment: .topLeading)
- .textSelection(.enabled)
- .padding(14)
- }
- }
- .background(codeFill, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
- .overlay {
- RoundedRectangle(cornerRadius: 16, style: .continuous)
- .strokeBorder(.separator.opacity(0.6), lineWidth: 0.5)
- }
- }
-
- @ViewBuilder
- private var statusBadge: some View {
- switch status {
- case "running":
- ProgressView().controlSize(.small)
- case "ok":
- Label(vm.tr("cs_done"), systemImage: "checkmark.circle.fill")
- .font(.subheadline.weight(.semibold))
- .foregroundStyle(vm.palette.success)
- case "fail", "timeout":
- Label(vm.tr("cs_failed"), systemImage: "xmark.octagon.fill")
- .font(.subheadline.weight(.semibold))
- .foregroundStyle(vm.palette.error)
- default:
- EmptyView()
- }
- }
-
- private var codeFill: AnyShapeStyle {
- #if os(macOS)
- AnyShapeStyle(.background.secondary)
- #else
- AnyShapeStyle(Color(.secondarySystemBackground))
- #endif
- }
-
- private func run() {
- guard !running else { return }
- status = "running"
- output = vm.tr("cs_running")
- CSharpRunner.run(code, stdin: needsInput ? stdin : "") { result, text in
- status = result
- switch result {
- case "unavailable":
- output = vm.tr("cs_unavailable")
- case "timeout":
- output = (text + "\n\n" + vm.tr("cs_timeout")).trimmingCharacters(in: .whitespacesAndNewlines)
- default:
- output = text.isEmpty ? vm.tr("cs_empty") : text
- }
- if result == "ok" { onSuccess() }
- }
- }
-}
-
-#if os(macOS)
-/// Plain-text editor for C#: monospaced, coloured, and without smart quotes or
-/// autocorrect, which would turn "text" into “text” and break the program.
-struct CodeEditor: NSViewRepresentable {
- @Binding var text: String
-
- func makeCoordinator() -> Coordinator { Coordinator(text: $text) }
-
- func makeNSView(context: Context) -> NSScrollView {
- let scroll = NSTextView.scrollableTextView()
- scroll.drawsBackground = false
- scroll.autohidesScrollers = true
- guard let view = scroll.documentView as? NSTextView else { return scroll }
- view.delegate = context.coordinator
- view.isRichText = false
- view.allowsUndo = true
- view.isAutomaticQuoteSubstitutionEnabled = false
- view.isAutomaticDashSubstitutionEnabled = false
- view.isAutomaticTextReplacementEnabled = false
- view.isAutomaticSpellingCorrectionEnabled = false
- view.isContinuousSpellCheckingEnabled = false
- view.isGrammarCheckingEnabled = false
- view.isAutomaticLinkDetectionEnabled = false
- view.smartInsertDeleteEnabled = false
- view.drawsBackground = false
- view.font = CSharpStyle.font
- view.textContainerInset = NSSize(width: 10, height: 12)
- view.typingAttributes = CSharpStyle.base
- view.string = text
- CSharpStyle.highlight(view.textStorage)
- return scroll
- }
-
- func updateNSView(_ scroll: NSScrollView, context: Context) {
- context.coordinator.text = $text
- guard let view = scroll.documentView as? NSTextView, view.string != text else { return }
- view.string = text
- CSharpStyle.highlight(view.textStorage)
- }
-
- final class Coordinator: NSObject, NSTextViewDelegate {
- var text: Binding
-
- init(text: Binding) {
- self.text = text
- }
-
- func textDidChange(_ notification: Notification) {
- guard let view = notification.object as? NSTextView else { return }
- text.wrappedValue = view.string
- CSharpStyle.highlight(view.textStorage)
- view.typingAttributes = CSharpStyle.base
- }
- }
-}
-
-enum CSharpStyle {
- static let font = NSFont.monospacedSystemFont(ofSize: 15, weight: .regular)
- static var base: [NSAttributedString.Key: Any] { [.font: font, .foregroundColor: NSColor.labelColor] }
-
- private static func adaptive(_ light: Int, _ dark: Int) -> NSColor {
- func color(_ hex: Int) -> NSColor {
- NSColor(
- srgbRed: CGFloat((hex >> 16) & 0xFF) / 255,
- green: CGFloat((hex >> 8) & 0xFF) / 255,
- blue: CGFloat(hex & 0xFF) / 255,
- alpha: 1
- )
- }
- return NSColor(name: nil) { appearance in
- appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua ? color(dark) : color(light)
- }
- }
-
- private static let keywords = [
- "abstract", "as", "async", "await", "base", "bool", "break", "case", "catch", "char", "class",
- "const", "continue", "decimal", "default", "do", "double", "else", "enum", "false", "finally",
- "float", "for", "foreach", "get", "if", "in", "int", "interface", "internal", "is", "long",
- "namespace", "new", "null", "object", "out", "override", "private", "protected", "public",
- "readonly", "record", "ref", "return", "sealed", "set", "static", "string", "struct", "switch",
- "this", "throw", "true", "try", "using", "var", "virtual", "void", "while",
- ]
-
- private static let rules: [(NSRegularExpression, NSColor)] = {
- func re(_ pattern: String) -> NSRegularExpression {
- try! NSRegularExpression(pattern: pattern)
- }
- return [
- (re("\\b(?:" + keywords.joined(separator: "|") + ")\\b"), adaptive(0x9B2393, 0xFF7AB2)),
- (re("\\b(?:Console|Math|List|Dictionary|File|Task|Exception|Func|Action|String|DateTime|Random)\\b"), adaptive(0x0B4F79, 0x5DD8FF)),
- (re("\\b\\d+(?:\\.\\d+)?\\b"), adaptive(0x1C00CF, 0xD9C97C)),
- (re("\\$?@?\"(?:\\\\.|[^\"\\\\\\n])*\"|'(?:\\\\.|[^'\\\\\\n])'"), adaptive(0xC41A16, 0xFC6A5D)),
- (re("//[^\\n]*|/\\*[\\s\\S]*?\\*/"), adaptive(0x5D6C79, 0x7F8C98)),
- ]
- }()
-
- static func highlight(_ storage: NSTextStorage?) {
- guard let storage else { return }
- let text = storage.string
- let full = NSRange(location: 0, length: (text as NSString).length)
- storage.beginEditing()
- storage.setAttributes(base, range: full)
- for (pattern, color) in rules {
- pattern.enumerateMatches(in: text, range: full) { match, _, _ in
- if let range = match?.range {
- storage.addAttribute(.foregroundColor, value: color, range: range)
- }
- }
- }
- storage.endEditing()
- }
-}
-#endif
-
-enum CSharpRunner {
- static func run(_ source: String, stdin: String, done: @escaping (String, String) -> Void) {
- #if os(macOS)
- DispatchQueue.global(qos: .userInitiated).async {
- let result = runMac(source, stdin: stdin)
- DispatchQueue.main.async { done(result.0, result.1) }
- }
- #else
- done("unavailable", "")
- #endif
- }
-
- #if os(macOS)
- private static func runMac(_ source: String, stdin: String) -> (String, String) {
- guard source.count <= 100_000 else { return ("fail", "Program je příliš dlouhý.") }
- guard let dotnet = findDotnet() else { return ("unavailable", "") }
- let dir = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent(".graduately/csharp-lab")
- let csproj = dir.appendingPathComponent("Lab.csproj")
- if !FileManager.default.fileExists(atPath: csproj.path) {
- try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
- let framework = targetFramework(dotnet)
- let xml = """
-
-
- Exe
- \(framework)
- enable
- disable
- latest
-
-
- """
- try? xml.write(to: csproj, atomically: true, encoding: .utf8)
- let restored = command(dotnet, ["restore", dir.path], dir, "", 90)
- if restored.0 != "ok" { return restored }
- }
- do {
- try source.write(to: dir.appendingPathComponent("Program.cs"), atomically: true, encoding: .utf8)
- } catch {
- return ("fail", error.localizedDescription)
- }
- var args = ["run", "--project", dir.path, "--verbosity", "quiet"]
- if FileManager.default.fileExists(atPath: dir.appendingPathComponent("obj").path) {
- args.append("--no-restore")
- }
- return command(dotnet, args, dir, stdin, 20)
- }
-
- private static func command(_ dotnet: String, _ args: [String], _ dir: URL, _ stdin: String, _ seconds: Double) -> (String, String) {
- let proc = Process()
- proc.executableURL = URL(fileURLWithPath: dotnet)
- proc.arguments = args
- proc.currentDirectoryURL = dir
- var env = ProcessInfo.processInfo.environment
- let root = (dotnet as NSString).deletingLastPathComponent
- env["PATH"] = [root, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].joined(separator: ":")
- env["DOTNET_ROOT"] = root
- env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1"
- env["DOTNET_NOLOGO"] = "1"
- env["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1"
- proc.environment = env
- let out = Pipe()
- let input = Pipe()
- proc.standardOutput = out
- proc.standardError = out
- proc.standardInput = input
- var collected = Data()
- let lock = NSLock()
- out.fileHandleForReading.readabilityHandler = { handle in
- let chunk = handle.availableData
- guard !chunk.isEmpty else { return }
- lock.lock()
- collected.append(chunk)
- lock.unlock()
- }
- do { try proc.run() } catch { return ("fail", error.localizedDescription) }
- if let data = stdin.data(using: .utf8) { input.fileHandleForWriting.write(data) }
- try? input.fileHandleForWriting.close()
- let group = DispatchGroup()
- group.enter()
- proc.terminationHandler = { _ in group.leave() }
- if group.wait(timeout: .now() + seconds) == .timedOut {
- proc.terminate()
- kill(proc.processIdentifier, SIGKILL)
- out.fileHandleForReading.readabilityHandler = nil
- return ("timeout", clip(collected, lock))
- }
- out.fileHandleForReading.readabilityHandler = nil
- let rest = out.fileHandleForReading.readDataToEndOfFile()
- lock.lock()
- collected.append(rest)
- lock.unlock()
- let text = clip(collected, lock)
- return proc.terminationStatus == 0 ? ("ok", text) : ("fail", text)
- }
-
- private static func clip(_ data: Data, _ lock: NSLock) -> String {
- lock.lock()
- let copy = data
- lock.unlock()
- let text = String(data: copy, encoding: .utf8) ?? ""
- let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
- return trimmed.count <= 16_000 ? trimmed : String(trimmed.prefix(16_000)) + "\n…"
- }
-
- private static func targetFramework(_ dotnet: String) -> String {
- let version = command(dotnet, ["--version"], URL(fileURLWithPath: NSTemporaryDirectory()), "", 20)
- let major = version.1.split(separator: ".").first.map(String.init) ?? ""
- return major.allSatisfy(\.isNumber) && !major.isEmpty ? "net\(major).0" : "net8.0"
- }
-
- private static func findDotnet() -> String? {
- let names = [
- "/usr/local/share/dotnet/dotnet",
- "/opt/homebrew/share/dotnet/dotnet",
- "/usr/local/bin/dotnet",
- NSHomeDirectory() + "/.dotnet/dotnet",
- ]
- return names.first { FileManager.default.isExecutableFile(atPath: $0) }
- }
- #endif
-}
diff --git a/ios/Graduately/Update/Updater.swift b/ios/Graduately/Update/Updater.swift
deleted file mode 100644
index 29f8a8a..0000000
--- a/ios/Graduately/Update/Updater.swift
+++ /dev/null
@@ -1,120 +0,0 @@
-import Foundation
-import UIKit
-
-struct UpdateState {
- var status = "idle"
- var remote: String?
- var messageKey = "update_current"
- var messageArg: String? = String(AppConfig.commit.prefix(7))
- var canInstall = false
-}
-
-final class Updater {
- private let repo = AppConfig.updateRepo
- private let branch = AppConfig.updateBranch
-
- func check(interactive: Bool, cb: @escaping (UpdateState) -> Void) {
- if AppConfig.commit == "dev" {
- emit(cb, UpdateState(status: "dev", messageKey: "update_err_devbuild", messageArg: nil))
- return
- }
- emit(cb, UpdateState(status: "checking", messageKey: "update_checking", messageArg: nil))
- DispatchQueue.global(qos: .utility).async {
- do {
- let atom = try self.get("https://github.com/\(self.repo)/commits/\(self.branch).atom")
- let remote: String
- // VERSION-ios names the commit whose IPA is really in the
- // branch; it is missing only when the iOS job failed or on
- // older branches, where the branch-wide commit is used.
- if let tip = self.atomTip(atom),
- let marker = try? self.get("https://raw.githubusercontent.com/\(self.repo)/\(tip)/VERSION-ios").trimmingCharacters(in: .whitespacesAndNewlines),
- marker.count >= 7 {
- remote = marker
- } else if let sha = self.atomMainSha(atom) {
- remote = sha
- } else if let tip = self.atomTip(atom) {
- remote = try self.get("https://raw.githubusercontent.com/\(self.repo)/\(tip)/VERSION").trimmingCharacters(in: .whitespacesAndNewlines)
- } else {
- throw URLError(.cannotParseResponse)
- }
- let local = AppConfig.commit
- if self.sameCommit(remote, local) {
- self.emit(cb, UpdateState(status: "ok", remote: remote, messageKey: "update_uptodate", messageArg: String(remote.prefix(7))))
- } else {
- self.emit(cb, UpdateState(status: "available", remote: remote, messageKey: "update_available", messageArg: String(remote.prefix(7)), canInstall: true))
- }
- } catch {
- self.emit(cb, UpdateState(status: "error", messageKey: "update_err_network", messageArg: nil))
- }
- }
- }
-
- func install(cb: @escaping (UpdateState) -> Void) {
- emit(cb, UpdateState(status: "downloading", messageKey: "update_downloading", messageArg: nil))
- DispatchQueue.global(qos: .utility).async {
- do {
- let atom = try self.get("https://github.com/\(self.repo)/commits/\(self.branch).atom")
- let tip = self.atomTip(atom) ?? ""
- let page = "https://github.com/\(self.repo)/raw/main/installers/graduately-ios.html"
- DispatchQueue.main.async {
- if let url = URL(string: page) {
- UIApplication.shared.open(url)
- }
- }
- self.emit(cb, UpdateState(status: "staged", remote: tip, messageKey: "update_ios_sideload", messageArg: String(tip.prefix(7))))
- } catch {
- self.emit(cb, UpdateState(status: "error", messageKey: "update_err_download", messageArg: nil))
- }
- }
- }
-
- private func emit(_ cb: @escaping (UpdateState) -> Void, _ state: UpdateState) {
- DispatchQueue.main.async { cb(state) }
- }
-
- private func atomTip(_ atom: String) -> String? {
- if let m = atom.range(of: #"Commit/([0-9a-f]{40})"#, options: [.regularExpression, .caseInsensitive]) {
- let s = String(atom[m])
- return s.split(separator: "/").last.map(String.init)
- }
- if let m = atom.range(of: #"/commit/([0-9a-f]{7,40})"#, options: [.regularExpression, .caseInsensitive]) {
- return String(atom[m]).split(separator: "/").last.map(String.init)
- }
- return nil
- }
-
- private func atomMainSha(_ atom: String) -> String? {
- guard let regex = try? NSRegularExpression(pattern: #"Build from ([0-9a-f]{7,40})"#, options: .caseInsensitive),
- let match = regex.firstMatch(in: atom, range: NSRange(atom.startIndex..., in: atom)),
- let range = Range(match.range(at: 1), in: atom)
- else { return nil }
- return String(atom[range])
- }
-
- private func sameCommit(_ a: String, _ b: String) -> Bool {
- let n = min(a.count, b.count)
- return n >= 7 && a.prefix(n).lowercased() == b.prefix(n).lowercased()
- }
-
- private func get(_ url: String) throws -> String {
- guard let u = URL(string: url) else { throw URLError(.badURL) }
- var req = URLRequest(url: u, timeoutInterval: 15)
- req.setValue("Graduately-ios", forHTTPHeaderField: "User-Agent")
- req.setValue("*/*", forHTTPHeaderField: "Accept")
- let sem = DispatchSemaphore(value: 0)
- var result: Result = .failure(URLError(.unknown))
- URLSession.shared.dataTask(with: req) { data, resp, err in
- if let err { result = .failure(err) }
- else if let http = resp as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
- result = .failure(URLError(.badServerResponse))
- } else if let data, let text = String(data: data, encoding: .utf8) {
- result = .success(text)
- } else {
- result = .failure(URLError(.cannotDecodeContentData))
- }
- sem.signal()
- }.resume()
- _ = sem.wait(timeout: .now() + 20)
- return try result.get()
- }
-}
diff --git a/macos/Graduately.xcodeproj/project.pbxproj b/macos/Graduately.xcodeproj/project.pbxproj
deleted file mode 100644
index 42b58cc..0000000
--- a/macos/Graduately.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,450 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 56;
- objects = {
-
-/* Begin PBXBuildFile section */
- B101000000000000000001 /* GraduatelyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000001 /* GraduatelyApp.swift */; };
- B101000000000000000002 /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000002 /* Platform.swift */; };
- B101000000000000000003 /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000003 /* Updater.swift */; };
- B101000000000000000004 /* AppModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000004 /* AppModel.swift */; };
- B101000000000000000005 /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000005 /* Route.swift */; };
- B101000000000000000006 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000006 /* JSON.swift */; };
- B101000000000000000007 /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000007 /* Content.swift */; };
- B101000000000000000008 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000008 /* Theme.swift */; };
- B101000000000000000009 /* Answers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000009 /* Answers.swift */; };
- B101000000000000000010 /* ProgressStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000010 /* ProgressStore.swift */; };
- B101000000000000000011 /* Components.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000011 /* Components.swift */; };
- B101000000000000000012 /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000012 /* Icons.swift */; };
- B101000000000000000013 /* Screens.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000013 /* Screens.swift */; };
- B101000000000000000014 /* Exercises.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000014 /* Exercises.swift */; };
- B101000000000000000015 /* ExercisesMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000015 /* ExercisesMore.swift */; };
- B101000000000000000016 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B201000000000000000016 /* Assets.xcassets */; };
- B101000000000000000018 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B201000000000000000020 /* Localizable.strings */; };
- B101000000000000000017 /* PathMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = B201000000000000000019 /* PathMap.swift */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXFileReference section */
- B201000000000000000001 /* GraduatelyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraduatelyApp.swift; sourceTree = ""; };
- B201000000000000000002 /* Platform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = ""; };
- B201000000000000000003 /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = ""; };
- B201000000000000000004 /* AppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppModel.swift; path = ../ios/Graduately/AppModel.swift; sourceTree = SOURCE_ROOT; };
- B201000000000000000005 /* Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Route.swift; path = ../ios/Graduately/Route.swift; sourceTree = SOURCE_ROOT; };
- B201000000000000000006 /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = JSON.swift; path = ../ios/Graduately/Data/JSON.swift; sourceTree = SOURCE_ROOT; };
- B201000000000000000007 /* Content.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Content.swift; path = ../ios/Graduately/Data/Content.swift; sourceTree = SOURCE_ROOT; };
- B201000000000000000008 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Theme.swift; path = ../ios/Graduately/Data/Theme.swift; sourceTree = SOURCE_ROOT; };
- B201000000000000000009 /* Answers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Answers.swift; path = ../ios/Graduately/Data/Answers.swift; sourceTree = SOURCE_ROOT; };
- B201000000000000000010 /* ProgressStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ProgressStore.swift; path = ../ios/Graduately/Data/ProgressStore.swift; sourceTree = SOURCE_ROOT; };
- B201000000000000000011 /* Components.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Components.swift; path = ../ios/Graduately/UI/Components.swift; sourceTree = SOURCE_ROOT; };
- B201000000000000000012 /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Icons.swift; path = ../ios/Graduately/UI/Icons.swift; sourceTree = SOURCE_ROOT; };
- B201000000000000000013 /* Screens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Screens.swift; path = ../ios/Graduately/UI/Screens.swift; sourceTree = SOURCE_ROOT; };
- B201000000000000000014 /* Exercises.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Exercises.swift; path = ../ios/Graduately/UI/Exercises.swift; sourceTree = SOURCE_ROOT; };
- B201000000000000000015 /* ExercisesMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ExercisesMore.swift; path = ../ios/Graduately/UI/ExercisesMore.swift; sourceTree = SOURCE_ROOT; };
- B201000000000000000016 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- B201000000000000000017 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- B201000000000000000018 /* Graduately.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Graduately.entitlements; sourceTree = ""; };
- B201000000000000000019 /* PathMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PathMap.swift; path = ../ios/Graduately/UI/PathMap.swift; sourceTree = SOURCE_ROOT; };
- B201000000000000000021 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
- B201000000000000000022 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; };
- B301000000000000000001 /* Graduately.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Graduately.app; sourceTree = BUILT_PRODUCTS_DIR; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
- B401000000000000000001 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
- B501000000000000000001 = {
- isa = PBXGroup;
- children = (
- B501000000000000000002 /* Graduately */,
- B501000000000000000006 /* Products */,
- );
- sourceTree = "";
- };
- B501000000000000000002 /* Graduately */ = {
- isa = PBXGroup;
- children = (
- B201000000000000000001 /* GraduatelyApp.swift */,
- B201000000000000000002 /* Platform.swift */,
- B201000000000000000004 /* AppModel.swift */,
- B201000000000000000005 /* Route.swift */,
- B501000000000000000003 /* Data */,
- B501000000000000000004 /* Update */,
- B501000000000000000005 /* UI */,
- B201000000000000000016 /* Assets.xcassets */,
- B201000000000000000020 /* Localizable.strings */,
- B201000000000000000017 /* Info.plist */,
- B201000000000000000018 /* Graduately.entitlements */,
- );
- path = Graduately;
- sourceTree = "";
- };
- B501000000000000000003 /* Data */ = {
- isa = PBXGroup;
- children = (
- B201000000000000000006 /* JSON.swift */,
- B201000000000000000007 /* Content.swift */,
- B201000000000000000008 /* Theme.swift */,
- B201000000000000000009 /* Answers.swift */,
- B201000000000000000010 /* ProgressStore.swift */,
- );
- name = Data;
- sourceTree = "";
- };
- B501000000000000000004 /* Update */ = {
- isa = PBXGroup;
- children = (
- B201000000000000000003 /* Updater.swift */,
- );
- path = Update;
- sourceTree = "";
- };
- B501000000000000000005 /* UI */ = {
- isa = PBXGroup;
- children = (
- B201000000000000000011 /* Components.swift */,
- B201000000000000000012 /* Icons.swift */,
- B201000000000000000019 /* PathMap.swift */,
- B201000000000000000013 /* Screens.swift */,
- B201000000000000000014 /* Exercises.swift */,
- B201000000000000000015 /* ExercisesMore.swift */,
- );
- name = UI;
- sourceTree = "";
- };
- B501000000000000000006 /* Products */ = {
- isa = PBXGroup;
- children = (
- B301000000000000000001 /* Graduately.app */,
- );
- name = Products;
- sourceTree = "";
- };
-/* End PBXGroup section */
-
-/* Begin PBXVariantGroup section */
- B201000000000000000020 /* Localizable.strings */ = {
- isa = PBXVariantGroup;
- children = (
- B201000000000000000021 /* en */,
- B201000000000000000022 /* cs */,
- );
- name = Localizable.strings;
- sourceTree = "";
- };
-/* End PBXVariantGroup section */
-
-/* Begin PBXNativeTarget section */
- B601000000000000000001 /* Graduately */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = B801000000000000000002 /* Build configuration list for PBXNativeTarget "Graduately" */;
- buildPhases = (
- B701000000000000000001 /* Sources */,
- B401000000000000000001 /* Frameworks */,
- B701000000000000000002 /* Resources */,
- B701000000000000000004 /* Build content */,
- );
- buildRules = (
- );
- dependencies = (
- );
- name = Graduately;
- productName = Graduately;
- productReference = B301000000000000000001 /* Graduately.app */;
- productType = "com.apple.product-type.application";
- };
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
- B001000000000000000001 /* Project object */ = {
- isa = PBXProject;
- attributes = {
- BuildIndependentTargetsInParallel = 1;
- LastSwiftUpdateCheck = 1600;
- LastUpgradeCheck = 2700;
- TargetAttributes = {
- B601000000000000000001 = {
- CreatedOnToolsVersion = 16.0;
- };
- };
- };
- buildConfigurationList = B801000000000000000001 /* Build configuration list for PBXProject "Graduately" */;
- compatibilityVersion = "Xcode 14.0";
- developmentRegion = en;
- hasScannedForEncodings = 0;
- knownRegions = (
- en,
- cs,
- Base,
- );
- mainGroup = B501000000000000000001;
- productRefGroup = B501000000000000000006 /* Products */;
- projectDirPath = "";
- projectRoot = "";
- targets = (
- B601000000000000000001 /* Graduately */,
- );
- };
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
- B701000000000000000002 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- B101000000000000000016 /* Assets.xcassets in Resources */,
- B101000000000000000018 /* Localizable.strings in Resources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXShellScriptBuildPhase section */
- B701000000000000000004 /* Build content */ = {
- isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- );
- inputPaths = (
- );
- name = "Build content";
- outputFileListPaths = (
- );
- outputPaths = (
- "$(DERIVED_FILE_DIR)/content.json",
- "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/content.json",
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "# Build content.json from the Markdown in content/ and drop it into the app\n# bundle. Nothing generated is kept in the repository, so a fresh clone opens\n# and runs in Xcode without extra steps.\nset -eu\nextract=\"${SRCROOT}/../scripts/build-content.py\"\nif [ ! -f \"$extract\" ]; then\n echo \"error: ${extract} not found. Keep macos/ inside the Graduately checkout.\" >&2\n exit 1\nfi\nout=\"${DERIVED_FILE_DIR}/content.json\"\nmkdir -p \"${DERIVED_FILE_DIR}\"\npython3 \"$extract\" --out \"$out\"\nmkdir -p \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}\"\ncp \"$out\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/content.json\"\n";
- };
-/* End PBXShellScriptBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- B701000000000000000001 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- B101000000000000000001 /* GraduatelyApp.swift in Sources */,
- B101000000000000000002 /* Platform.swift in Sources */,
- B101000000000000000003 /* Updater.swift in Sources */,
- B101000000000000000004 /* AppModel.swift in Sources */,
- B101000000000000000005 /* Route.swift in Sources */,
- B101000000000000000006 /* JSON.swift in Sources */,
- B101000000000000000007 /* Content.swift in Sources */,
- B101000000000000000008 /* Theme.swift in Sources */,
- B101000000000000000009 /* Answers.swift in Sources */,
- B101000000000000000010 /* ProgressStore.swift in Sources */,
- B101000000000000000011 /* Components.swift in Sources */,
- B101000000000000000012 /* Icons.swift in Sources */,
- B101000000000000000017 /* PathMap.swift in Sources */,
- B101000000000000000013 /* Screens.swift in Sources */,
- B101000000000000000014 /* Exercises.swift in Sources */,
- B101000000000000000015 /* ExercisesMore.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXSourcesBuildPhase section */
-
-/* Begin XCBuildConfiguration section */
- B901000000000000000001 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = dwarf;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = YES;
- GCC_DYNAMIC_NO_PIC = NO;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 26.0;
- MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
- ONLY_ACTIVE_ARCH = YES;
- SDKROOT = macosx;
- STRING_CATALOG_GENERATE_SYMBOLS = YES;
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- SWIFT_VERSION = 5.0;
- };
- name = Debug;
- };
- B901000000000000000002 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = YES;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 26.0;
- MTL_ENABLE_DEBUG_INFO = NO;
- SDKROOT = macosx;
- STRING_CATALOG_GENERATE_SYMBOLS = YES;
- SWIFT_COMPILATION_MODE = wholemodule;
- SWIFT_OPTIMIZATION_LEVEL = "-O";
- SWIFT_VERSION = 5.0;
- };
- name = Release;
- };
- B901000000000000000003 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_ENTITLEMENTS = Graduately/Graduately.entitlements;
- CODE_SIGN_STYLE = Automatic;
- COMBINE_HIDPI_IMAGES = YES;
- COMMIT = dev;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = C2U9S337H8;
- ENABLE_HARDENED_RUNTIME = YES;
- ENABLE_PREVIEWS = YES;
- GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_FILE = Graduately/Info.plist;
- INFOPLIST_KEY_CFBundleDisplayName = Graduately;
- INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
- INFOPLIST_KEY_NSHighResolutionCapable = YES;
- INFOPLIST_KEY_NSHumanReadableCopyright = "GPL-3.0";
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/../Frameworks",
- );
- MACOSX_DEPLOYMENT_TARGET = 26.0;
- MARKETING_VERSION = 0.2;
- PRODUCT_BUNDLE_IDENTIFIER = com.bukovinafilip.maturita.macos;
- PRODUCT_NAME = Graduately;
- SDKROOT = macosx;
- SUPPORTED_PLATFORMS = macosx;
- SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_VERSION = 5.0;
- };
- name = Debug;
- };
- B901000000000000000004 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_ENTITLEMENTS = Graduately/Graduately.entitlements;
- CODE_SIGN_STYLE = Automatic;
- COMBINE_HIDPI_IMAGES = YES;
- COMMIT = dev;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = C2U9S337H8;
- ENABLE_HARDENED_RUNTIME = YES;
- ENABLE_PREVIEWS = YES;
- GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_FILE = Graduately/Info.plist;
- INFOPLIST_KEY_CFBundleDisplayName = Graduately;
- INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
- INFOPLIST_KEY_NSHighResolutionCapable = YES;
- INFOPLIST_KEY_NSHumanReadableCopyright = "GPL-3.0";
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/../Frameworks",
- );
- MACOSX_DEPLOYMENT_TARGET = 26.0;
- MARKETING_VERSION = 0.2;
- PRODUCT_BUNDLE_IDENTIFIER = com.bukovinafilip.maturita.macos;
- PRODUCT_NAME = Graduately;
- SDKROOT = macosx;
- SUPPORTED_PLATFORMS = macosx;
- SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_VERSION = 5.0;
- };
- name = Release;
- };
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
- B801000000000000000001 /* Build configuration list for PBXProject "Graduately" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- B901000000000000000001 /* Debug */,
- B901000000000000000002 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- B801000000000000000002 /* Build configuration list for PBXNativeTarget "Graduately" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- B901000000000000000003 /* Debug */,
- B901000000000000000004 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
-/* End XCConfigurationList section */
- };
- rootObject = B001000000000000000001 /* Project object */;
-}
diff --git a/macos/Graduately.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/macos/Graduately.xcodeproj/project.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 919434a..0000000
--- a/macos/Graduately.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
diff --git a/macos/Graduately.xcodeproj/xcshareddata/xcschemes/Graduately.xcscheme b/macos/Graduately.xcodeproj/xcshareddata/xcschemes/Graduately.xcscheme
deleted file mode 100644
index 58bd4d5..0000000
--- a/macos/Graduately.xcodeproj/xcshareddata/xcschemes/Graduately.xcscheme
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/macos/Graduately/Assets.xcassets/AccentColor.colorset/Contents.json b/macos/Graduately/Assets.xcassets/AccentColor.colorset/Contents.json
deleted file mode 100644
index eb87897..0000000
--- a/macos/Graduately/Assets.xcassets/AccentColor.colorset/Contents.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "colors" : [
- {
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Graduately/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index 700d5cf..0000000
--- a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "images" : [
- { "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" },
- { "filename" : "icon_16x16@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" },
- { "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" },
- { "filename" : "icon_32x32@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" },
- { "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" },
- { "filename" : "icon_128x128@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" },
- { "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" },
- { "filename" : "icon_256x256@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" },
- { "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" },
- { "filename" : "icon_512x512@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
deleted file mode 100644
index c7f6be9..0000000
Binary files a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_128x128.png and /dev/null differ
diff --git a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
deleted file mode 100644
index f01e78a..0000000
Binary files a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png and /dev/null differ
diff --git a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
deleted file mode 100644
index 4c3e221..0000000
Binary files a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_16x16.png and /dev/null differ
diff --git a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
deleted file mode 100644
index dff719a..0000000
Binary files a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png and /dev/null differ
diff --git a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
deleted file mode 100644
index f01e78a..0000000
Binary files a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_256x256.png and /dev/null differ
diff --git a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
deleted file mode 100644
index c86d6ea..0000000
Binary files a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png and /dev/null differ
diff --git a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
deleted file mode 100644
index dff719a..0000000
Binary files a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_32x32.png and /dev/null differ
diff --git a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
deleted file mode 100644
index 6f0fa6c..0000000
Binary files a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png and /dev/null differ
diff --git a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
deleted file mode 100644
index c86d6ea..0000000
Binary files a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_512x512.png and /dev/null differ
diff --git a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
deleted file mode 100644
index 9fbf69f..0000000
Binary files a/macos/Graduately/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png and /dev/null differ
diff --git a/macos/Graduately/Assets.xcassets/Contents.json b/macos/Graduately/Assets.xcassets/Contents.json
deleted file mode 100644
index 73c0059..0000000
--- a/macos/Graduately/Assets.xcassets/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/macos/Graduately/Graduately.entitlements b/macos/Graduately/Graduately.entitlements
deleted file mode 100644
index 08ba3a3..0000000
--- a/macos/Graduately/Graduately.entitlements
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
- com.apple.security.app-sandbox
-
- com.apple.security.network.client
-
-
-
diff --git a/macos/Graduately/GraduatelyApp.swift b/macos/Graduately/GraduatelyApp.swift
deleted file mode 100644
index 45a44b0..0000000
--- a/macos/Graduately/GraduatelyApp.swift
+++ /dev/null
@@ -1,170 +0,0 @@
-import SwiftUI
-
-@main
-struct GraduatelyApp: App {
- @StateObject private var vm = AppModel()
-
- var body: some Scene {
- WindowGroup {
- MacRoot(vm: vm)
- }
- .defaultSize(width: 1280, height: 840)
- .windowResizability(.contentMinSize)
- .windowToolbarStyle(.unified)
- .windowStyle(.automatic)
- .commands {
- CommandGroup(replacing: .newItem) {}
- CommandGroup(after: .sidebar) {
- Button(vm.tr("search_home")) {
- vm.showPracticeRoot()
- }
- .keyboardShortcut("1", modifiers: [.command])
- Button(vm.tr("stats")) {
- vm.tab = .progress
- }
- .keyboardShortcut("2", modifiers: [.command])
- Button(vm.tr("search")) {
- vm.tab = .search
- }
- .keyboardShortcut("k", modifiers: [.command])
- Button(vm.tr("settings")) {
- vm.tab = .settings
- }
- .keyboardShortcut("3", modifiers: [.command])
- }
- }
-
- Settings {
- SettingsScreen(vm: vm)
- .formStyle(.grouped)
- .frame(minWidth: 480, minHeight: 520)
- .tint(vm.palette.tint)
- .preferredColorScheme(vm.mode == .dark ? .dark : .light)
- }
- }
-}
-
-struct MacRoot: View {
- @ObservedObject var vm: AppModel
- @State private var progressPath = NavigationPath()
- @State private var settingsPath = NavigationPath()
- @State private var searchPath = NavigationPath()
-
- var body: some View {
- tabs
- .tint(vm.palette.tint)
- .preferredColorScheme(vm.mode == .dark ? .dark : .light)
- .overlay(alignment: .top) {
- if showsUpdateAccessory {
- UpdateBanner(vm: vm)
- .padding(16)
- .transition(.move(edge: .top).combined(with: .opacity))
- }
- }
- .animation(Motion.settle, value: showsUpdateAccessory)
- .onAppear { vm.checkUpdate(false) }
- .frame(minWidth: 980, minHeight: 640)
- }
-
- private var showsUpdateAccessory: Bool {
- vm.update.canInstall || vm.update.status == "downloading" || vm.update.status == "staged"
- }
-
- private var tabs: some View {
- TabView(selection: $vm.tab) {
- Tab(vm.tr("search_home"), systemImage: "square.grid.2x2.fill", value: AppTab.practice) {
- NavigationStack(path: $vm.practicePath) {
- PracticeHome(vm: vm)
- .navigationDestination(for: Route.self) { route in
- RouteDestination(vm: vm, route: route)
- }
- }
- }
- Tab(vm.tr("stats"), systemImage: "chart.bar.fill", value: AppTab.progress) {
- NavigationStack(path: $progressPath) {
- StatsScreen(vm: vm)
- }
- }
- Tab(vm.tr("settings"), systemImage: "gearshape.fill", value: AppTab.settings) {
- NavigationStack(path: $settingsPath) {
- SettingsScreen(vm: vm)
- }
- }
- Tab(vm.tr("search"), systemImage: "magnifyingglass", value: AppTab.search, role: .search) {
- NavigationStack(path: $searchPath) {
- SearchScreen(vm: vm)
- }
- }
- }
- .tabViewStyle(.sidebarAdaptable)
- .toolbar {
- MacWindowToolbar(vm: vm)
- }
- }
-}
-
-private struct MacWindowToolbar: ToolbarContent {
- @ObservedObject var vm: AppModel
-
- var body: some ToolbarContent {
- ToolbarItemGroup(placement: .primaryAction) {
- Button {
- vm.tab = .search
- } label: {
- Label(vm.tr("search"), systemImage: "magnifyingglass")
- }
- .controlSize(.large)
- .help(vm.tr("search"))
-
- Button {
- vm.tab = .progress
- } label: {
- Label(vm.tr("stats"), systemImage: "chart.bar.fill")
- }
- .controlSize(.large)
- .help(vm.tr("stats"))
- }
-
- ToolbarSpacer(.fixed, placement: .primaryAction)
-
- ToolbarItem(placement: .primaryAction) {
- Button {
- vm.tab = .settings
- } label: {
- Label(vm.tr("settings"), systemImage: "gearshape")
- }
- .controlSize(.large)
- .help(vm.tr("settings"))
- }
- }
-}
-
-private struct UpdateBanner: View {
- @ObservedObject var vm: AppModel
-
- var body: some View {
- HStack(spacing: 12) {
- Image(systemName: "arrow.down.app.fill")
- .font(.title3)
- Text(bannerText)
- .font(.body)
- Spacer()
- if vm.update.canInstall {
- Button(vm.tr("update_install")) { vm.installUpdate() }
- .buttonStyle(.borderedProminent)
- .controlSize(.large)
- }
- }
- .padding(.horizontal, 18)
- .padding(.vertical, 14)
- .softSurface(cornerRadius: 16)
- .frame(maxWidth: 640)
- }
-
- private var bannerText: String {
- if let arg = vm.update.messageArg {
- return vm.fmt(vm.update.messageKey, arg)
- }
- return vm.tr(vm.update.messageKey)
- }
-}
diff --git a/macos/Graduately/Info.plist b/macos/Graduately/Info.plist
deleted file mode 100644
index 4787884..0000000
--- a/macos/Graduately/Info.plist
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- GITCommit
- $(COMMIT)
- UPDATE_BRANCH
- builds
- UPDATE_REPO
- pepaskopec-blip/Graduately
-
-
diff --git a/macos/Graduately/Platform.swift b/macos/Graduately/Platform.swift
deleted file mode 100644
index cc9c1e2..0000000
--- a/macos/Graduately/Platform.swift
+++ /dev/null
@@ -1,38 +0,0 @@
-#if os(macOS)
-import AppKit
-import SwiftUI
-
-extension NSColor {
- static var tertiarySystemFill: NSColor {
- NSColor.quaternaryLabelColor.withAlphaComponent(0.16)
- }
-
- static var secondarySystemGroupedBackground: NSColor {
- .controlBackgroundColor
- }
-
- static var separator: NSColor { .separatorColor }
-}
-
-enum TextInputAutocapitalization {
- case never
-}
-
-enum UIKeyboardType {
- case decimalPad
-}
-
-enum MacNavBarTitleMode {
- case large
- case inline
-}
-
-extension View {
- func textInputAutocapitalization(_ style: TextInputAutocapitalization) -> some View { self }
-
- func keyboardType(_ type: UIKeyboardType) -> some View { self }
-
- func navigationBarTitleDisplayMode(_ mode: MacNavBarTitleMode) -> some View { self }
-}
-
-#endif
diff --git a/macos/Graduately/Update/Updater.swift b/macos/Graduately/Update/Updater.swift
deleted file mode 100644
index 0b081cb..0000000
--- a/macos/Graduately/Update/Updater.swift
+++ /dev/null
@@ -1,233 +0,0 @@
-import AppKit
-import Foundation
-
-struct UpdateState {
- var status = "idle"
- var remote: String?
- var messageKey = "update_current"
- var messageArg: String? = String(AppConfig.commit.prefix(7))
- var canInstall = false
-}
-
-final class Updater {
- private let repo = AppConfig.updateRepo
- private let branch = AppConfig.updateBranch
-
- func check(interactive: Bool, cb: @escaping (UpdateState) -> Void) {
- if AppConfig.commit == "dev" {
- emit(cb, UpdateState(status: "dev", messageKey: "update_err_devbuild", messageArg: nil))
- return
- }
- emit(cb, UpdateState(status: "checking", messageKey: "update_checking", messageArg: nil))
- DispatchQueue.global(qos: .utility).async {
- do {
- let atom = try self.get("https://github.com/\(self.repo)/commits/\(self.branch).atom")
- let remote: String
- if let tip = self.atomTip(atom),
- let marker = try? self.get("https://raw.githubusercontent.com/\(self.repo)/\(tip)/VERSION-macos")
- .trimmingCharacters(in: .whitespacesAndNewlines),
- marker.count >= 7 {
- remote = marker
- } else if let sha = self.atomMainSha(atom) {
- remote = sha
- } else if let tip = self.atomTip(atom) {
- remote = try self.get("https://raw.githubusercontent.com/\(self.repo)/\(tip)/VERSION")
- .trimmingCharacters(in: .whitespacesAndNewlines)
- } else {
- throw URLError(.cannotParseResponse)
- }
- let local = AppConfig.commit
- if self.sameCommit(remote, local) {
- self.emit(cb, UpdateState(status: "ok", remote: remote, messageKey: "update_uptodate", messageArg: String(remote.prefix(7))))
- } else {
- self.emit(cb, UpdateState(status: "available", remote: remote, messageKey: "update_available", messageArg: String(remote.prefix(7)), canInstall: true))
- }
- } catch {
- self.emit(cb, UpdateState(status: "error", messageKey: "update_err_network", messageArg: nil))
- }
- }
- }
-
- func install(cb: @escaping (UpdateState) -> Void) {
- emit(cb, UpdateState(status: "downloading", messageKey: "update_downloading", messageArg: nil))
- DispatchQueue.global(qos: .utility).async {
- do {
- let atom = try self.get("https://github.com/\(self.repo)/commits/\(self.branch).atom")
- let tip = self.atomTip(atom) ?? ""
- guard !tip.isEmpty else { throw URLError(.cannotParseResponse) }
- let zipName = Self.assetName()
- let url = "https://raw.githubusercontent.com/\(self.repo)/\(tip)/\(zipName)"
- let cache = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
- ?? URL(fileURLWithPath: NSTemporaryDirectory())
- let staging = cache.appendingPathComponent("graduately-update-\(ProcessInfo.processInfo.processIdentifier)")
- try? FileManager.default.removeItem(at: staging)
- try FileManager.default.createDirectory(at: staging, withIntermediateDirectories: true)
- let archive = staging.appendingPathComponent(zipName)
- try self.download(url, to: archive)
- let unpacked = staging.appendingPathComponent("new")
- try FileManager.default.createDirectory(at: unpacked, withIntermediateDirectories: true)
- try self.run("/usr/bin/ditto", ["-x", "-k", archive.path, unpacked.path])
- guard let source = self.findApp(in: unpacked) else {
- throw URLError(.cannotParseResponse)
- }
- guard let target = Self.runningAppURL() else {
- throw URLError(.cannotOpenFile)
- }
- let script = self.writeSwapScript()
- let proc = Process()
- proc.executableURL = URL(fileURLWithPath: "/bin/sh")
- proc.arguments = [script.path, "\(ProcessInfo.processInfo.processIdentifier)", target.path, source.path, staging.path]
- try proc.run()
- self.emit(cb, UpdateState(status: "staged", remote: tip, messageKey: "update_staged", messageArg: String(tip.prefix(7))))
- DispatchQueue.main.async {
- NSApp.terminate(nil)
- }
- } catch {
- self.emit(cb, UpdateState(status: "error", messageKey: "update_err_download", messageArg: nil))
- }
- }
- }
-
- private static func assetName() -> String {
- #if arch(arm64)
- return "graduately-macos-arm64.zip"
- #else
- return "graduately-macos-x86_64.zip"
- #endif
- }
-
- private static func runningAppURL() -> URL? {
- let bundle = Bundle.main.bundleURL
- return bundle.pathExtension == "app" ? bundle : nil
- }
-
- private func findApp(in root: URL) -> URL? {
- let keys: [URLResourceKey] = [.isDirectoryKey]
- guard let en = FileManager.default.enumerator(at: root, includingPropertiesForKeys: keys) else { return nil }
- for case let url as URL in en where url.pathExtension == "app" {
- return url
- }
- return nil
- }
-
- private func writeSwapScript() -> URL {
- let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("graduately-swift-update.sh")
- let body = """
- #!/bin/sh
- if [ -z "$MATURITA_UPDATE_DETACHED" ]; then
- MATURITA_UPDATE_DETACHED=1
- export MATURITA_UPDATE_DETACHED
- nohup /bin/sh "$0" "$@" >/dev/null 2>&1 &
- exit 0
- fi
- pid=$1; target=$2; source=$3; staging=$4
- n=0
- while kill -0 "$pid" 2>/dev/null && [ $n -lt 600 ]; do
- sleep 0.2; n=$((n+1))
- done
- place=$target
- if /usr/bin/ditto "$source" "$target" 2>/dev/null; then
- xattr -cr "$target" 2>/dev/null
- else
- alt="$HOME/Applications/$(basename "$target")"
- mkdir -p "$HOME/Applications"
- if /usr/bin/ditto "$source" "$alt" 2>/dev/null; then
- xattr -cr "$alt" 2>/dev/null
- place=$alt
- fi
- fi
- open -n "$place"
- rm -rf "$staging"
- rm -f "$0"
- """
- try? body.write(to: url, atomically: true, encoding: .utf8)
- try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path)
- return url
- }
-
- private func emit(_ cb: @escaping (UpdateState) -> Void, _ state: UpdateState) {
- DispatchQueue.main.async { cb(state) }
- }
-
- private func atomTip(_ atom: String) -> String? {
- if let m = atom.range(of: #"Commit/([0-9a-f]{40})"#, options: [.regularExpression, .caseInsensitive]) {
- return String(atom[m]).split(separator: "/").last.map(String.init)
- }
- if let m = atom.range(of: #"/commit/([0-9a-f]{7,40})"#, options: [.regularExpression, .caseInsensitive]) {
- return String(atom[m]).split(separator: "/").last.map(String.init)
- }
- return nil
- }
-
- private func atomMainSha(_ atom: String) -> String? {
- guard let regex = try? NSRegularExpression(pattern: #"Build from ([0-9a-f]{7,40})"#, options: .caseInsensitive),
- let match = regex.firstMatch(in: atom, range: NSRange(atom.startIndex..., in: atom)),
- let range = Range(match.range(at: 1), in: atom)
- else { return nil }
- return String(atom[range])
- }
-
- private func sameCommit(_ a: String, _ b: String) -> Bool {
- let n = min(a.count, b.count)
- return n >= 7 && a.prefix(n).lowercased() == b.prefix(n).lowercased()
- }
-
- private func get(_ url: String) throws -> String {
- guard let u = URL(string: url) else { throw URLError(.badURL) }
- var req = URLRequest(url: u, timeoutInterval: 15)
- req.setValue("Graduately-macos", forHTTPHeaderField: "User-Agent")
- req.setValue("*/*", forHTTPHeaderField: "Accept")
- let sem = DispatchSemaphore(value: 0)
- var result: Result = .failure(URLError(.unknown))
- URLSession.shared.dataTask(with: req) { data, resp, err in
- if let err { result = .failure(err) }
- else if let http = resp as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
- result = .failure(URLError(.badServerResponse))
- } else if let data, let text = String(data: data, encoding: .utf8) {
- result = .success(text)
- } else {
- result = .failure(URLError(.cannotDecodeContentData))
- }
- sem.signal()
- }.resume()
- _ = sem.wait(timeout: .now() + 20)
- return try result.get()
- }
-
- private func download(_ url: String, to dest: URL) throws {
- guard let u = URL(string: url) else { throw URLError(.badURL) }
- let sem = DispatchSemaphore(value: 0)
- var result: Result = .failure(URLError(.unknown))
- let task = URLSession.shared.downloadTask(with: u) { file, resp, err in
- if let err { result = .failure(err) }
- else if let http = resp as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
- result = .failure(URLError(.badServerResponse))
- } else if let file {
- do {
- try? FileManager.default.removeItem(at: dest)
- try FileManager.default.moveItem(at: file, to: dest)
- result = .success(())
- } catch {
- result = .failure(error)
- }
- } else {
- result = .failure(URLError(.cannotDecodeContentData))
- }
- sem.signal()
- }
- task.resume()
- _ = sem.wait(timeout: .now() + 1800)
- try result.get()
- }
-
- private func run(_ launch: String, _ args: [String]) throws {
- let proc = Process()
- proc.executableURL = URL(fileURLWithPath: launch)
- proc.arguments = args
- try proc.run()
- proc.waitUntilExit()
- if proc.terminationStatus != 0 {
- throw URLError(.cannotParseResponse)
- }
- }
-}
diff --git a/macos/Graduately/cs.lproj/Localizable.strings b/macos/Graduately/cs.lproj/Localizable.strings
deleted file mode 100644
index a080c4c..0000000
--- a/macos/Graduately/cs.lproj/Localizable.strings
+++ /dev/null
@@ -1 +0,0 @@
-/* Označí češtinu jako podporovaný jazyk, aby nabídky macOS zůstaly česky na českém systému. */
diff --git a/macos/Graduately/en.lproj/Localizable.strings b/macos/Graduately/en.lproj/Localizable.strings
deleted file mode 100644
index 3c0e8ca..0000000
--- a/macos/Graduately/en.lproj/Localizable.strings
+++ /dev/null
@@ -1 +0,0 @@
-/* Marks English as a supported language so macOS menus follow an English system. */
diff --git a/maturita-android.apk b/maturita-android.apk
new file mode 100644
index 0000000..b571264
Binary files /dev/null and b/maturita-android.apk differ
diff --git a/maturita-installer-android.zip b/maturita-installer-android.zip
new file mode 100644
index 0000000..a38e7d9
Binary files /dev/null and b/maturita-installer-android.zip differ
diff --git a/maturita-installer-ios.zip b/maturita-installer-ios.zip
new file mode 100644
index 0000000..ec608cd
Binary files /dev/null and b/maturita-installer-ios.zip differ
diff --git a/maturita-installer-linux.sh b/maturita-installer-linux.sh
new file mode 100755
index 0000000..b31c863
--- /dev/null
+++ b/maturita-installer-linux.sh
@@ -0,0 +1,92 @@
+#!/bin/sh
+# Graduately installer for Linux.
+#
+# Carries no application of its own. Fastly caches branch-named URLs on
+# raw.githubusercontent.com, so this script looks up the tip of `builds`
+# and downloads that commit — not a stale AppImage from the last five minutes.
+#
+# Double-click the file (or run: sh maturita-installer-linux.sh).
+# The app updates itself from then on.
+
+set -eu
+
+REPO="pepaskopec-blip/Graduately"
+BRANCH="builds"
+ASSET="maturita-linux-x86_64.AppImage"
+
+DEST_DIR="$HOME/Applications"
+DEST="$DEST_DIR/maturita.AppImage"
+DESKTOP_DIR="$HOME/.local/share/applications"
+ICON_DIR="$HOME/.local/share/icons/hicolor/512x512/apps"
+
+say() { printf '%s\n' "$*"; }
+die() {
+ printf '\nError: %s\n' "$*" >&2
+ printf 'Press Enter to close. '
+ read -r _ || true
+ exit 1
+}
+
+builds_sha() {
+ curl -fsSL --max-time 20 \
+ "https://github.com/$REPO/commits/$BRANCH.atom" |
+ sed -n 's/.*Commit\/\([0-9a-f]\{40\}\).*/\1/p' |
+ head -n 1
+}
+
+say "Installing Graduately"
+say "---------------------"
+
+arch=$(uname -m)
+if [ "$arch" != "x86_64" ]; then
+ die "only x86_64 builds are published, but this machine reports '$arch'.
+Build from source instead: https://github.com/$REPO"
+fi
+
+command -v curl >/dev/null 2>&1 || die "curl was not found. Install it first."
+
+mkdir -p "$DEST_DIR"
+
+tmp=$(mktemp -d)
+trap 'rm -rf "$tmp"' EXIT INT TERM
+
+say "Looking up the latest build..."
+sha=$(builds_sha)
+[ ${#sha} -eq 40 ] || die "could not find the latest build on GitHub."
+URL="https://raw.githubusercontent.com/$REPO/$sha/$ASSET"
+
+say "Downloading build $(printf '%.7s' "$sha")..."
+curl -fL --progress-bar --max-time 1800 -o "$tmp/$ASSET" "$URL" ||
+ die "the download failed. Check your internet connection."
+
+say "Installing to $DEST"
+chmod +x "$tmp/$ASSET"
+mv -f "$tmp/$ASSET" "$DEST" || die "could not write to $DEST_DIR."
+
+if (cd "$tmp" && "$DEST" --appimage-extract \
+ 'usr/share/icons/hicolor/512x512/apps/maturita.png' >/dev/null 2>&1); then
+ icon="$tmp/squashfs-root/usr/share/icons/hicolor/512x512/apps/maturita.png"
+ if [ -f "$icon" ]; then
+ mkdir -p "$ICON_DIR"
+ cp -f "$icon" "$ICON_DIR/maturita.png"
+ fi
+fi
+
+mkdir -p "$DESKTOP_DIR"
+cat > "$DESKTOP_DIR/maturita.desktop" </dev/null 2>&1 &&
+ update-desktop-database "$DESKTOP_DIR" >/dev/null 2>&1 || true
+
+say ""
+say "Done. Starting the app..."
+"$DEST" >/dev/null 2>&1 &
diff --git a/maturita-installer-linux.zip b/maturita-installer-linux.zip
new file mode 100644
index 0000000..af483b4
Binary files /dev/null and b/maturita-installer-linux.zip differ
diff --git a/maturita-installer-macos.zip b/maturita-installer-macos.zip
new file mode 100644
index 0000000..28c6e79
Binary files /dev/null and b/maturita-installer-macos.zip differ
diff --git a/maturita-installer-windows.cmd b/maturita-installer-windows.cmd
new file mode 100644
index 0000000..9a185e5
--- /dev/null
+++ b/maturita-installer-windows.cmd
@@ -0,0 +1,82 @@
+@echo off
+rem Graduately installer for Windows.
+rem
+rem Carries no application of its own. Fastly caches branch-named URLs on
+rem raw.githubusercontent.com, so this script looks up the tip of `builds`
+rem and downloads that commit — not a stale zip from the last five minutes.
+rem
+rem Double-click the file to run it. The app updates itself from then on.
+
+setlocal EnableDelayedExpansion
+set "REPO=pepaskopec-blip/Graduately"
+set "BRANCH=builds"
+set "ASSET=maturita-windows-x64.zip"
+set "DEST=%LOCALAPPDATA%\Programs\Graduately"
+
+echo Installing Graduately
+echo ---------------------
+
+where curl >nul 2>&1 || goto :no_tools
+where tar >nul 2>&1 || goto :no_tools
+
+set "TMP_DIR=%TEMP%\maturita-installer"
+rmdir /s /q "%TMP_DIR%" 2>nul
+mkdir "%TMP_DIR%" || goto :fail_tmp
+
+echo Looking up the latest build...
+set "SHA="
+for /f "usebackq delims=" %%S in (`powershell -NoProfile -ExecutionPolicy Bypass -Command "$r = & curl.exe -fsSL --max-time 20 'https://github.com/%REPO%/commits/%BRANCH%.atom'; if ($r -match 'Commit/([0-9a-f]{40})') { $Matches[1] }"`) do set "SHA=%%S"
+if not defined SHA goto :fail_download
+set "URL=https://raw.githubusercontent.com/%REPO%/%SHA%/%ASSET%"
+
+echo Downloading build %SHA:~0,7%...
+curl -fL --progress-bar --max-time 1800 -o "%TMP_DIR%\%ASSET%" "%URL%" || goto :fail_download
+
+echo Installing to %DEST%
+if not exist "%DEST%" mkdir "%DEST%" || goto :fail_dest
+
+tar -xf "%TMP_DIR%\%ASSET%" -C "%DEST%" || goto :fail_extract
+
+set "LNK=%APPDATA%\Microsoft\Windows\Start Menu\Programs\Graduately.lnk"
+powershell -NoProfile -ExecutionPolicy Bypass -Command ^
+ "$s=(New-Object -ComObject WScript.Shell).CreateShortcut('%LNK%');" ^
+ "$s.TargetPath='%DEST%\maturita.exe';$s.WorkingDirectory='%DEST%';$s.Save()" >nul 2>&1
+
+rmdir /s /q "%TMP_DIR%" 2>nul
+
+echo.
+echo Done. Starting the app...
+start "" "%DEST%\maturita.exe"
+exit /b 0
+
+:no_tools
+echo.
+echo Error: this installer needs curl and tar, which come with Windows 10
+echo version 1803 and later. Please update Windows, or download the zip
+echo manually from https://github.com/%REPO%/tree/%BRANCH%
+pause
+exit /b 1
+
+:fail_tmp
+echo.
+echo Error: could not create a temporary folder in %TEMP%.
+pause
+exit /b 1
+
+:fail_download
+echo.
+echo Error: the download failed. Check your internet connection.
+pause
+exit /b 1
+
+:fail_dest
+echo.
+echo Error: could not create %DEST%.
+pause
+exit /b 1
+
+:fail_extract
+echo.
+echo Error: the archive could not be unpacked.
+pause
+exit /b 1
diff --git a/maturita-installer-windows.zip b/maturita-installer-windows.zip
new file mode 100644
index 0000000..5ef36d9
Binary files /dev/null and b/maturita-installer-windows.zip differ
diff --git a/maturita-ios.ipa b/maturita-ios.ipa
new file mode 100644
index 0000000..573f605
Binary files /dev/null and b/maturita-ios.ipa differ
diff --git a/maturita-linux-x86_64.AppImage b/maturita-linux-x86_64.AppImage
new file mode 100644
index 0000000..707d47e
Binary files /dev/null and b/maturita-linux-x86_64.AppImage differ
diff --git a/maturita-macos-arm64.zip b/maturita-macos-arm64.zip
new file mode 100644
index 0000000..8244109
Binary files /dev/null and b/maturita-macos-arm64.zip differ
diff --git a/maturita-windows-x64.zip b/maturita-windows-x64.zip
new file mode 100644
index 0000000..ef50bd1
Binary files /dev/null and b/maturita-windows-x64.zip differ
diff --git a/scripts/build-content.py b/scripts/build-content.py
deleted file mode 100644
index ca44c03..0000000
--- a/scripts/build-content.py
+++ /dev/null
@@ -1,1195 +0,0 @@
-#!/usr/bin/env python3
-"""Build content.json from the Markdown files in content/.
-
-The format is described in content/README.md. Every platform reads the
-generated JSON; nothing under content/ is read at run time.
-"""
-
-from __future__ import annotations
-
-import json
-import re
-import sys
-import unicodedata
-from dataclasses import dataclass, field
-from pathlib import Path
-
-
-def _force_utf8_stdio() -> None:
- """Windows defaults stdout to cp1252, which cannot print Czech logs."""
- for stream in (sys.stdout, sys.stderr):
- reconfigure = getattr(stream, "reconfigure", None)
- if reconfigure is None:
- continue
- try:
- reconfigure(encoding="utf-8", errors="replace")
- except (OSError, ValueError):
- pass
-
-
-_force_utf8_stdio()
-
-ROOT = Path(__file__).resolve().parents[1]
-CONTENT = ROOT / "content"
-OUTS = [ROOT / "android" / "app" / "src" / "main" / "assets" / "content.json"]
-
-
-# ---------------------------------------------------------------------------
-# Errors
-# ---------------------------------------------------------------------------
-
-class ContentError(Exception):
- pass
-
-
-ERRORS: list[str] = []
-WARNINGS: list[str] = []
-
-
-def rel(path: Path) -> str:
- try:
- return str(path.relative_to(ROOT))
- except ValueError:
- return str(path)
-
-
-def fail(path: Path, line: int | None, msg: str) -> None:
- where = f"{rel(path)}:{line}" if line else rel(path)
- ERRORS.append(f"{where}: {msg}")
-
-
-def warn(path: Path, line: int | None, msg: str) -> None:
- where = f"{rel(path)}:{line}" if line else rel(path)
- WARNINGS.append(f"{where}: {msg}")
-
-
-# ---------------------------------------------------------------------------
-# Markdown subset
-# ---------------------------------------------------------------------------
-
-@dataclass
-class Node:
- kind: str # h1 h2 h3 bullet num quote answer table para
- text: str
- line: int
- checked: bool = False
- rows: list[list[str]] = field(default_factory=list)
- lines: list[str] = field(default_factory=list)
-
-
-@dataclass
-class Doc:
- path: Path
- meta: dict[str, str]
- meta_lines: dict[str, int]
- nodes: list[Node]
-
-
-BULLET = re.compile(r"^- (\[([ xX])\] )?(.*)$")
-NUMBERED = re.compile(r"^(\d+)\. (.*)$")
-TABLE_SEP = re.compile(r"^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$")
-
-
-def parse_doc(path: Path) -> Doc:
- raw = path.read_text(encoding="utf-8").replace("\r\n", "\n")
- lines = raw.split("\n")
- meta: dict[str, str] = {}
- meta_lines: dict[str, int] = {}
- i = 0
- if lines and lines[0].strip() == "---":
- i = 1
- while i < len(lines) and lines[i].strip() != "---":
- ln = lines[i]
- if ln.strip() and not ln.lstrip().startswith("#"):
- key, sep, value = ln.partition(":")
- if not sep:
- fail(path, i + 1, f"v hlavičce chybí dvojtečka: {ln!r}")
- else:
- meta[key.strip()] = value.strip()
- meta_lines[key.strip()] = i + 1
- i += 1
- if i >= len(lines):
- fail(path, 1, "hlavička začíná --- , ale nikde nekončí")
- i += 1
-
- nodes: list[Node] = []
- para: Node | None = None
- table: Node | None = None
- in_comment = False
- indexes = iter(range(i, len(lines)))
- for n in indexes:
- ln = lines[n].rstrip()
- no = n + 1
- if ln.startswith("```"):
- para = table = None
- body: list[str] = []
- for n2 in indexes:
- raw_line = lines[n2]
- if raw_line.rstrip().startswith("```"):
- break
- body.append(raw_line)
- nodes.append(Node("code", "\n".join(body).strip("\n"), no))
- continue
- if in_comment:
- if "-->" in ln:
- in_comment = False
- continue
- if ln.lstrip().startswith("" not in ln:
- in_comment = True
- para = table = None
- continue
- if not ln.strip():
- para = table = None
- continue
- if ln.startswith("|"):
- para = None
- if table is None:
- table = Node("table", "", no)
- nodes.append(table)
- if not TABLE_SEP.match(ln):
- table.rows.append(split_row(ln))
- continue
- table = None
- if ln.startswith("\\"):
- text = ln[1:]
- if para is None:
- para = Node("para", "", no)
- nodes.append(para)
- para.lines.append(text)
- continue
- m = re.match(r"^(#{1,3}) (.*)$", ln) or re.match(r"^(#{1,3})$", ln)
- if m:
- para = None
- nodes.append(Node(f"h{len(m.group(1))}", (m.group(2) if m.lastindex and m.lastindex > 1 else "").strip(), no))
- continue
- m = BULLET.match(ln)
- if m:
- para = None
- nodes.append(Node("bullet", m.group(3), no, checked=bool(m.group(2)) and m.group(2).lower() == "x"))
- continue
- m = NUMBERED.match(ln)
- if m:
- para = None
- nodes.append(Node("num", m.group(2), no))
- continue
- if ln.startswith("> ") or ln == ">":
- para = None
- nodes.append(Node("quote", ln[2:], no))
- continue
- if ln.startswith("= ") or ln == "=":
- para = None
- nodes.append(Node("answer", ln[2:], no))
- continue
- if para is None:
- para = Node("para", "", no)
- nodes.append(para)
- para.lines.append(ln)
- return Doc(path, meta, meta_lines, nodes)
-
-
-def split_row(line: str) -> list[str]:
- s = line.strip()
- if s.startswith("|"):
- s = s[1:]
- if s.endswith("|") and not s.endswith("\\|"):
- s = s[:-1]
- cells: list[str] = []
- cur: list[str] = []
- i = 0
- while i < len(s):
- c = s[i]
- if c == "\\" and i + 1 < len(s):
- cur.append(s[i:i + 2])
- i += 2
- continue
- if c == "|":
- cells.append("".join(cur).strip())
- cur = []
- else:
- cur.append(c)
- i += 1
- cells.append("".join(cur).strip())
- return cells
-
-
-def unesc(s: str) -> str:
- out = []
- i = 0
- while i < len(s):
- if s[i] == "\\" and i + 1 < len(s):
- out.append("\n" if s[i + 1] == "n" else s[i + 1])
- i += 2
- continue
- out.append(s[i])
- i += 1
- return "".join(out)
-
-
-def ungap(s: str) -> tuple[list[str], list[str]]:
- """Split 'a [x] b [y] c' into segments ['a ', ' b ', ' c'] and answers ['x', 'y']."""
- segs: list[str] = []
- answers: list[str] = []
- cur: list[str] = []
- in_gap = False
- i = 0
- while i < len(s):
- c = s[i]
- if c == "\\" and i + 1 < len(s):
- cur.append("\n" if s[i + 1] == "n" else s[i + 1])
- i += 2
- continue
- if c == "[" and not in_gap:
- segs.append("".join(cur))
- cur = []
- in_gap = True
- elif c == "]" and in_gap:
- answers.append("".join(cur))
- cur = []
- in_gap = False
- else:
- cur.append(c)
- i += 1
- if in_gap:
- raise ContentError(f"neuzavřená mezera [ ... ] v {s!r}")
- segs.append("".join(cur))
- return segs, answers
-
-
-def norm(s: str) -> str:
- s = unicodedata.normalize("NFKD", s.replace("ß", "ss"))
- s = "".join(c for c in s if not unicodedata.combining(c))
- return re.sub(r"\s+", " ", s).strip().lower()
-
-
-def slugify(s: str) -> str:
- return re.sub(r"[^a-z0-9]+", "-", norm(s)).strip("-")
-
-
-# ---------------------------------------------------------------------------
-# Structure helpers
-# ---------------------------------------------------------------------------
-
-@dataclass
-class Part:
- title: str
- line: int
- nodes: list[Node]
-
- def items(self) -> list["Part"]:
- """Split on ## headings. Nodes before the first ## are dropped."""
- out: list[Part] = []
- for nd in self.nodes:
- if nd.kind == "h2":
- out.append(Part(nd.text, nd.line, []))
- elif out:
- out[-1].nodes.append(nd)
- return out
-
- def head(self) -> list[Node]:
- out = []
- for nd in self.nodes:
- if nd.kind == "h2":
- break
- out.append(nd)
- return out
-
- def subparts(self) -> list["Part"]:
- out: list[Part] = []
- for nd in self.nodes:
- if nd.kind == "h3":
- out.append(Part(nd.text, nd.line, []))
- elif out:
- out[-1].nodes.append(nd)
- return out
-
-
-def parts(doc: Doc) -> list[Part]:
- out: list[Part] = []
- for nd in doc.nodes:
- if nd.kind == "h1":
- out.append(Part(nd.text, nd.line, []))
- elif out:
- out[-1].nodes.append(nd)
- elif nd.kind != "h1":
- fail(doc.path, nd.line, "text před první sekcí (# Název) se nepoužije")
- return out
-
-
-def part(doc: Doc, *names: str) -> Part | None:
- wanted = {norm(n) for n in names}
- for p in parts(doc):
- if norm(p.title) in wanted:
- return p
- return None
-
-
-def text_of(nodes: list[Node]) -> str | None:
- paras = ["\n".join(nd.lines) for nd in nodes if nd.kind == "para"]
- return "\n\n".join(paras) if paras else None
-
-
-def bullets(nodes: list[Node]) -> list[str]:
- return [nd.text for nd in nodes if nd.kind == "bullet"]
-
-
-def numbered(nodes: list[Node]) -> list[str]:
- return [nd.text for nd in nodes if nd.kind == "num"]
-
-
-def quote(nodes: list[Node]) -> str | None:
- q = [nd.text for nd in nodes if nd.kind == "quote"]
- return "\n".join(q) if q else None
-
-
-def answer(nodes: list[Node]) -> str | None:
- a = [nd.text for nd in nodes if nd.kind == "answer"]
- return "\n".join(a) if a else None
-
-
-def table(doc: Doc, nodes: list[Node], cols: int, where: str) -> list[list[str]]:
- tables = [nd for nd in nodes if nd.kind == "table"]
- if not tables:
- return []
- t = tables[0]
- rows = t.rows[1:]
- for k, r in enumerate(rows):
- if len(r) != cols:
- fail(doc.path, t.line + k + 2,
- f"{where}: řádek tabulky má {len(r)} sloupců, čekám {cols} "
- "(svislítko uvnitř textu se píše jako \\|)")
- return [r + [""] * (cols - len(r)) for r in rows]
-
-
-def flag(doc: Doc, key: str) -> bool | None:
- if key not in doc.meta:
- return None
- v = norm(doc.meta[key])
- if v in ("ano", "true", "yes", "1"):
- return True
- if v in ("ne", "false", "no", "0"):
- return False
- fail(doc.path, doc.meta_lines.get(key), f"{key}: čekám ano nebo ne, ne {doc.meta[key]!r}")
- return None
-
-
-def need(doc: Doc, key: str) -> str:
- v = doc.meta.get(key)
- if v is None:
- fail(doc.path, None, f"v hlavičce chybí {key}:")
- return ""
- return v
-
-
-def opt_int(s: str) -> int | None:
- s = s.strip()
- return int(s) if re.fullmatch(r"-?\d+", s) else None
-
-
-# ---------------------------------------------------------------------------
-# Shared readers
-# ---------------------------------------------------------------------------
-
-def read_quiz(doc: Doc, p: Part | None, expl_key: str | None = "expl") -> list[dict]:
- if p is None:
- return []
- out = []
- for it in p.items():
- opts = [nd for nd in it.nodes if nd.kind == "bullet"]
- if len(opts) < 2:
- fail(doc.path, it.line, f"otázka {it.title!r} má méně než dvě možnosti")
- right = [k for k, nd in enumerate(opts) if nd.checked]
- if len(right) != 1:
- fail(doc.path, it.line, f"otázka {it.title!r} musí mít právě jednu správnou možnost (- [x] …)")
- q = {"prompt": it.title, "options": [nd.text for nd in opts],
- "correct": right[0] if right else 0}
- ex = quote(it.nodes)
- if ex is not None and expl_key:
- q[expl_key] = ex
- out.append(q)
- return out
-
-
-def read_slides(doc: Doc, p: Part | None) -> list[dict]:
- if p is None:
- return []
- items = p.items()
- out = []
- for k, it in enumerate(items):
- label = None
- for nd in it.nodes:
- if nd.kind == "para":
- m = re.fullmatch(r"\*(.+)\*", nd.lines[0])
- if m:
- label = m.group(1)
- break
- if label is None:
- fail(doc.path, it.line, f"snímek {it.title!r}: pod nadpisem chybí téma (*Téma*)")
- label = ""
- out.append({
- "kicker": f"{k + 1} / {len(items)} • {label}",
- "title": it.title,
- "tip": quote(it.nodes),
- "lines": bullets(it.nodes),
- })
- return out
-
-
-def read_typed(doc: Doc, p: Part | None, meaning_key: str = "meaning") -> list[dict]:
- if p is None:
- return []
- out = []
- for it in p.items():
- a = answer(it.nodes)
- if a is None:
- fail(doc.path, it.line, f"úloha {it.title!r} nemá odpověď (= …)")
- out.append({"prompt": it.title, "answers": a or "", meaning_key: quote(it.nodes)})
- return out
-
-
-# ---------------------------------------------------------------------------
-# i18n
-# ---------------------------------------------------------------------------
-
-class I18n:
- def __init__(self) -> None:
- self.map: dict[str, dict] = {}
- self.origin: dict[str, str] = {}
-
- def put(self, key: str, cs: str | None, en: str | None, src: Path) -> str:
- if key in self.map and self.origin[key] != rel(src):
- prev = self.map[key]
- if (prev["cs"], prev["en"]) != (cs, en):
- warn(src, None, f"klíč {key!r} už definuje {self.origin[key]}")
- self.map[key] = {"cs": cs, "en": en}
- self.origin[key] = rel(src)
- return key
-
- def text(self, cs: str, en: str | None, src: Path) -> None:
- """Translation of a content string: the Czech text itself is the key."""
- if cs in self.map:
- prev = self.map[cs]
- if prev["en"] and en and prev["en"] != en:
- warn(src, None, f"text {cs[:40]!r} má jiný překlad v {self.origin[cs]}")
- if prev["en"] and not en:
- return
- self.map[cs] = {"cs": cs, "en": en}
- self.origin[cs] = rel(src)
-
-
-I18N = I18n()
-
-
-def en_doc(path: Path) -> Doc | None:
- p = path.with_name(path.stem + ".en.md")
- return parse_doc(p) if p.exists() else None
-
-
-def load_translations(doc: Doc | None) -> None:
- if doc is None:
- return
- p = part(doc, "Překlady", "Translations")
- if p is None:
- return
- for r in table(doc, p.nodes, 2, "Překlady"):
- cs, en = unesc(r[0]), unesc(r[1])
- if cs:
- I18N.text(cs, en or None, doc.path)
-
-
-def keyed(key: str, doc: Doc, en: Doc | None, field_name: str, required: bool = True) -> str | None:
- cs = doc.meta.get(field_name)
- if cs is None:
- if required:
- fail(doc.path, None, f"v hlavičce chybí {field_name}:")
- return None
- e = en.meta.get(field_name) if en else None
- return I18N.put(key, cs or None, e or None, doc.path)
-
-
-# ---------------------------------------------------------------------------
-# Progress keys
-# ---------------------------------------------------------------------------
-
-PROGRESS: dict[str, str] = {}
-LEGACY: dict[str, str] = {}
-IDS: dict[str, Path] = {}
-
-
-def lesson_id(doc: Doc, address: str) -> str:
- lid = need(doc, "id")
- if lid in IDS:
- fail(doc.path, doc.meta_lines.get("id"), f"id {lid!r} už má {rel(IDS[lid])}")
- IDS[lid] = doc.path
- PROGRESS[address] = lid
- old = doc.meta.get("puvodni")
- if old:
- for o in old.split(","):
- LEGACY[o.strip()] = lid
- return lid
-
-
-# ---------------------------------------------------------------------------
-# Lesson kinds
-# ---------------------------------------------------------------------------
-
-def by_order(p: Path) -> tuple[int, str]:
- m = re.match(r"^(\d+)-", p.name)
- return (int(m.group(1)) if m else 1 << 30, p.name)
-
-
-def lesson_files(directory: Path) -> list[Path]:
- if not directory.is_dir():
- fail(directory, None, "složka chybí")
- return []
- files = sorted((p for p in directory.glob("*.md")
- if not p.name.endswith(".en.md") and not p.name.startswith("_")), key=by_order)
- for p in files:
- if not re.match(r"^\d+-", p.name):
- fail(p, None, "název souboru musí začínat pořadím, např. 03-nazev.md")
- return files
-
-
-def code_of(p: Part | None) -> str:
- if p is None:
- return ""
- for nd in p.nodes:
- if nd.kind == "code":
- return nd.text
- return ""
-
-
-def code_lessons(directory: Path, prefix: str) -> list[dict]:
- """Lessons with an explanation and a C# program the student can edit and run."""
- out = []
- for i, path in enumerate(lesson_files(directory), start=1):
- doc, en = parse_doc(path), en_doc(path)
- load_translations(en)
- lid = lesson_id(doc, f"{prefix}.{i}")
- code = code_of(part(doc, "Kód", "Code"))
- if not code.strip():
- fail(path, None, "lekce nemá ukázkový kód v sekci # Kód")
- out.append({
- "id": i,
- "key": lid,
- "titleKey": keyed(f"{prefix}_unit{i}", doc, en, "nazev"),
- "subKey": keyed(f"{prefix}_unit{i}_sub", doc, en, "popis"),
- "slides": read_slides(doc, part(doc, "Výklad")),
- "code": code,
- })
- return out
-
-
-def std_lessons(directory: Path, prefix: str) -> list[dict]:
- """Lessons with slides and a choice quiz (IT, civics, literature, sciences, physics)."""
- out = []
- for i, path in enumerate(lesson_files(directory), start=1):
- doc, en = parse_doc(path), en_doc(path)
- load_translations(en)
- lid = lesson_id(doc, f"{prefix}.{i}")
- ex_key = "net_ex_title" if prefix == "net" and i == 1 else f"{prefix}_ex{i}_title"
- lesson = {
- "id": i,
- "key": lid,
- "titleKey": keyed(f"{prefix}_unit{i}", doc, en, "nazev"),
- "subKey": keyed(f"{prefix}_unit{i}_sub", doc, en, "popis"),
- "exTitleKey": keyed(ex_key, doc, en, "nazev_cviceni"),
- "quizHeadKey": keyed(f"{prefix}_quiz{i}_head", doc, en, "nadpis_kvizu", required=False),
- "slides": read_slides(doc, part(doc, "Výklad")),
- "quiz": read_quiz(doc, part(doc, "Kvíz")),
- }
- out.append(lesson)
- if prefix == "net" and i == 1:
- NET_TASKS.extend(read_net_tasks(doc, part(doc, "Úlohy na podsítě")))
- return out
-
-
-NET_TASKS: list[tuple[dict, list[dict]]] = []
-
-
-def read_net_tasks(doc: Doc, p: Part | None) -> list[tuple[dict, list[dict]]]:
- if p is None:
- return []
- out = []
- for it in p.items():
- head = []
- for nd in it.nodes:
- if nd.kind == "h3":
- break
- head.append(nd)
- sub = {norm(s.title): s for s in it.subparts()}
- sol = sub.get(norm("Řešení"))
- chk = sub.get(norm("Kontrola"))
- rows = []
- for r in table(doc, chk.nodes if chk else [], 5, "Kontrola"):
- pfx = opt_int(r[0])
- rows.append({"pfx": pfx if pfx is not None else -1, "net": r[1] or None,
- "bcast": r[2] or None, "lo": r[3] or None, "hi": r[4] or None})
- out.append(({"prompt": text_of(head) or "",
- "solution": text_of(sol.nodes) if sol else ""}, rows))
- return out
-
-
-def math_lessons(directory: Path, prefix: str) -> list[dict]:
- out = []
- for i, path in enumerate(lesson_files(directory), start=1):
- doc, en = parse_doc(path), en_doc(path)
- load_translations(en)
- lid = lesson_id(doc, f"{prefix}.{i}")
- problems = []
- for it in (part(doc, "Příklady").items() if part(doc, "Příklady") else []):
- a = answer(it.nodes)
- if a is None:
- fail(doc.path, it.line, f"příklad {it.title!r} nemá výsledek (= …)")
- problems.append({"prompt": it.title, "answer": a or "", "hint": quote(it.nodes) or ""})
- out.append({
- "id": i,
- "key": lid,
- "titleKey": keyed(f"{prefix}_unit{i}", doc, en, "nazev"),
- "subKey": keyed(f"{prefix}_unit{i}_sub", doc, en, "popis"),
- "exTitleKey": keyed(f"{prefix}_ex{i}_title", doc, en, "nazev_cviceni"),
- "quizHeadKey": keyed(f"{prefix}_quiz{i}_head", doc, en, "nadpis_kvizu"),
- "slides": read_slides(doc, part(doc, "Výklad")),
- "problems": problems,
- })
- return out
-
-
-def skill_lessons(directory: Path, tag: str, year: int) -> list[dict]:
- """English and German years: slides, reading, listening, gaps, writing, quiz."""
- out = []
- for i, path in enumerate(lesson_files(directory), start=1):
- doc, en = parse_doc(path), en_doc(path)
- load_translations(en)
- lid = lesson_id(doc, f"{tag}.{year}.{i}")
- read = part(doc, "Čtení")
- listen = part(doc, "Poslech")
- write = part(doc, "Psaní")
- writing = None
- if write is not None:
- sub = {norm(s.title): s for s in write.items()}
- model = sub.get(norm("Vzorová odpověď"))
- keys = sub.get(norm("Musí obsahovat"))
- words = sub.get(norm("Minimum slov"))
- n = opt_int(text_of(words.nodes) or "") if words else None
- writing = {
- "prompt": text_of(write.head()) or "",
- "model": text_of(model.nodes) if model else "",
- "keys": "|".join(bullets(keys.nodes)) if keys else "",
- "minWords": n if n is not None else 20,
- }
- out.append({
- "id": i,
- "key": lid,
- "titleKey": keyed(f"{tag}_y{year}_u{i}", doc, en, "nazev"),
- "subKey": keyed(f"{tag}_y{year}_u{i}_sub", doc, en, "popis"),
- "slides": read_slides(doc, part(doc, "Výklad")),
- "reading": text_of(read.head()) if read else None,
- "readQuiz": read_quiz(doc, read),
- "listening": text_of(listen.head()) if listen else None,
- "listenQuiz": read_quiz(doc, listen),
- "gaps": read_typed(doc, part(doc, "Doplňování")),
- "writing": writing,
- "quiz": read_quiz(doc, part(doc, "Kvíz")),
- })
- return out
-
-
-MLUV_TYPES = {"doplnovani": "typed", "vyber": "choice", "reseni": "reveal"}
-
-
-def mluvnice(directory: Path) -> list[dict]:
- out = []
- for n, path in enumerate(lesson_files(directory), start=1):
- doc, en = parse_doc(path), en_doc(path)
- load_translations(en)
- lid = lesson_id(doc, f"mluv.{n}")
- kind = MLUV_TYPES.get(norm(need(doc, "typ")))
- if kind is None:
- fail(path, doc.meta_lines.get("typ"), f"typ musí být jeden z: {', '.join(MLUV_TYPES)}")
- kind = "typed"
- item = {"id": n, "key": lid, "name": need(doc, "zkratka"), "type": kind,
- "title": need(doc, "nazev"), "sub": need(doc, "popis"),
- "note": doc.meta.get("poznamka") or None}
- p = part(doc, "Úlohy")
- its = p.items() if p else []
- if kind == "typed":
- rows = []
- for it in its:
- hints, answers = [], []
- for b in bullets(it.nodes):
- h, sep, a = b.partition(" = ")
- if not sep:
- fail(path, it.line, f"mezera {b!r} musí mít tvar „nápověda = odpověď“")
- hints.append(h)
- answers.append(a)
- rows.append({"prompt": it.title, "shown": quote(it.nodes) or "",
- "hints": hints, "answers": answers})
- item["items"] = rows
- elif kind == "reveal":
- item["items"] = [{"prompt": it.title, "solution": answer(it.nodes) or ""} for it in its]
- else:
- qs = read_quiz(doc, p, expl_key=None)
- item["questions"] = qs
- expls = [quote(it.nodes) for it in its]
- if any(e is not None for e in expls):
- item["expls"] = [e or "" for e in expls]
- out.append(item)
- return out
-
-
-BOOK_ICONS = ["book", "globe", "people", "bulb"]
-
-
-def books(directory: Path) -> list[dict]:
- out = []
- for path in lesson_files(directory):
- doc, en = parse_doc(path), en_doc(path)
- load_translations(en)
- bid = need(doc, "id")
- if bid in IDS:
- fail(path, doc.meta_lines.get("id"), f"id {bid!r} už má {rel(IDS[bid])}")
- IDS[bid] = path
- PROGRESS[f"book.{bid}.quiz"] = f"{bid}.kviz"
- PROGRESS[f"book.{bid}.plot"] = f"{bid}.dej"
- LEGACY[f"book.{bid}.quiz"] = f"{bid}.kviz"
- LEGACY[f"book.{bid}.plot"] = f"{bid}.dej"
- author, title = need(doc, "autor"), need(doc, "nazev")
- notes, quiz, plot, plot_meaning = [], [], [], []
- for p in parts(doc):
- t = norm(p.title)
- if t == norm("Kvíz"):
- quiz = read_quiz(doc, p)
- elif t == norm("Děj"):
- for it in p.items():
- plot.append({"prompt": it.title, "words": numbered(it.nodes)})
- m = quote(it.nodes)
- if m is not None:
- plot_meaning.append(m)
- else:
- icon = BOOK_ICONS[len(notes)] if len(notes) < len(BOOK_ICONS) else "book"
- notes.append({"title": p.title, "icon": icon, "lines": bullets(p.nodes)})
-
- def label(field_name: str, default: str) -> str:
- if field_name in doc.meta:
- return keyed(f"book_{slugify(bid)}_{field_name}", doc, en, field_name) or default
- return default
-
- out.append({
- "id": bid,
- "title": f"{author} – {title}",
- "subKey": label("popis", "cetba1984_sub" if quiz else "cetba_entry_sub"),
- "page": doc.meta.get("stranka") or f"cetba_{bid.replace('-', '_')}",
- "genre": need(doc, "zanr"),
- "quizTitle": label("kviz_nadpis", "book_quiz_heading") if quiz else "",
- "quizSub": label("kviz_popis", "lit_quiz_sub") if quiz else "",
- "plotTitle": label("dej_nadpis", "lit_plot_title") if plot else "",
- "plotSub": label("dej_popis", "lit_plot_sub") if plot else "",
- "notes": notes,
- "quiz": quiz,
- "plot": plot,
- "plotMeaning": plot_meaning,
- })
- return out
-
-
-# ---------------------------------------------------------------------------
-# German textbook
-# ---------------------------------------------------------------------------
-
-def gap_rows(doc: Doc, rows: list[list[str]], col: int, where: str) -> list[tuple[str, str, str]]:
- out = []
- for r in rows:
- try:
- segs, ans = ungap(r[col])
- except ContentError as e:
- fail(doc.path, None, f"{where}: {e}")
- segs, ans = [r[col]], [""]
- if len(ans) != 1:
- fail(doc.path, None, f"{where}: věta {r[col]!r} musí mít právě jednu mezeru [ … ]")
- ans = (ans + [""])[:1]
- segs = (segs + ["", ""])[:2]
- out.append((segs[0], ans[0], segs[1]))
- return out
-
-
-def seg_rows(doc: Doc, text: str, pad: int, where: str) -> tuple[list[str], list[str]]:
- try:
- segs, ans = ungap(text)
- except ContentError as e:
- fail(doc.path, None, f"{where}: {e}")
- return [text], []
- while len(segs) < pad:
- segs.append("")
- return segs, ans
-
-
-def blist(doc: Doc, name: str) -> list[str]:
- p = part(doc, name)
- return bullets(p.nodes) if p else []
-
-
-def rows_of(doc: Doc, name: str, cols: int) -> list[list[str]]:
- p = part(doc, name)
- return table(doc, p.nodes, cols, name) if p else []
-
-
-def nullable(s: str) -> str | None:
- return unesc(s) if s != "" else None
-
-
-def german_exercise(doc: Doc, kind: str) -> dict:
- ex: dict = {"type": kind}
- pool = lambda: blist(doc, "Nabídka")
- meanings = lambda: blist(doc, "Překlad")
- if kind == "dialog":
- dialogues = []
- p = part(doc, "Rozhovory")
- for it in (p.items() if p else []):
- rows = []
- for r in table(doc, it.nodes, 2, it.title):
- (b, a, af), = gap_rows(doc, [r], 1, it.title)
- rows.append({"speaker": unesc(r[0]), "before": b, "answer": a, "after": af})
- dialogues.append({"name": it.title, "rows": rows})
- ex.update(pool=pool(), rows=[], meanings=meanings(), dialogues=dialogues)
- elif kind == "assembly":
- items = [{"prompt": None, "words": b.split(" / ")} for b in blist(doc, "Věty")]
- items += [{"prompt": nullable(r[0]), "words": unesc(r[1]).split(" / ")}
- for r in rows_of(doc, "Úlohy", 2)]
- ex.update(items=items, meanings=meanings())
- elif kind == "choice":
- ex.update(questions=read_quiz(doc, part(doc, "Otázky"), expl_key=None), meanings=meanings())
- elif kind == "free":
- ex["questions"] = [{"question": unesc(r[0]), "qCs": unesc(r[1]), "sample": unesc(r[2]),
- "aCs": unesc(r[3])} for r in rows_of(doc, "Otázky", 4)]
- elif kind == "number":
- ex.update(pool=pool(), rows=[{"digits": unesc(r[0]), "answer": unesc(r[1])}
- for r in rows_of(doc, "Úlohy", 2)], meanings=meanings())
- elif kind == "count":
- ex.update(pool=pool(), rows=[{"emoji": unesc(r[0]), "count": opt_int(r[1]) or 0,
- "noun": unesc(r[2]), "answer": unesc(r[3])}
- for r in rows_of(doc, "Úlohy", 4)], meanings=meanings())
- elif kind in ("seq", "verb"):
- rows = [{"before": b, "after": af, "answer": a}
- for b, a, af in gap_rows(doc, rows_of(doc, "Úlohy", 1), 0, "Úlohy")]
- ex.update(pool=pool(), rows=rows, meanings=meanings())
- if part(doc, "Glosář") is not None:
- ex["gloss"] = blist(doc, "Glosář")
- elif kind == "verb_sections":
- sections = []
- p = part(doc, "Oddíly")
- for it in (p.items() if p else []):
- rows = [{"before": b, "after": af, "answer": a}
- for b, a, af in gap_rows(doc, table(doc, it.nodes, 1, it.title), 0, it.title)]
- sections.append({"title": it.title, "rows": rows, "meanings": bullets(it.nodes)})
- ex.update(pool=pool(), rows=[], meanings=[], sections=sections)
- elif kind == "assign":
- groups = blist(doc, "Skupiny")
- items = []
- for r in rows_of(doc, "Slova", 3):
- g = unesc(r[2])
- if g not in groups:
- fail(doc.path, None, f"skupina {g!r} není v seznamu # Skupiny")
- items.append({"emoji": nullable(r[0]), "label": unesc(r[1]),
- "group": groups.index(g) if g in groups else 0})
- ex.update(items=items, groups=groups, meanings=meanings())
- elif kind == "verbclue":
- rs = rows_of(doc, "Úlohy", 2)
- rows = [{"emoji": unesc(r[0]), "before": b, "after": af, "answer": a}
- for r, (b, a, af) in zip(rs, gap_rows(doc, rs, 1, "Úlohy"))]
- ex.update(pool=pool(), rows=rows, meanings=meanings())
- elif kind == "typed":
- ex["rows"] = [{"prompt": unesc(r[0]), "answers": unesc(r[1]), "meaning": nullable(r[2])}
- for r in rows_of(doc, "Úlohy", 3)]
- p = part(doc, "Nabídka")
- if p is not None:
- ex["bank"] = text_of(p.nodes)
- elif kind == "profile":
- ex["questions"] = [{"stem": unesc(r[0]), "sample": unesc(r[1])}
- for r in rows_of(doc, "Otázky", 2)]
- elif kind == "hangman":
- rs = rows_of(doc, "Slova", 2)
- letters = text_of(part(doc, "Písmena").nodes) if part(doc, "Písmena") else ""
- ex.update(words=[unesc(r[0]) for r in rs], tips=[unesc(r[1]) for r in rs],
- letters=(letters or "").split())
- elif kind == "ordne":
- ex.update(pool=[], rows=[{"mid": unesc(r[1]), "fw": unesc(r[0]), "ans": unesc(r[2])}
- for r in rows_of(doc, "Úlohy", 3)],
- meanings=meanings(), fwPool=blist(doc, "Tázací slova"),
- ansPool=blist(doc, "Odpovědi"))
- elif kind in ("fill", "letters"):
- pad = 4 if kind == "fill" else 5
- rows = []
- for r in rows_of(doc, "Úlohy", 3):
- segs, ans = seg_rows(doc, r[1], pad, "Úlohy")
- rows.append({"num": nullable(r[0]), "segs": segs, "answers": ans, "mean": nullable(r[2])})
- ex["rows"] = rows
- if kind == "fill":
- ex.update(pool=pool(), meanings=meanings())
- elif kind == "ex2":
- items = []
- p = part(doc, "Úlohy")
- for it in (p.items() if p else []):
- rows = []
- for r in table(doc, it.nodes, 2, it.title):
- segs, ans = seg_rows(doc, r[1], 0, it.title)
- rows.append({"num": nullable(r[0]), "segs": segs, "answers": ans})
- items.append({"czech": it.title, "rows": rows})
- ex.update(pool=pool(), items=items)
- elif kind == "letters_gap":
- rows = []
- p = part(doc, "Věty")
- for it in (p.items() if p else []):
- segs, ans = seg_rows(doc, it.title, 0, "Věty")
- words, gmean = [], []
- for b in bullets(it.nodes):
- w, _, m = b.partition(" = ")
- words.append(w)
- gmean.append(m)
- rows.append({"segs": segs, "answers": ans, "czech": quote(it.nodes),
- "words": words, "gmean": gmean})
- ex["rows"] = rows
- elif kind == "table":
- p = part(doc, "Tabulka")
- t = [nd for nd in (p.nodes if p else []) if nd.kind == "table"]
- rows = t[0].rows if t else [[""]]
- ex.update(persons=[unesc(c) for c in rows[0][1:]],
- nouns=[unesc(r[0]) for r in rows[1:]],
- answers=[[unesc(c) for c in r[1:]] for r in rows[1:]])
- elif kind == "kw":
- ex["rows"] = [{"prompt": unesc(r[0]), "answers": unesc(r[1]), "mean": nullable(r[2]),
- "german": nullable(r[3])} for r in rows_of(doc, "Úlohy", 4)]
- elif kind == "vocab":
- sections = []
- p = part(doc, "Slovíčka")
- for it in (p.items() if p else []):
- sections.append({"header": it.title, "rows": [
- {"prompt": unesc(r[0]), "answers": unesc(r[1]), "meaning": nullable(r[2])}
- for r in table(doc, it.nodes, 3, it.title)]})
- ex["sections"] = sections
- else:
- fail(doc.path, doc.meta_lines.get("typ"), f"neznámý typ cvičení {kind!r}")
- for fm, js in (("ukazka", "sample"),):
- if fm in doc.meta:
- ex[js] = doc.meta[fm]
- for fm, js in (("vyznam_predem", "preMeaning"), ("preklad_predem", "transUpfront"),
- ("odhalit_nemcinu", "revealGerman"), ("ukazat_nemcinu", "showGerman"),
- ("poznamka_k_odpovedim", "answersNote")):
- v = flag(doc, fm)
- if v is not None:
- ex[js] = v
- return ex
-
-
-def german_textbook(directory: Path) -> list[dict]:
- units = []
- unit_dirs = sorted((p for p in directory.iterdir() if p.is_dir()), key=by_order) \
- if directory.is_dir() else []
- for u, udir in enumerate(unit_dirs):
- info_path = udir / "_lekce.md"
- if not info_path.exists():
- fail(udir, None, "chybí _lekce.md")
- continue
- info, info_en = parse_doc(info_path), en_doc(info_path)
- load_translations(info_en)
- n = u + 1
- title = need(info, "nazev")
- if flag(info, "zamceno"):
- units.append({"id": u, "title": title, "page": None, "tag": None, "sub": None,
- "unlocked": False, "names": [], "exercises": {}})
- continue
- exercises: dict[str, dict] = {}
- names: list[str] = []
- vocab = None
- files = sorted((p for p in udir.glob("*.md")
- if not p.name.endswith(".en.md") and not p.name.startswith("_")), key=by_order)
- k = 0
- for path in files:
- doc, en = parse_doc(path), en_doc(path)
- load_translations(en)
- kind = need(doc, "typ")
- if kind == "vocab":
- lesson_id(doc, f"g.{u}.vocab")
- vocab = german_exercise(doc, kind)
- vocab = {"type": "vocab", "title": need(doc, "nazev"),
- "sub": keyed(f"u{n}vocab_sub", doc, en, "popis"),
- "sections": vocab["sections"]}
- continue
- if not re.match(r"^\d+-", path.name):
- fail(path, None, "název souboru musí začínat pořadím, např. 03-nazev.md")
- k += 1
- lesson_id(doc, f"g.{u}.{k}")
- ex = german_exercise(doc, kind)
- ex["title"] = need(doc, "nazev")
- ex["sub"] = keyed(f"u{n}e{k}_sub", doc, en, "popis")
- if "tip" in doc.meta:
- ex["tip"] = keyed(f"u{n}e{k}_tip", doc, en, "tip")
- exercises[str(k)] = ex
- names.append(ex["title"])
- unit = {"id": u, "title": title, "page": f"unit{n}", "tag": f"u{n}",
- "sub": keyed(f"unit{n}_sub", info, info_en, "popis"), "unlocked": True,
- "names": names, "branch": f"u{n}vocab", "exercises": exercises}
- if vocab is not None:
- unit["vocab"] = vocab
- units.append(unit)
- return units
-
-
-# ---------------------------------------------------------------------------
-# App-level files
-# ---------------------------------------------------------------------------
-
-def ui_strings(path: Path) -> None:
- doc = parse_doc(path)
- for t in (nd for nd in doc.nodes if nd.kind == "table"):
- for r in table(doc, [t], 3, "Texty"):
- key = unesc(r[0])
- if not key:
- continue
- if key in I18N.map:
- warn(path, None, f"klíč {key!r} je v textech dvakrát")
- I18N.put(key, nullable(r[1]), nullable(r[2]), path)
-
-
-def subjects(path: Path) -> list[dict]:
- doc = parse_doc(path)
- out = []
- p = part(doc, "Předměty")
- for r in table(doc, p.nodes if p else [], 6, "Předměty"):
- key = unesc(r[0])
- if r[1] or r[2]:
- I18N.put(key, nullable(r[1]), nullable(r[2]), path)
- out.append({"key": key, "open": flag_cell(r[5]), "target": nullable(r[4]),
- "icon": unesc(r[3])})
- return out
-
-
-def flag_cell(s: str) -> bool:
- return norm(s) in ("ano", "true", "yes", "1")
-
-
-def parse_changelog(path: Path) -> list[dict]:
- if not path.is_file():
- return []
- entries: list[dict] = []
- current = None
- for raw in path.read_text(encoding="utf-8").splitlines():
- line = raw.strip()
- if not line or line.startswith("#"):
- continue
- if line.startswith("---"):
- current = {"date": line[3:].strip(), "cs": [], "en": []}
- entries.append(current)
- continue
- if current is None:
- continue
- if line.startswith("cs:"):
- current["cs"].append(line[3:].strip())
- elif line.startswith("en:"):
- current["en"].append(line[3:].strip())
- return [e for e in entries if e["cs"] or e["en"]]
-
-
-# ---------------------------------------------------------------------------
-# Main
-# ---------------------------------------------------------------------------
-
-STD = [
- ("pocitacove-site/2-rocnik", "net2"), ("pocitacove-site/3-rocnik", "net3"),
- ("pocitacove-site/4-rocnik", "net4"),
- ("technicke-vybaveni/1-rocnik", "hw"), ("technicke-vybaveni/2-rocnik", "hw2"),
- ("technicke-vybaveni/3-rocnik", "hw3"), ("technicke-vybaveni/4-rocnik", "hw4"),
- ("obcanska-nauka/1-rocnik", "on"), ("obcanska-nauka/2-rocnik", "on2"),
- ("obcanska-nauka/3-rocnik", "on3"), ("obcanska-nauka/4-rocnik", "on4"),
- ("cestina/literatura/1-rocnik", "lit"), ("cestina/literatura/2-rocnik", "lit2"),
- ("cestina/literatura/3-rocnik", "lit3"), ("cestina/literatura/4-rocnik", "lit4"),
- ("prirodni-vedy/chemie", "chem"), ("prirodni-vedy/biologie", "bio"),
- ("fyzika/1-rocnik", "fyz"), ("fyzika/2-rocnik", "fyz2"),
- ("fyzika/3-rocnik", "fyz3"), ("fyzika/4-rocnik", "fyz4"),
- ("technicka-grafika/1-rocnik", "tg"), ("technicka-grafika/2-rocnik", "tg2"),
- ("technicka-grafika/3-rocnik", "tg3"), ("technicka-grafika/4-rocnik", "tg4"),
- ("prezentacni-grafika/1-rocnik", "pg"), ("prezentacni-grafika/2-rocnik", "pg2"),
- ("prezentacni-grafika/3-rocnik", "pg3"), ("prezentacni-grafika/4-rocnik", "pg4"),
- ("programove-vybaveni/1-rocnik", "sw"), ("programove-vybaveni/2-rocnik", "sw2"),
- ("programove-vybaveni/3-rocnik", "sw3"), ("programove-vybaveni/4-rocnik", "sw4"),
-]
-CODE = [
- ("programovani/1-rocnik", "cs"), ("programovani/2-rocnik", "cs2"),
- ("programovani/3-rocnik", "cs3"), ("programovani/4-rocnik", "cs4"),
-]
-MATH = [
- ("matematika/0-opakovani", "mat0"), ("matematika/1-rocnik", "mat"),
- ("matematika/2-rocnik", "mat2"), ("matematika/3-rocnik", "mat3"),
- ("matematika/4-rocnik", "mat4"),
-]
-SKILLS = [("anglictina", "en"), ("nemcina", "de")]
-
-
-def build(content: Path = CONTENT) -> dict:
- ui_strings(content / "aplikace" / "texty.md")
- payload: dict = {"i18n": None}
- payload["subjects"] = subjects(content / "aplikace" / "predmety.md")
- payload["german"] = german_textbook(content / "nemcina" / "ucebnice")
- net = std_lessons(content / "pocitacove-site" / "1-rocnik", "net")
- payload["net"] = {
- "lessons": net,
- "tasks": [t for t, _ in NET_TASKS],
- "answers": [a for _, a in NET_TASKS],
- }
- for d, key in STD:
- payload[key] = std_lessons(content / d, key)
- for d, key in MATH:
- payload[key] = math_lessons(content / d, key)
- for d, key in CODE:
- payload[key] = code_lessons(content / d, key)
- for d, tag in SKILLS:
- for year in range(1, 5):
- payload[tag if year == 1 else f"{tag}{year}"] = skill_lessons(
- content / d / f"{year}-rocnik", tag, year)
- payload["mluvnice"] = mluvnice(content / "cestina" / "mluvnice")
- payload["books"] = books(content / "cestina" / "cetba")
- payload["changelog"] = parse_changelog(content / "aplikace" / "novinky.txt")
- payload["progress"] = {"keys": PROGRESS, "legacy": LEGACY}
- payload["i18n"] = I18N.map
- check(payload)
- return payload
-
-
-def check(payload: dict) -> None:
- for sec, lessons in payload.items():
- if not isinstance(lessons, list):
- continue
- for L in lessons:
- if not isinstance(L, dict):
- continue
- for q in L.get("quiz") or []:
- if isinstance(q, dict) and q.get("options") and not 0 <= q["correct"] < len(q["options"]):
- ERRORS.append(f"{sec} {L.get('id')}: správná odpověď mimo možnosti")
-
-
-def main() -> int:
- outs = OUTS
- if "--out" in sys.argv:
- outs = [Path(a) for a in sys.argv[sys.argv.index("--out") + 1:] if a]
- if not outs:
- sys.exit("usage: build-content.py [--out PATH ...]")
- payload = build()
- for w in WARNINGS:
- print(f"upozornění: {w}", file=sys.stderr)
- if ERRORS:
- for e in ERRORS:
- print(f"chyba: {e}", file=sys.stderr)
- print(f"{len(ERRORS)} chyb, content.json nevznikl", file=sys.stderr)
- return 1
- text = json.dumps(payload, ensure_ascii=False, indent=1)
- for dest in outs:
- dest.parent.mkdir(parents=True, exist_ok=True)
- if not dest.exists() or dest.read_text(encoding="utf-8") != text:
- dest.write_text(text, encoding="utf-8")
- print(f"content.json: {len(IDS)} lekcí, {len(payload['i18n'])} textů → "
- + ", ".join(rel(d) for d in outs))
- return 0
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/scripts/bundle-ios.sh b/scripts/bundle-ios.sh
deleted file mode 100755
index c9b9033..0000000
--- a/scripts/bundle-ios.sh
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/bin/sh
-# Build an unsigned iOS IPA for sideloading (Xcode / AltStore / Sideloadly).
-
-set -eu
-
-root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
-cd "$root"
-
-# The Xcode target builds content.json from content/ itself.
-export DEVELOPER_DIR="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}"
-COMMIT="${COMMIT:-$(git -C "$root" rev-parse HEAD 2>/dev/null || echo dev)}"
-dd="$root/ios/DerivedData"
-
-rm -rf "$root/ios/Payload"
-mkdir -p "$dd" "$root/dist-ios"
-
-xcodebuild \
- -project ios/Graduately.xcodeproj \
- -scheme Graduately \
- -configuration Release \
- -sdk iphoneos \
- -destination 'generic/platform=iOS' \
- -derivedDataPath "$dd" \
- CODE_SIGNING_ALLOWED=NO \
- CODE_SIGNING_REQUIRED=NO \
- COMMIT="$COMMIT"
-
-app=$(find "$dd" -name 'Graduately.app' -path '*/Release-iphoneos/*' | head -n 1)
-if [ -z "$app" ]; then
- echo "Graduately.app not found" >&2
- exit 1
-fi
-
-mkdir -p "$root/ios/Payload"
-cp -R "$app" "$root/ios/Payload/Graduately.app"
-(cd "$root/ios" && zip -r -q "$root/dist-ios/graduately-ios.ipa" Payload)
-echo "wrote $root/dist-ios/graduately-ios.ipa"
diff --git a/scripts/bundle-linux.sh b/scripts/bundle-linux.sh
deleted file mode 100755
index 795ec12..0000000
--- a/scripts/bundle-linux.sh
+++ /dev/null
@@ -1,62 +0,0 @@
-#!/usr/bin/env bash
-# Linux AppImage (x86_64) of the Compose desktop app, with its own Java runtime.
-# Needs python3, curl and a JDK 17+ that ships jmods (set PACKAGE_JDK, e.g.
-# Temurin 21). The asset name and the icon path inside the image are what the
-# installer and the updater of older builds expect; keep them.
-set -euo pipefail
-
-ROOT="$(cd "$(dirname "$0")/.." && pwd)"
-cd "$ROOT"
-
-ARCH="$(uname -m)"
-case "$ARCH" in
- x86_64|amd64) ARCH=x86_64 ;;
- *)
- echo "Only x86_64 AppImages are built, this machine is $ARCH" >&2
- exit 1
- ;;
-esac
-
-OUT="$ROOT/dist/graduately-linux-${ARCH}.AppImage"
-APPDIR="$ROOT/dist/AppDir"
-TOOLS="$ROOT/dist/tools"
-ICON="$ROOT/data/share/icons/hicolor/512x512/apps/maturita.png"
-IMAGE="$ROOT/android/desktop/build/compose/binaries/main-release/app/graduately"
-
-echo "==> Building the desktop app"
-(cd android && ./gradlew :desktop:createReleaseDistributable --no-daemon)
-
-echo "==> Preparing AppDir"
-rm -rf "$APPDIR"
-mkdir -p "$APPDIR/usr/share/icons/hicolor/512x512/apps" "$APPDIR/usr/share/applications"
-cp -R "$IMAGE" "$APPDIR/graduately"
-cp -f "$ICON" "$APPDIR/maturita.png"
-cp -f "$ICON" "$APPDIR/.DirIcon"
-cp -f "$ICON" "$APPDIR/usr/share/icons/hicolor/512x512/apps/maturita.png"
-
-cat > "$APPDIR/AppRun" <<'EOF'
-#!/bin/sh
-HERE="$(dirname "$(readlink -f "$0")")"
-exec "$HERE/graduately/bin/graduately" "$@"
-EOF
-chmod +x "$APPDIR/AppRun"
-
-DESKTOP="$ROOT/data/share/applications/org.maturita.Maturita.desktop"
-cp -f "$DESKTOP" "$APPDIR/org.maturita.Maturita.desktop"
-cp -f "$DESKTOP" "$APPDIR/usr/share/applications/org.maturita.Maturita.desktop"
-
-TOOL="$TOOLS/appimagetool-${ARCH}.AppImage"
-if [[ ! -x "$TOOL" ]]; then
- echo "==> Downloading appimagetool"
- mkdir -p "$TOOLS"
- curl -fsSL -o "$TOOL" \
- "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${ARCH}.AppImage"
- chmod +x "$TOOL"
-fi
-
-echo "==> Building AppImage"
-rm -f "$OUT"
-# CI runners have no FUSE; extract-and-run avoids needing it.
-APPIMAGE_EXTRACT_AND_RUN=1 ARCH="$ARCH" "$TOOL" --no-appstream "$APPDIR" "$OUT"
-chmod +x "$OUT"
-echo "Created ${OUT#"$ROOT"/}"
diff --git a/scripts/bundle-macos-swift.sh b/scripts/bundle-macos-swift.sh
deleted file mode 100755
index 9a8a27c..0000000
--- a/scripts/bundle-macos-swift.sh
+++ /dev/null
@@ -1,128 +0,0 @@
-#!/bin/sh
-# Build the native Swift macOS .app and zip it for the builds branch.
-
-set -eu
-
-root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
-cd "$root"
-
-export DEVELOPER_DIR="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}"
-if [ -z "${COMMIT:-}" ]; then
- if [ ! -e "$root/.git" ] ||
- ! COMMIT="$(git -C "$root" rev-parse --verify HEAD 2>/dev/null)"; then
- echo "error: cannot determine the source commit; set COMMIT when bundling without Git metadata" >&2
- exit 1
- fi
-fi
-export COMMIT
-
-arch=$(uname -m)
-case "$arch" in
- arm64|aarch64) arch_tag=arm64 ;;
- x86_64) arch_tag=x86_64 ;;
- *)
- echo "Unsupported architecture: $arch" >&2
- exit 1
- ;;
-esac
-
-dd="$root/macos/DerivedData"
-out_zip="graduately-macos-${arch_tag}.zip"
-mkdir -p "$dd" "$root/dist"
-
-xcodebuild \
- -project macos/Graduately.xcodeproj \
- -scheme Graduately \
- -configuration Release \
- -destination "generic/platform=macOS" \
- -derivedDataPath "$dd" \
- CODE_SIGNING_ALLOWED=NO \
- CODE_SIGNING_REQUIRED=NO \
- COMMIT="$COMMIT"
-
-app=$(find "$dd" -name 'Graduately.app' -path '*/Release/*' | head -n 1)
-if [ -z "$app" ]; then
- echo "Graduately.app not found" >&2
- exit 1
-fi
-
-dest="$root/dist/Graduately.app"
-rm -rf "$dest"
-cp -R "$app" "$dest"
-
-ENTITLEMENTS="$root/macos/Graduately/Graduately.entitlements"
-
-pick_sign_identity() {
- if [ -n "${MACOS_SIGN_IDENTITY:-}" ]; then
- printf '%s\n' "$MACOS_SIGN_IDENTITY"
- return
- fi
- found="$(security find-identity -v -p codesigning 2>/dev/null \
- | awk -F'"' '/Developer ID Application/ { print $2; exit }')"
- if [ -n "$found" ]; then
- printf '%s\n' "$found"
- return
- fi
- printf '%s\n' "-"
-}
-
-IDENTITY="$(pick_sign_identity)"
-echo "==> Signing Graduately.app ($IDENTITY)"
-sign_args="--force --sign $IDENTITY"
-if [ "$IDENTITY" != "-" ]; then
- sign_args="$sign_args --options runtime --timestamp"
-fi
-if [ -f "$ENTITLEMENTS" ]; then
- # shellcheck disable=SC2086
- codesign $sign_args --entitlements "$ENTITLEMENTS" "$dest"
-else
- # shellcheck disable=SC2086
- codesign $sign_args "$dest"
-fi
-codesign --verify --strict --verbose=2 "$dest"
-
-can_notarize() {
- [ "$IDENTITY" != "-" ] || return 1
- if [ -n "${APPLE_API_KEY:-}" ] && [ -n "${APPLE_API_KEY_ID:-}" ] &&
- [ -n "${APPLE_API_ISSUER:-}" ]; then
- return 0
- fi
- if [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" ] &&
- [ -n "${APPLE_TEAM_ID:-}" ]; then
- return 0
- fi
- return 1
-}
-
-if can_notarize; then
- echo "==> Notarizing Graduately.app"
- zip="$root/dist/.notarize-$out_zip"
- rm -f "$zip"
- ditto -c -k --keepParent "$dest" "$zip"
- if [ -n "${APPLE_API_KEY:-}" ]; then
- keyfile="$(mktemp)"
- printf '%s\n' "$APPLE_API_KEY" > "$keyfile"
- xcrun notarytool submit "$zip" --wait \
- --key "$keyfile" --key-id "$APPLE_API_KEY_ID" --issuer "$APPLE_API_ISSUER"
- rm -f "$keyfile"
- else
- xcrun notarytool submit "$zip" --wait \
- --apple-id "$APPLE_ID" \
- --password "$APPLE_APP_SPECIFIC_PASSWORD" \
- --team-id "$APPLE_TEAM_ID"
- fi
- rm -f "$zip"
- xcrun stapler staple "$dest"
-else
- if [ "$IDENTITY" != "-" ] && [ -n "${GITHUB_ACTIONS:-}${CI:-}" ]; then
- echo "error: Developer ID is present but notarization secrets are missing." >&2
- exit 1
- fi
-fi
-
-rm -f "$root/dist/$out_zip"
-(
- cd "$root/dist"
- zip -qry "$out_zip" Graduately.app
-)
-echo "wrote $root/dist/$out_zip"
diff --git a/scripts/bundle-windows.sh b/scripts/bundle-windows.sh
deleted file mode 100755
index 8ca8650..0000000
--- a/scripts/bundle-windows.sh
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/usr/bin/env bash
-# Windows x64 zip of the Compose desktop app, with its own Java runtime.
-# Runs in Git Bash or MSYS2. Needs python3 and a JDK 17+ that ships jmods
-# (set PACKAGE_JDK, e.g. Temurin 21).
-#
-# graduately.exe must sit at the root of the zip: the installer and the
-# updater of older builds unpack it over the install folder and start it.
-set -euo pipefail
-
-ROOT="$(cd "$(dirname "$0")/.." && pwd)"
-cd "$ROOT"
-
-OUT="$ROOT/dist/graduately-windows-x64.zip"
-IMAGE="$ROOT/android/desktop/build/compose/binaries/main-release/app/graduately"
-
-echo "==> Building the desktop app"
-(cd android && ./gradlew :desktop:createReleaseDistributable --no-daemon)
-
-[[ -f "$IMAGE/graduately.exe" ]] || { echo "graduately.exe missing in $IMAGE" >&2; exit 1; }
-
-echo "==> Zipping"
-mkdir -p "$ROOT/dist"
-rm -f "$OUT"
-if command -v 7z >/dev/null 2>&1; then
- (cd "$IMAGE" && 7z a -tzip -mx=9 "$OUT" . >/dev/null)
-else
- powershell -NoProfile -Command "Compress-Archive -Path '$(cygpath -w "$IMAGE")\\*' -DestinationPath '$(cygpath -w "$OUT")'"
-fi
-echo "Created ${OUT#"$ROOT"/}"
diff --git a/scripts/gen-icons.py b/scripts/gen-icons.py
deleted file mode 100755
index d9fcb7d..0000000
--- a/scripts/gen-icons.py
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/env python3
-"""Regenerate assets/app-icon.ico and data/share/icons from assets/app-icon.png.
-
-Uses macOS `sips` when available; otherwise requires Pillow.
-Run from the repository root.
-"""
-from __future__ import annotations
-
-import struct
-import subprocess
-import sys
-import tempfile
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parent.parent
-SRC = ROOT / "assets" / "app-icon.png"
-ICO = ROOT / "assets" / "app-icon.ico"
-ICO_SIZES = [16, 32, 48, 64, 128, 256]
-SHARE_SIZES = [16, 32, 48, 64, 128, 256, 512]
-
-
-def resize_png(src: Path, dest: Path, size: int) -> None:
- dest.parent.mkdir(parents=True, exist_ok=True)
- if subprocess.call(["which", "sips"], stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL) == 0:
- subprocess.check_call(
- ["sips", "-z", str(size), str(size), str(src), "--out", str(dest)],
- stdout=subprocess.DEVNULL,
- )
- return
- try:
- from PIL import Image
- except ImportError as exc:
- raise SystemExit(
- "Need macOS sips or Pillow to resize icons"
- ) from exc
- img = Image.open(src).convert("RGBA")
- img = img.resize((size, size), Image.Resampling.LANCZOS)
- img.save(dest, format="PNG")
-
-
-def write_ico(entries: list[tuple[int, bytes]], dest: Path) -> None:
- count = len(entries)
- header = struct.pack("= 256 else size
- h = 0 if size >= 256 else size
- direntries.append(
- struct.pack(" int:
- if not SRC.is_file():
- print(f"missing {SRC}", file=sys.stderr)
- return 1
-
- with tempfile.TemporaryDirectory(prefix="graduately-ico-") as tmp:
- entries: list[tuple[int, bytes]] = []
- for size in ICO_SIZES:
- png = Path(tmp) / f"icon_{size}.png"
- resize_png(SRC, png, size)
- entries.append((size, png.read_bytes()))
- write_ico(entries, ICO)
- print(f"wrote {ICO.relative_to(ROOT)}")
-
- for size in SHARE_SIZES:
- dest = ROOT / "data" / "share" / "icons" / "hicolor" / f"{size}x{size}" / "apps" / "maturita.png"
- resize_png(SRC, dest, size)
- print(f"wrote {dest.relative_to(ROOT)}")
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main())
diff --git a/scripts/smoke-platforms.sh b/scripts/smoke-platforms.sh
deleted file mode 100755
index 483de1e..0000000
--- a/scripts/smoke-platforms.sh
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/usr/bin/env bash
-# Local smoke checks for installers, content packs, and published builds.
-# Does not replace CI; catches packaging / content regressions early.
-set -euo pipefail
-
-ROOT="$(cd "$(dirname "$0")/.." && pwd)"
-cd "$ROOT"
-REPO="${REPO:-pepaskopec-blip/Graduately}"
-fail=0
-
-say() { printf '==> %s\n' "$*"; }
-ok() { printf ' ok: %s\n' "$*"; }
-bad() { printf ' FAIL: %s\n' "$*" >&2; fail=1; }
-
-say "Rebuild installer zips"
-chmod +x installers/pack-installer-zips.sh
-./installers/pack-installer-zips.sh
-ok "installer zips refreshed"
-
-say "Installer zip contents"
-expect_zip() {
- local zip=$1 want=$2
- if unzip -Z1 "$zip" | grep -qxF "$want"; then
- ok "$zip → $want"
- else
- bad "$zip missing $want (has: $(unzip -Z1 "$zip" | tr '\n' ' '))"
- fi
-}
-expect_zip installers/graduately-installer-macos.zip "Nainstalovat Graduately.app/Contents/MacOS/installer"
-expect_zip installers/graduately-installer-linux.zip "graduately-installer-linux.sh"
-expect_zip installers/graduately-installer-windows.zip "graduately-installer-windows.cmd"
-expect_zip installers/graduately-installer-android.zip "Stahnout Graduately.html"
-expect_zip installers/graduately-installer-ios.zip "Jak nainstalovat Graduately.html"
-
-say "Build content.json"
-tmp=$(mktemp)
-python3 scripts/build-content.py --out "$tmp" >/dev/null
-python3 - "$tmp" <<'PY'
-import json, sys
-path = sys.argv[1]
-d = json.load(open(path, encoding="utf-8"))
-books = d.get("books") or []
-assert len(books) == 82, f"expected 82 books, got {len(books)}"
-for b in books:
- bid = b.get("id")
- assert b.get("quiz"), f"{bid}: empty quiz"
- assert b.get("plot"), f"{bid}: empty plot"
- assert b.get("notes"), f"{bid}: empty notes"
- assert b.get("quizTitle"), f"{bid}: missing quizTitle"
- assert b.get("plotTitle"), f"{bid}: missing plotTitle"
-assert d.get("changelog"), "changelog missing from content.json"
-print(f"books={len(books)} changelog={len(d['changelog'])}")
-PY
-ok "content packs"
-rm -f "$tmp"
-
-say "Answer checks"
-if (cd android && ./gradlew -q :desktop:test); then
- ok "Kotlin"
-else
- bad "Kotlin answer checks"
-fi
-if command -v xcrun >/dev/null 2>&1; then
- if ./scripts/test-answers-swift.sh >/dev/null; then
- ok "Swift"
- else
- bad "Swift answer checks"
- fi
-fi
-
-say "Published builds tip"
-sha=$(curl -fsSL --max-time 20 \
- "https://github.com/$REPO/commits/builds.atom" |
- sed -n 's/.*Commit\/\([0-9a-f]\{40\}\).*/\1/p' |
- head -n 1)
-if [[ ${#sha} -eq 40 ]]; then
- ok "builds tip ${sha:0:7}"
- for asset in \
- graduately-macos-arm64.zip \
- graduately-windows-x64.zip \
- graduately-linux-x86_64.AppImage \
- graduately-android.apk \
- graduately-ios.ipa \
- VERSION
- do
- code=$(curl -sI -o /dev/null -w '%{http_code}' --max-time 30 \
- "https://raw.githubusercontent.com/$REPO/$sha/$asset")
- if [[ "$code" == "200" ]]; then
- ok "$asset ($code)"
- continue
- fi
- legacy="${asset/graduately/maturita}"
- if [[ "$legacy" != "$asset" ]]; then
- code2=$(curl -sI -o /dev/null -w '%{http_code}' --max-time 30 \
- "https://raw.githubusercontent.com/$REPO/$sha/$legacy")
- if [[ "$code2" == "200" ]]; then
- ok "$asset not yet; legacy $legacy still published ($code2)"
- continue
- fi
- fi
- bad "$asset HTTP $code"
- done
-else
- bad "could not resolve builds tip SHA"
-fi
-
-if [[ "$fail" -ne 0 ]]; then
- echo "smoke failed" >&2
- exit 1
-fi
-echo "smoke passed"
diff --git a/scripts/test-answers-swift.sh b/scripts/test-answers-swift.sh
deleted file mode 100755
index 7f776f5..0000000
--- a/scripts/test-answers-swift.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/usr/bin/env bash
-# Shared answer-check cases (tests/answers.json) against the Swift apps' code.
-set -euo pipefail
-
-ROOT="$(cd "$(dirname "$0")/.." && pwd)"
-OUT="$ROOT/build/tests"
-mkdir -p "$OUT"
-
-python3 "$ROOT/scripts/build-content.py" --out "$OUT/content.json"
-xcrun swiftc -O -o "$OUT/swift-answers" \
- "$ROOT/ios/Graduately/Data/Answers.swift" "$ROOT/tests/swift/main.swift"
-"$OUT/swift-answers" "$ROOT/tests/answers.json" "$OUT/content.json"
diff --git a/scripts/test-macos-bundle-commit.sh b/scripts/test-macos-bundle-commit.sh
deleted file mode 100755
index d6ea6bb..0000000
--- a/scripts/test-macos-bundle-commit.sh
+++ /dev/null
@@ -1,109 +0,0 @@
-#!/usr/bin/env bash
-# Exercise build identity at the real bundler entry point without Xcode.
-set -euo pipefail
-
-ROOT="$(cd "$(dirname "$0")/.." && pwd)"
-TMP="$(mktemp -d "${TMPDIR:-/tmp}/graduately-bundle-commit.XXXXXX")"
-trap 'rm -rf "$TMP"' EXIT
-
-fail() {
- echo "FAIL: $*" >&2
- exit 1
-}
-
-mkdir -p "$TMP/bin"
-cat > "$TMP/bin/xcodebuild" <<'EOF'
-#!/usr/bin/env bash
-printf '%s\n' "${COMMIT-UNSET}" >> "$MATURITA_TEST_MAKE_LOG"
-# Stop before packaging, after observing the real build's environment.
-exit 97
-EOF
-cat > "$TMP/bin/uname" <<'EOF'
-#!/bin/sh
-echo arm64
-EOF
-chmod +x "$TMP/bin/xcodebuild" "$TMP/bin/uname"
-
-new_fixture() {
- mkdir -p "$1/scripts"
- cp "$ROOT/scripts/bundle-macos.sh" "$1/scripts/bundle-macos.sh"
- cp "$ROOT/scripts/bundle-macos-swift.sh" "$1/scripts/bundle-macos-swift.sh"
- chmod +x "$1/scripts/bundle-macos.sh" "$1/scripts/bundle-macos-swift.sh"
-}
-
-run_bundle() {
- local fixture="$1" mode="$2" value="${3-}"
- MAKE_LOG="$fixture/make.log"
- OUTPUT_LOG="$fixture/output.log"
- rm -f "$MAKE_LOG"
- if (
- unset COMMIT GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE
- export PATH="$TMP/bin:$PATH"
- export MATURITA_TEST_MAKE_LOG="$MAKE_LOG"
- # A source archive must not accidentally inherit an enclosing checkout.
- export GIT_CEILING_DIRECTORIES="$TMP"
- if [[ "$mode" == supplied ]]; then
- export COMMIT="$value"
- fi
- bash "$fixture/scripts/bundle-macos.sh"
- ) > "$OUTPUT_LOG" 2>&1; then
- STATUS=0
- else
- STATUS=$?
- fi
-}
-
-expect_build_commit() {
- local expected="$1" actual
- [[ "$STATUS" -eq 97 ]] || {
- cat "$OUTPUT_LOG" >&2
- fail "expected the stubbed build to run (status $STATUS)"
- }
- [[ -f "$MAKE_LOG" ]] || fail "xcodebuild was not called"
- actual="$(cat "$MAKE_LOG")"
- [[ "$actual" == "$expected" ]] ||
- fail "build did not receive COMMIT=$expected (got '$actual')"
-}
-
-expect_missing_identity() {
- [[ "$STATUS" -ne 0 ]] || fail 'missing identity unexpectedly succeeded'
- [[ ! -e "$MAKE_LOG" ]] || fail 'missing identity reached xcodebuild'
- grep -q 'COMMIT' "$OUTPUT_LOG" || fail 'missing identity has no COMMIT guidance'
-}
-
-GIT_FIXTURE="$TMP/checkout with spaces"
-new_fixture "$GIT_FIXTURE"
-git init -q "$GIT_FIXTURE"
-git -C "$GIT_FIXTURE" -c user.name='Bundle test' \
- -c user.email='bundle-test@example.invalid' -c commit.gpgsign=false \
- -c core.hooksPath=/dev/null commit --allow-empty -qm 'Fixture commit'
-EXPECTED="$(git -C "$GIT_FIXTURE" rev-parse HEAD)"
-
-run_bundle "$GIT_FIXTURE" unset
-expect_build_commit "$EXPECTED"
-echo 'PASS: Git checkout identity with spaces in its path'
-
-run_bundle "$GIT_FIXTURE" supplied ''
-expect_build_commit "$EXPECTED"
-echo 'PASS: empty COMMIT falls back to checkout identity'
-
-ARCHIVE_FIXTURE="$TMP/source archive"
-new_fixture "$ARCHIVE_FIXTURE"
-EXPLICIT=0123456789abcdef0123456789abcdef01234567
-run_bundle "$ARCHIVE_FIXTURE" supplied "$EXPLICIT"
-expect_build_commit "$EXPLICIT"
-echo 'PASS: explicit COMMIT works without Git metadata'
-
-run_bundle "$ARCHIVE_FIXTURE" supplied dev
-expect_build_commit dev
-echo 'PASS: explicit development identity is preserved'
-
-run_bundle "$ARCHIVE_FIXTURE" unset
-expect_missing_identity
-echo 'PASS: source archive without COMMIT fails before building'
-
-NESTED_FIXTURE="$GIT_FIXTURE/exported sources"
-new_fixture "$NESTED_FIXTURE"
-run_bundle "$NESTED_FIXTURE" unset
-expect_missing_identity
-echo 'PASS: source archive does not inherit an unrelated parent Git identity'
diff --git a/tests/answers.json b/tests/answers.json
deleted file mode 100644
index 36d4fe1..0000000
--- a/tests/answers.json
+++ /dev/null
@@ -1,104 +0,0 @@
-{
- "_about": "Answer checking cases shared by the Swift (iOS, macOS) and Kotlin (Android, desktop) apps. Both must agree with every line. Run with `make test`.",
- "normalize": [
- ["Größe", "groesse"],
- ["Über", "ueber"],
- [" Hallo, Welt! ", "hallo welt"],
- ["Příliš žluťoučký kůň", "prilis zlutoucky kun"],
- ["a\tb\nc", "a b c"],
- ["e-mail", "email"],
- ["a , b", "a b"],
- ["", ""]
- ],
- "accepts": [
- ["Deutschland", "deutschland|eutschland", true],
- ["Osterreich", "österreich|osterreich", true],
- ["Oesterreich", "österreich", true],
- ["hasnt", "hasn't|has not", true],
- ["has not", "hasn't|has not", true],
- ["Seděla", "seděla|sedla", true],
- ["sedela", "seděla|sedla", true],
- ["is", "are", false],
- ["", "", false],
- ["x", null, false]
- ],
- "incomplete": [
- ["der", "der Vater", true],
- ["der vater", "der Vater", false],
- ["", "der Vater", false],
- ["die", "der Vater|die Mutter", true]
- ],
- "netTxtEq": [
- ["10.0.0.0", " 10.0.0.0 ", true],
- ["ABC", "abc", true],
- ["10.0.0.1", "10.0.0.2", false],
- [null, null, true],
- ["a", null, false]
- ],
- "hasUmlaut": [
- ["Mädchen", true],
- ["Straße", true],
- ["Strasse", false],
- [null, false]
- ],
- "math": [
- ["num:12", "12", true],
- ["num:12", "12.0", true],
- ["num:12", "12,0", true],
- ["num:12", "12 cm", true],
- ["num:12", "13", false],
- ["num:12", "", false],
- ["num:12", "abc", false],
- ["num:1/2", "0,5", true],
- ["num:1/2", "2/4", true],
- ["num:1/2", "1/3", false],
- ["num:3/2", "1 1/2", true],
- ["num:-3", "−3", true],
- ["num:50", "50 %", true],
- ["num:2√3", "√12", true],
- ["num:2√3", "sqrt(12)", true],
- ["num:2√3", "3.4641", true],
- ["num:2√3", "3.46", false],
- ["num:2π", "2pi", true],
- ["num:2π", "6.283", false],
- ["set:2;3", "3; 2", true],
- ["set:2;3", "2, 3", true],
- ["set:2;3", "2;3;4", false],
- ["set:2;3", "2", false],
- ["pair:2;-1", "2; -1", true],
- ["pair:2;-1", "-1; 2", false],
- ["word:ano|yes", "Ano", true],
- ["word:ano|yes", "áno", true],
- ["word:ano|yes", "ne", false],
- [null, "1", false]
- ],
- "drawNeed": [
- ["square:2,3;5,3", 2],
- ["mid:0,0;4,4", 1],
- ["points:1,4;fix:1,1;fix:5,1", 1],
- ["num:3", 0]
- ],
- "drawGiven": [
- ["points:1,4;fix:1,1;fix:5,1", [[1, 1], [5, 1]]],
- ["mid:0,0;4,4", [[0, 0], [4, 4]]],
- ["para:0,0;2,0;0,2", [[0, 0], [2, 0], [0, 2]]],
- ["num:3", []]
- ],
- "draw": [
- ["mid:0,0;4,4", [[2, 2]], true],
- ["mid:0,0;4,4", [[2, 3]], false],
- ["mid:0,0;3,4", [[1, 2]], false],
- ["para:0,0;2,0;0,2", [[2, 2]], true],
- ["para:0,0;2,0;0,2", [[2, 1]], false],
- ["right:2,4;8,4", [[5, 1]], true],
- ["right:2,4;8,4", [[5, 4]], false],
- ["square:2,3;5,3", [[5, 6], [2, 6]], true],
- ["square:2,3;5,3", [[2, 6], [5, 6]], true],
- ["square:2,3;5,3", [[5, 0], [2, 0]], true],
- ["square:2,3;5,3", [[5, 5], [2, 5]], false],
- ["points:1,4;fix:1,1", [[1, 4]], true],
- ["points:1,4;fix:1,1", [[1, 5]], false],
- ["points:1,4;fix:1,1", [[11, 0]], false],
- ["num:3", [], false]
- ]
-}
diff --git a/tests/swift/main.swift b/tests/swift/main.swift
deleted file mode 100644
index fb4c289..0000000
--- a/tests/swift/main.swift
+++ /dev/null
@@ -1,64 +0,0 @@
-// Runs tests/answers.json against ios/Graduately/Data/Answers.swift, the same
-// cases the Kotlin apps check (android/desktop/src/test). Usage:
-// swift-answers
-import Foundation
-
-let args = CommandLine.arguments
-guard args.count == 3,
- let casesData = FileManager.default.contents(atPath: args[1]),
- let cases = try? JSONSerialization.jsonObject(with: casesData) as? [String: Any],
- let contentData = FileManager.default.contents(atPath: args[2]),
- let content = try? JSONSerialization.jsonObject(with: contentData) as? [String: Any]
-else {
- print("usage: swift-answers tests/answers.json content.json")
- exit(2)
-}
-
-var failures = 0
-var checked = 0
-
-func rows(_ name: String) -> [[Any]] { cases[name] as? [[Any]] ?? [] }
-func str(_ v: Any) -> String? { v as? String }
-func points(_ v: Any) -> [(Int, Int)] { (v as? [[Int]] ?? []).map { ($0[0], $0[1]) } }
-
-func expect(_ got: T, _ want: T, _ what: String) {
- checked += 1
- if got != want {
- failures += 1
- print("FAIL \(what): got \(got), want \(want)")
- }
-}
-
-for r in rows("normalize") { expect(normalizeAnswer(str(r[0])), str(r[1]) ?? "", "normalize \(r[0])") }
-for r in rows("accepts") { expect(answerAccepts(normalizeAnswer(str(r[0])), str(r[1])), r[2] as! Bool, "accepts \(r)") }
-for r in rows("incomplete") { expect(answerIncomplete(normalizeAnswer(str(r[0])), str(r[1])), r[2] as! Bool, "incomplete \(r)") }
-for r in rows("netTxtEq") { expect(netTxtEq(str(r[0]), str(r[1])), r[2] as! Bool, "netTxtEq \(r)") }
-for r in rows("hasUmlaut") { expect(hasUmlaut(str(r[0])), r[1] as! Bool, "hasUmlaut \(r)") }
-for r in rows("math") { expect(mathAnswerOk(str(r[0]), str(r[1])), r[2] as! Bool, "math \(r)") }
-for r in rows("drawNeed") { expect(mathDrawNeed(str(r[0])), r[1] as! Int, "drawNeed \(r)") }
-for r in rows("drawGiven") {
- expect(mathDrawGiven(str(r[0])).map { "\($0.0),\($0.1)" }, points(r[1]).map { "\($0.0),\($0.1)" }, "drawGiven \(r)")
-}
-for r in rows("draw") { expect(mathDrawOk(str(r[0]), points(r[1])), r[2] as! Bool, "draw \(r)") }
-
-// Every worked answer in content/ must pass its own check.
-var problems = 0
-for section in ["mat0", "mat", "mat2", "mat3", "mat4"] {
- for lesson in content[section] as? [[String: Any]] ?? [] {
- for p in lesson["problems"] as? [[String: Any]] ?? [] {
- guard let spec = p["answer"] as? String else { continue }
- let at = "\(section) \(lesson["key"] ?? ""): \(spec)"
- if mathIsDraw(spec) {
- expect(mathDrawNeed(spec) > 0, true, at)
- } else {
- let body = spec.split(separator: ":", maxSplits: 1).last.map(String.init) ?? spec
- expect(mathAnswerOk(spec, body.components(separatedBy: "|")[0]), true, at)
- }
- problems += 1
- }
- }
-}
-expect(problems > 100, true, "content has math problems")
-
-print("swift: \(checked - failures)/\(checked) ok")
-exit(failures == 0 ? 0 : 1)