Initial commit
3
.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
17
.github/workflows/publish.yaml
vendored
@ -1,17 +0,0 @@
|
||||
name: Publish to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: subosito/flutter-action@v1
|
||||
- uses: bluefireteam/flutter-gh-pages@v7
|
||||
with:
|
||||
webRenderer: canvaskit
|
||||
# baseHref: /s5_practice/
|
76
.gitignore
vendored
@ -1,50 +1,36 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
*.pem
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# Web related
|
||||
lib/generated_plugin_registrant.dart
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# moodle_gen.py output
|
||||
radioamaterstvo.xml
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
30
.metadata
@ -1,30 +0,0 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled.
|
||||
|
||||
version:
|
||||
revision: f1875d570e39de09040c8f79aa13cc56baab8db1
|
||||
channel: stable
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
|
||||
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
|
||||
- platform: android
|
||||
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
|
||||
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
18
.vscode/launch.json
vendored
@ -1,18 +0,0 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "HTML (lib\\main.dart)",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib\\main.dart",
|
||||
"args": ["--web-renderer", "html"]
|
||||
},
|
||||
{
|
||||
"name": "Canvaskit (lib\\main.dart)",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib\\main.dart",
|
||||
"args": ["--web-renderer", "canvaskit"]
|
||||
}
|
||||
]
|
||||
}
|
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
}
|
33
README.md
@ -1,3 +1,32 @@
|
||||
# s5_practice
|
||||
# Radioamateski izpit
|
||||
|
||||
S5 Practice Exam Questions
|
||||
Uporabljene tehnologije:
|
||||
|
||||
- Next.js
|
||||
- TailwindCSS
|
||||
- Zustand
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Domača stran
|
||||
- [ ] Uvod
|
||||
- [ ] Uporabne povezave
|
||||
- [ ] Priprave
|
||||
- [ ] Vaje po kategorijah
|
||||
- [ ] Izpis vseh vprašanj
|
||||
- [ ] Simulacija izpita
|
||||
- [ ] Persistenca?
|
||||
- [ ] Katex
|
||||
- [ ] Generator pol
|
||||
- [ ] A / N razred
|
||||
- [ ] Pravilno generiranje
|
||||
- [ ] Stran s pravilnimi odgovori
|
||||
- [ ] Številka pole v glavi
|
||||
- [ ] Katex
|
||||
- [ ] Slike
|
||||
- [ ] PDF?
|
||||
- [ ] Prepreči lom strani med vprašanjem in odgovori
|
||||
|
||||
## Viri
|
||||
|
||||
- [Primer pole](http://www.volkd.si/Izpitna/A-094.pdf)
|
||||
|
@ -1,7 +0,0 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
rules:
|
||||
avoid_print: false
|
||||
prefer_single_quotes: true
|
||||
prefer_relative_imports: true
|
13
android/.gitignore
vendored
@ -1,13 +0,0 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
@ -1,81 +0,0 @@
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "cc.jkob.s5_practice"
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="cc.jkob.s5_practice">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
@ -1,18 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="cc.jkob.s5_practice">
|
||||
<application android:label="S5 Vaja" android:name="${applicationName}" android:icon="@mipmap/ic_launcher">
|
||||
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data android:name="flutterEmbedding" android:value="2" />
|
||||
</application>
|
||||
</manifest>
|
@ -1,6 +0,0 @@
|
||||
package cc.jkob.s5_practice
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
}
|
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 3.2 KiB |
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 14 KiB |
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 9.2 KiB |
@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#197CD7</color>
|
||||
</resources>
|
@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
@ -1,7 +0,0 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="cc.jkob.s5_practice">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
@ -1,31 +0,0 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.6.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
@ -1,6 +0,0 @@
|
||||
#Fri Jun 23 08:50:38 CEST 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
@ -1,11 +0,0 @@
|
||||
include ':app'
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
@ -1,5 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'src/app.dart';
|
||||
|
||||
void main() => runApp(const App());
|
@ -1,39 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'cubit/questions_cubit.dart';
|
||||
import 'home_screen.dart';
|
||||
import 'quiz/cubit/quiz_cubit.dart';
|
||||
import 'quiz/quiz_screen.dart';
|
||||
|
||||
class App extends StatelessWidget {
|
||||
const App({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocProvider(
|
||||
create: (context) => QuestionsCubit()..load(),
|
||||
child: MaterialApp(
|
||||
title: 'Izpitna vprašanja za radioamaterje',
|
||||
initialRoute: '/',
|
||||
routes: <String, WidgetBuilder>{
|
||||
'/': (context) => const HomeScreen(),
|
||||
},
|
||||
onGenerateRoute: (settings) {
|
||||
if (settings.name == '/quiz' && settings.arguments is QuizState) {
|
||||
return MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
QuizScreen(quizState: settings.arguments as QuizState),
|
||||
settings: settings,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
theme: ThemeData(
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SizedCard extends StatelessWidget {
|
||||
final Widget? child;
|
||||
|
||||
const SizedCard({Key? key, this.child}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import '../models/category.dart';
|
||||
import '../models/question.dart';
|
||||
|
||||
part 'questions_state.dart';
|
||||
|
||||
class QuestionsCubit extends Cubit<QuestionsState> {
|
||||
QuestionsCubit() : super(QuestionsInitial());
|
||||
|
||||
Future<void> load() async {
|
||||
final s = await rootBundle.loadString('assets/questions.json');
|
||||
final decoded = jsonDecode(s);
|
||||
final c = (decoded['categories'] as List)
|
||||
.map((e) => Category.fromJson(e))
|
||||
.toList();
|
||||
final q = (decoded['questions'] as List)
|
||||
.map((e) => Question.fromJson(e))
|
||||
.toList();
|
||||
emit(QuestionsLoaded(c, q));
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
part of 'questions_cubit.dart';
|
||||
|
||||
abstract class QuestionsState extends Equatable {
|
||||
const QuestionsState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class QuestionsInitial extends QuestionsState {}
|
||||
|
||||
class QuestionsLoaded extends QuestionsState {
|
||||
final List<Category> categories;
|
||||
final List<Question> questions;
|
||||
|
||||
const QuestionsLoaded(this.categories, this.questions);
|
||||
|
||||
@override
|
||||
List<Object> get props => [categories, questions];
|
||||
|
||||
List<Question> getRandom([
|
||||
int? categoryId,
|
||||
bool skipDraw = false,
|
||||
]) {
|
||||
final cat = categoryId == null
|
||||
? null
|
||||
: categories.firstWhere((e) => e.id == categoryId);
|
||||
var q = cat == null
|
||||
? questions.toList()
|
||||
: [
|
||||
for (final range in cat.questions)
|
||||
for (int i = range.first - 1; i < range.last; i++) questions[i],
|
||||
];
|
||||
if (skipDraw) q = q.where((e) => e.answers != null).toList();
|
||||
return q..shuffle();
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../models/category.dart';
|
||||
|
||||
part 'generator_state.dart';
|
||||
|
||||
class GeneratorCubit extends Cubit<GeneratorState> {
|
||||
GeneratorCubit() : super(const GeneratorState());
|
||||
|
||||
void setSingleCategory(bool singleCategory) =>
|
||||
emit(state.copyWith(singleCategory: singleCategory));
|
||||
|
||||
void setCategory(Category category) => emit(state.copyWith(
|
||||
singleCategory: true,
|
||||
category: category,
|
||||
));
|
||||
|
||||
void setPracticeQuestionCount(String questions) =>
|
||||
emit(state.copyWith(practiceQuestionCount: int.tryParse(questions)));
|
||||
|
||||
void setTestQuestionCount(String questions) =>
|
||||
emit(state.copyWith(testQuestionCount: int.tryParse(questions)));
|
||||
|
||||
void setDuration(String minutes) {
|
||||
final min = int.tryParse(minutes);
|
||||
if (min == null) return;
|
||||
emit(state.copyWith(timerDuration: Duration(minutes: min)));
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
part of 'generator_cubit.dart';
|
||||
|
||||
enum GeneratorType {
|
||||
practice,
|
||||
test,
|
||||
}
|
||||
|
||||
class GeneratorState extends Equatable {
|
||||
final int practiceQuestionCount;
|
||||
final int testQuestionCount;
|
||||
final bool singleCategory;
|
||||
final Category? category;
|
||||
final Duration timerDuration;
|
||||
|
||||
const GeneratorState({
|
||||
this.practiceQuestionCount = 5,
|
||||
this.testQuestionCount = kDebugMode ? 6 : 60,
|
||||
this.singleCategory = false,
|
||||
this.category,
|
||||
this.timerDuration = const Duration(minutes: kDebugMode ? 9 : 90),
|
||||
});
|
||||
|
||||
GeneratorState copyWith({
|
||||
GeneratorType? type,
|
||||
int? practiceQuestionCount,
|
||||
int? testQuestionCount,
|
||||
bool? singleCategory,
|
||||
Category? category,
|
||||
Duration? timerDuration,
|
||||
}) {
|
||||
final newSingleCategory = singleCategory ?? this.singleCategory;
|
||||
|
||||
return GeneratorState(
|
||||
practiceQuestionCount:
|
||||
practiceQuestionCount ?? this.practiceQuestionCount,
|
||||
testQuestionCount: testQuestionCount ?? this.testQuestionCount,
|
||||
singleCategory: newSingleCategory,
|
||||
category: newSingleCategory ? (category ?? this.category) : null,
|
||||
timerDuration: timerDuration ?? this.timerDuration,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
practiceQuestionCount,
|
||||
testQuestionCount,
|
||||
singleCategory,
|
||||
category,
|
||||
timerDuration,
|
||||
];
|
||||
}
|
@ -1,141 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../cubit/questions_cubit.dart';
|
||||
import '../models/category.dart';
|
||||
import '../quiz/cubit/quiz_cubit.dart';
|
||||
import 'cubit/generator_cubit.dart';
|
||||
|
||||
class PracticeTab extends StatelessWidget {
|
||||
const PracticeTab({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Vaja',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
const Text(
|
||||
'Izberi pogročje in vpiši število vprašanj, ki jih želiš generirati. '
|
||||
'Če želiš generirati vprašanja iz vseh področij, pusti polje za področje prazno.'),
|
||||
const SizedBox(height: 20),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth > 600) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _CategoryInput(),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: _QuestionNumberInput(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_CategoryInput(),
|
||||
const SizedBox(height: 15),
|
||||
_QuestionNumberInput(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
final generatorState = context.read<GeneratorCubit>().state;
|
||||
final questionsState =
|
||||
context.read<QuestionsCubit>().state as QuestionsLoaded;
|
||||
Navigator.pushNamed(context, '/quiz',
|
||||
arguments: QuizState(
|
||||
title: generatorState.category == null
|
||||
? 'Vaja - Vse kategorije'
|
||||
: 'Vaja - ${generatorState.category!.title}',
|
||||
questions:
|
||||
questionsState.getRandom(generatorState.category?.id),
|
||||
count: generatorState.practiceQuestionCount,
|
||||
revealInstantly: true,
|
||||
));
|
||||
},
|
||||
child: const Text('Začni'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _CategoryInput extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
BlocBuilder<QuestionsCubit, QuestionsState>(
|
||||
builder: (context, qstate) =>
|
||||
BlocBuilder<GeneratorCubit, GeneratorState>(
|
||||
builder: (context, gstate) {
|
||||
qstate as QuestionsLoaded;
|
||||
|
||||
return DropdownButtonFormField<Category>(
|
||||
isExpanded: true,
|
||||
value: gstate.category,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
enabled: gstate.singleCategory,
|
||||
labelText: 'Izberi področje',
|
||||
suffixIcon: gstate.category == null
|
||||
? null
|
||||
: IconButton(
|
||||
splashRadius: 18,
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () => context
|
||||
.read<GeneratorCubit>()
|
||||
.setSingleCategory(false),
|
||||
),
|
||||
),
|
||||
items: qstate.categories
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text(
|
||||
e.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
context.read<GeneratorCubit>().setCategory(value);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _QuestionNumberInput extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
BlocBuilder<GeneratorCubit, GeneratorState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.practiceQuestionCount != current.practiceQuestionCount,
|
||||
builder: (context, state) => TextFormField(
|
||||
initialValue: '${state.practiceQuestionCount}',
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Število vprašanj',
|
||||
),
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: context.read<GeneratorCubit>().setPracticeQuestionCount,
|
||||
),
|
||||
);
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../cubit/questions_cubit.dart';
|
||||
import '../quiz/cubit/quiz_cubit.dart';
|
||||
import 'cubit/generator_cubit.dart';
|
||||
|
||||
class TestTab extends StatelessWidget {
|
||||
const TestTab({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Preizkus uspeha',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
const Text(
|
||||
'Kandidati za radioamaterja razreda A opravljajo izpit, ki je '
|
||||
'sestavljen iz 60 različnih vprašanj. Vsako vprašanje ima 3 možne odgovore, od katerih je '
|
||||
'samo en pravilen. Kandidat ima na voljo 90 minut za reševanje izpitne pole. Kandidat mora '
|
||||
'pravilno odgovoriti vsaj na 36 vprašanj (60%).'),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Preizkus uspeha NE bo vseboval vprašanj s področja "Risanje"!'),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(child: _TestQuestionNumberInput()),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: _DurationInput()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
final generatorState = context.read<GeneratorCubit>().state;
|
||||
final questionsState =
|
||||
context.read<QuestionsCubit>().state as QuestionsLoaded;
|
||||
Navigator.pushNamed(context, '/quiz',
|
||||
arguments: QuizState(
|
||||
title: 'Preizkus uspeha',
|
||||
questions: questionsState
|
||||
.getRandom(null, true)
|
||||
.take(generatorState.testQuestionCount)
|
||||
.toList(),
|
||||
count: generatorState.testQuestionCount,
|
||||
duration: generatorState.timerDuration,
|
||||
revealInstantly: false,
|
||||
));
|
||||
},
|
||||
child: const Text('Naprej'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _TestQuestionNumberInput extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
BlocBuilder<GeneratorCubit, GeneratorState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.testQuestionCount != current.testQuestionCount,
|
||||
builder: (context, state) => TextFormField(
|
||||
initialValue: '${state.testQuestionCount}',
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Število vprašanj',
|
||||
),
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: context.read<GeneratorCubit>().setTestQuestionCount,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _DurationInput extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
BlocBuilder<GeneratorCubit, GeneratorState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.timerDuration != current.timerDuration,
|
||||
builder: (context, state) => TextFormField(
|
||||
initialValue: '${state.timerDuration.inMinutes}',
|
||||
decoration: const InputDecoration(
|
||||
suffixText: 'min',
|
||||
labelText: 'Čas za reševanje',
|
||||
),
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: context.read<GeneratorCubit>().setDuration,
|
||||
),
|
||||
);
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'components/sized_card.dart';
|
||||
import 'cubit/questions_cubit.dart';
|
||||
import 'generator/cubit/generator_cubit.dart';
|
||||
import 'generator/practice_tab.dart';
|
||||
import 'generator/test_tab.dart';
|
||||
|
||||
class HomeScreen extends StatelessWidget {
|
||||
static const _tabs = [
|
||||
Text('Vaja'),
|
||||
Text('Preizkus uspeha'),
|
||||
];
|
||||
|
||||
const HomeScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text('Izpitna vprašanja za radioamaterje')),
|
||||
body: BlocBuilder<QuestionsCubit, QuestionsState>(
|
||||
builder: (context, state) => state is! QuestionsLoaded
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: BlocProvider(
|
||||
create: (_) => GeneratorCubit(),
|
||||
child: DefaultTabController(
|
||||
length: _tabs.length,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
child: TabBar(
|
||||
labelColor: Theme.of(context).colorScheme.primary,
|
||||
unselectedLabelColor: Colors.grey.shade700,
|
||||
labelStyle: const TextStyle(fontSize: 16),
|
||||
labelPadding: const EdgeInsets.all(12),
|
||||
tabs: _tabs,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
const PracticeTab(),
|
||||
const TestTab(),
|
||||
]
|
||||
.map(
|
||||
(e) => SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: SizedCard(child: e),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class Category extends Equatable {
|
||||
final int id;
|
||||
final String title;
|
||||
final List<List<int>> questions;
|
||||
|
||||
const Category({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.questions,
|
||||
});
|
||||
|
||||
factory Category.fromJson(Map<String, dynamic> json) => Category(
|
||||
id: json['id'],
|
||||
title: json['title'],
|
||||
questions: (json['questions'] as List)
|
||||
.map((e) => (e as List).cast<int>())
|
||||
.toList(),
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, title, questions];
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class Question extends Equatable {
|
||||
final int id;
|
||||
final String question;
|
||||
final String? image;
|
||||
final List<String>? answers;
|
||||
final int? correct;
|
||||
|
||||
const Question({
|
||||
required this.id,
|
||||
required this.question,
|
||||
required this.image,
|
||||
required this.answers,
|
||||
required this.correct,
|
||||
});
|
||||
|
||||
factory Question.fromJson(Map<String, dynamic> json) => Question(
|
||||
id: json['id'],
|
||||
question: json['question'],
|
||||
image: json['image'],
|
||||
answers: (json['answers'] as List?)?.cast(),
|
||||
correct: json['correct'],
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, question, image, answers, correct];
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../models/question.dart';
|
||||
|
||||
part 'quiz_state.dart';
|
||||
|
||||
class QuizCubit extends Cubit<QuizState> {
|
||||
QuizCubit(QuizState quizState) : super(quizState);
|
||||
|
||||
void start() => emit(state.copyWith(
|
||||
answers: state.revealInstantly
|
||||
? null
|
||||
: List.filled(state.questions.length, null),
|
||||
revealed: state.revealInstantly
|
||||
? List.filled(state.questions.length, null)
|
||||
: null,
|
||||
startTime: DateTime.now(),
|
||||
));
|
||||
|
||||
void answer(int index, int answer) {
|
||||
if (state.revealInstantly) {
|
||||
final revealed = state.revealed!.toList();
|
||||
revealed[index] = ((revealed[index]?.toSet() ?? {})..add(answer));
|
||||
|
||||
emit(state.copyWith(revealed: revealed));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final answers = state.answers!.toList();
|
||||
answers[index] = answer;
|
||||
|
||||
emit(state.copyWith(answers: answers));
|
||||
}
|
||||
|
||||
void extend() => emit(state.copyWith(count: state.count + state.firstCount));
|
||||
|
||||
void finish() => emit(state.copyWith(endTime: DateTime.now()));
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
part of 'quiz_cubit.dart';
|
||||
|
||||
class QuizState extends Equatable {
|
||||
final String title;
|
||||
final List<Question> questions;
|
||||
final int firstCount;
|
||||
final int count;
|
||||
final Duration? duration;
|
||||
final bool revealInstantly;
|
||||
|
||||
final DateTime? startTime;
|
||||
final DateTime? endTime;
|
||||
final List<int?>? answers;
|
||||
final List<Set<int>?>? revealed;
|
||||
|
||||
int? get score {
|
||||
if (answers == null) return null;
|
||||
int c = 0;
|
||||
for (int i = 0; i < answers!.length; ++i) {
|
||||
if (answers![i] == questions[i].correct) ++c;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
List<int>? get incorrectAnswers {
|
||||
final ret = <int>[];
|
||||
for (int i = 0; i < answers!.length; ++i) {
|
||||
if (answers![i] != questions[i].correct) ret.add(i);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
QuizState({
|
||||
required this.title,
|
||||
required this.questions,
|
||||
int? firstCount,
|
||||
required int count,
|
||||
this.duration,
|
||||
required this.revealInstantly,
|
||||
this.startTime,
|
||||
this.endTime,
|
||||
this.answers,
|
||||
this.revealed,
|
||||
}) : firstCount = firstCount ?? min(count, questions.length),
|
||||
count = min(count, questions.length);
|
||||
|
||||
QuizState copyWith({
|
||||
String? title,
|
||||
List<Question>? questions,
|
||||
int? count,
|
||||
Duration? duration,
|
||||
bool? revealInstantly,
|
||||
DateTime? startTime,
|
||||
DateTime? endTime,
|
||||
List<int?>? answers,
|
||||
List<Set<int>?>? revealed,
|
||||
}) =>
|
||||
QuizState(
|
||||
title: title ?? this.title,
|
||||
questions: questions ?? this.questions,
|
||||
count: count ?? this.count,
|
||||
duration: duration ?? this.duration,
|
||||
revealInstantly: revealInstantly ?? this.revealInstantly,
|
||||
startTime: startTime ?? this.startTime,
|
||||
endTime: endTime ?? this.endTime,
|
||||
answers: answers ?? this.answers,
|
||||
revealed: revealed ?? this.revealed,
|
||||
firstCount: firstCount,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
title,
|
||||
questions,
|
||||
firstCount,
|
||||
count,
|
||||
duration,
|
||||
revealInstantly,
|
||||
startTime,
|
||||
endTime,
|
||||
answers,
|
||||
revealed,
|
||||
];
|
||||
}
|
@ -1,261 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_math_fork/flutter_math.dart';
|
||||
|
||||
import '../components/sized_card.dart';
|
||||
import 'cubit/quiz_cubit.dart';
|
||||
|
||||
class QuestionCard extends StatelessWidget {
|
||||
final int qIndex;
|
||||
final bool forResultScreen;
|
||||
|
||||
const QuestionCard({
|
||||
Key? key,
|
||||
required this.qIndex,
|
||||
this.forResultScreen = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocBuilder<QuizCubit, QuizState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.questions[qIndex] != current.questions[qIndex] ||
|
||||
previous.answers?[qIndex] != current.answers?[qIndex] ||
|
||||
previous.revealed?[qIndex] != current.revealed?[qIndex],
|
||||
builder: (context, state) {
|
||||
final theme = Theme.of(context);
|
||||
final question = state.questions[qIndex];
|
||||
|
||||
final child = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${question.id}'.padLeft(3, '0'),
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
'${qIndex + 1}. ${question.question}',
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (question.image != null)
|
||||
Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 500,
|
||||
maxWidth: 500,
|
||||
),
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: question.answers != null
|
||||
? Image.asset('assets/images/${question.image}')
|
||||
: HiddenImage(
|
||||
img: question.image!,
|
||||
isHidden: state.revealed![qIndex] == null,
|
||||
onClick: () =>
|
||||
context.read<QuizCubit>().answer(qIndex, 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (question.answers != null)
|
||||
Material(
|
||||
color: Colors.white,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: ListView.separated(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: question.answers!.length,
|
||||
itemBuilder: (_, aIndex) {
|
||||
if (state.revealInstantly) {
|
||||
final revealed = state.revealed![qIndex] ?? {};
|
||||
|
||||
return AnswerTile(
|
||||
index: aIndex,
|
||||
text: question.answers![aIndex],
|
||||
isCorrect: aIndex == question.correct,
|
||||
isRevealed: revealed.contains(aIndex),
|
||||
isEnabled: !revealed.contains(question.correct),
|
||||
onClick: () =>
|
||||
context.read<QuizCubit>().answer(qIndex, aIndex),
|
||||
);
|
||||
}
|
||||
|
||||
if (forResultScreen) {
|
||||
return AnswerTile(
|
||||
index: aIndex,
|
||||
text: question.answers![aIndex],
|
||||
isCorrect: aIndex == question.correct,
|
||||
isRevealed: true,
|
||||
isEnabled: false,
|
||||
isSelected: state.answers![qIndex] == aIndex,
|
||||
);
|
||||
}
|
||||
|
||||
return AnswerTile(
|
||||
index: aIndex,
|
||||
text: question.answers![aIndex],
|
||||
isCorrect: aIndex == question.correct,
|
||||
isRevealed: false,
|
||||
isEnabled: true,
|
||||
isSelected: state.answers![qIndex] == aIndex,
|
||||
onClick: () =>
|
||||
context.read<QuizCubit>().answer(qIndex, aIndex),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Divider(height: 0),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
if (forResultScreen) return child;
|
||||
|
||||
return SizedCard(child: child);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class HiddenImage extends StatelessWidget {
|
||||
final String img;
|
||||
final bool isHidden;
|
||||
final void Function()? onClick;
|
||||
|
||||
const HiddenImage({
|
||||
Key? key,
|
||||
required this.img,
|
||||
this.isHidden = true,
|
||||
this.onClick,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return FramedWidget(
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.asset('assets/images/$img'),
|
||||
if (isHidden)
|
||||
Positioned.fill(
|
||||
child: Material(
|
||||
color: Color.alphaBlend(
|
||||
theme.colorScheme.primary.withAlpha(20),
|
||||
theme.colorScheme.surface,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onClick,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(30),
|
||||
child: Text(
|
||||
'Razkrij odgovor',
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FramedWidget extends StatelessWidget {
|
||||
final Widget? child;
|
||||
|
||||
const FramedWidget({
|
||||
Key? key,
|
||||
this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Material(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: theme.colorScheme.primary),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnswerTile extends StatelessWidget {
|
||||
final int index;
|
||||
final String text;
|
||||
final bool isCorrect;
|
||||
final void Function()? onClick;
|
||||
final bool isRevealed;
|
||||
final bool isEnabled;
|
||||
final bool? isSelected;
|
||||
|
||||
const AnswerTile({
|
||||
Key? key,
|
||||
required this.index,
|
||||
required this.text,
|
||||
required this.isCorrect,
|
||||
this.onClick,
|
||||
this.isRevealed = false,
|
||||
this.isEnabled = true,
|
||||
this.isSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tileColor = isRevealed
|
||||
? (isCorrect ? Colors.green.shade50 : Colors.red.shade50)
|
||||
: null;
|
||||
final textColor = isRevealed
|
||||
? (isCorrect ? Colors.green.shade900 : Colors.red.shade900)
|
||||
: null;
|
||||
|
||||
return ListTile(
|
||||
leading: _leading(context),
|
||||
onTap: !isRevealed && isEnabled ? onClick : null,
|
||||
title: text.startsWith(r'$')
|
||||
? Math.tex(
|
||||
text.substring(1, text.length - 1),
|
||||
textScaleFactor: 1.2,
|
||||
)
|
||||
: Text(text),
|
||||
minVerticalPadding: 10,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
iconColor: isRevealed
|
||||
? (isCorrect ? Colors.green.shade900 : Colors.red.shade900)
|
||||
: null,
|
||||
tileColor: tileColor,
|
||||
textColor: textColor,
|
||||
selectedTileColor: tileColor,
|
||||
selectedColor: textColor,
|
||||
selected: isSelected == true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _leading(BuildContext context) {
|
||||
if (isSelected == true) return const Icon(Icons.radio_button_checked);
|
||||
if (isSelected == false) return const Icon(Icons.radio_button_off);
|
||||
|
||||
if (!isRevealed) {
|
||||
return SizedBox(
|
||||
width: 24,
|
||||
child: Text(
|
||||
String.fromCharCode(65 + index),
|
||||
style:
|
||||
Theme.of(context).textTheme.displaySmall!.copyWith(fontSize: 20),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (isCorrect) return const Icon(Icons.check);
|
||||
return const Icon(Icons.clear);
|
||||
}
|
||||
}
|
@ -1,357 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:timer_builder/timer_builder.dart';
|
||||
|
||||
import '../components/sized_card.dart';
|
||||
import 'cubit/quiz_cubit.dart';
|
||||
import 'question_card.dart';
|
||||
|
||||
class QuizScreen extends StatefulWidget {
|
||||
final QuizState quizState;
|
||||
|
||||
const QuizScreen({
|
||||
Key? key,
|
||||
required this.quizState,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<QuizScreen> createState() => _QuizScreenState();
|
||||
}
|
||||
|
||||
class _QuizScreenState extends State<QuizScreen> {
|
||||
late final QuizCubit _quizCubit;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_quizCubit = QuizCubit(widget.quizState);
|
||||
if (widget.quizState.duration != null) {
|
||||
_timer = Timer(
|
||||
widget.quizState.duration!,
|
||||
() => _quizCubit.finish(),
|
||||
);
|
||||
} else {
|
||||
_quizCubit.start();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_timer?.cancel();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => WillPopScope(
|
||||
onWillPop: () async {
|
||||
final state = _quizCubit.state;
|
||||
if (state.duration == null ||
|
||||
state.startTime == null ||
|
||||
state.endTime != null) return true;
|
||||
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Zapusti kviz?'),
|
||||
content: const Text(
|
||||
'Če zapustite to stran, bodo vaši odgovori izgubljeni.'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Ostani'),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(primary: Colors.green),
|
||||
onPressed: () {
|
||||
_quizCubit.finish();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('Zaključi'),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(primary: Colors.red),
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Zapusti'),
|
||||
),
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
},
|
||||
child: BlocProvider.value(
|
||||
value: _quizCubit,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: _Title(),
|
||||
actions: [
|
||||
_TimerCountdown(),
|
||||
],
|
||||
),
|
||||
body: BlocBuilder<QuizCubit, QuizState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.startTime != current.startTime ||
|
||||
previous.endTime != current.endTime,
|
||||
builder: (context, state) {
|
||||
if (state.startTime == null) return _StartPage();
|
||||
|
||||
if (state.endTime != null) return _ResultPage();
|
||||
|
||||
return _QuizPage();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _Title extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocBuilder<QuizCubit, QuizState>(
|
||||
buildWhen: (previous, current) => previous.title != current.title,
|
||||
builder: (context, state) => Text(state.title),
|
||||
);
|
||||
}
|
||||
|
||||
class _TimerCountdown extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocBuilder<QuizCubit, QuizState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.startTime != current.startTime ||
|
||||
previous.endTime != current.endTime,
|
||||
builder: (context, state) {
|
||||
if (state.duration == null ||
|
||||
state.startTime == null ||
|
||||
state.endTime != null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 15,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: TimerBuilder.periodic(
|
||||
const Duration(seconds: 1),
|
||||
builder: (context) {
|
||||
final dur = state.startTime!
|
||||
.add(state.duration!)
|
||||
.difference(DateTime.now());
|
||||
return Text(
|
||||
'${dur.inSeconds ~/ 60}:${'${dur.inSeconds % 60}'.padLeft(2, '0')}',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _StartPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tTheme = Theme.of(context).textTheme;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: SizedCard(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Preizkus uspeha',
|
||||
style: tTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final halfW =
|
||||
BoxConstraints(minWidth: constraints.maxWidth / 2);
|
||||
|
||||
return BlocBuilder<QuizCubit, QuizState>(
|
||||
builder: (context, state) {
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: halfW,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${state.count}',
|
||||
style: tTheme.headlineMedium,
|
||||
),
|
||||
Text(
|
||||
'vprašanj',
|
||||
style: tTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: halfW,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${state.duration!.inMinutes}',
|
||||
style: tTheme.headlineMedium,
|
||||
),
|
||||
Text(
|
||||
'minut',
|
||||
style: tTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text('Ob pritisku na spodnji gumb, se preizkus začne. '
|
||||
'Ob izteku časa, se samodejno zaključi.'),
|
||||
const SizedBox(height: 10),
|
||||
Center(
|
||||
child: ElevatedButton(
|
||||
child: const Text('Začni preizkus'),
|
||||
onPressed: () => context.read<QuizCubit>().start(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ResultPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: SizedCard(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Rezultat',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
BlocBuilder<QuizCubit, QuizState>(
|
||||
builder: (context, state) {
|
||||
final correct = state.score!;
|
||||
final count = state.count;
|
||||
final correctPercentage = correct / count * 100;
|
||||
final corrPerStr = correctPercentage.toStringAsFixed(1);
|
||||
return Text(
|
||||
'$correct / $count ($corrPerStr %)',
|
||||
style: Theme.of(context).textTheme.headlineMedium!.copyWith(
|
||||
color: correctPercentage >= 60
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Center(
|
||||
child: ElevatedButton(
|
||||
child: const Text('Nazaj domov'),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
BlocBuilder<QuizCubit, QuizState>(
|
||||
builder: (context, state) {
|
||||
final wrongs = state.incorrectAnswers!;
|
||||
|
||||
if (wrongs.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.only(top: 15),
|
||||
shrinkWrap: true,
|
||||
itemCount: wrongs.length + 2,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 20),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return Divider(
|
||||
color: Colors.grey.shade200,
|
||||
thickness: 2,
|
||||
);
|
||||
}
|
||||
|
||||
if (index == 1) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Napačni odgovori',
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return QuestionCard(
|
||||
qIndex: wrongs[index - 2],
|
||||
forResultScreen: true,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _QuizPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocBuilder<QuizCubit, QuizState>(
|
||||
builder: (context, state) {
|
||||
final moreQuestions = state.count < state.questions.length;
|
||||
final isTest = state.duration != null;
|
||||
final extraLine = moreQuestions || isTest;
|
||||
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(20),
|
||||
shrinkWrap: true,
|
||||
itemCount: state.count + (extraLine ? 1 : 0),
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == state.count) {
|
||||
if (moreQuestions) {
|
||||
return Center(
|
||||
child: ElevatedButton(
|
||||
child: const Text('Več'),
|
||||
onPressed: () => context.read<QuizCubit>().extend(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: ElevatedButton(
|
||||
child: const Text('Končaj'),
|
||||
onPressed: () => context.read<QuizCubit>().finish(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return QuestionCard(qIndex: index);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
8
next.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
appDir: true,
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
35
package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "izpit",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"format": "prettier --write ./src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/font": "13.1.6",
|
||||
"@types/node": "18.14.0",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"bulma": "^0.9.4",
|
||||
"eslint": "8.34.0",
|
||||
"eslint-config-next": "13.1.6",
|
||||
"next": "13.1.7-canary.21",
|
||||
"node-sass": "^8.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-katex": "^3.0.1",
|
||||
"typescript": "4.9.5",
|
||||
"zustand": "^4.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-katex": "^3.0.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "^2.8.4",
|
||||
"sass": "^1.58.3"
|
||||
}
|
||||
}
|
28
public/logo/zrs_logo_black.svg
Normal file
After Width: | Height: | Size: 12 KiB |
28
public/logo/zrs_logo_white.svg
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |