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
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
*.class
|
|
||||||
*.log
|
# dependencies
|
||||||
*.pyc
|
/node_modules
|
||||||
*.swp
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.atom/
|
*.pem
|
||||||
.buildlog/
|
|
||||||
.history
|
|
||||||
.svn/
|
|
||||||
migrate_working_dir/
|
|
||||||
|
|
||||||
# IntelliJ related
|
# debug
|
||||||
*.iml
|
npm-debug.log*
|
||||||
*.ipr
|
yarn-debug.log*
|
||||||
*.iws
|
yarn-error.log*
|
||||||
.idea/
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# The .vscode folder contains launch configuration and tasks you configure in
|
# local env files
|
||||||
# VS Code which you may wish to be included in version control, so this line
|
.env*.local
|
||||||
# is commented out by default.
|
|
||||||
#.vscode/
|
|
||||||
|
|
||||||
# Flutter/Dart/Pub related
|
# vercel
|
||||||
**/doc/api/
|
.vercel
|
||||||
**/ios/Flutter/.last_build_id
|
|
||||||
.dart_tool/
|
|
||||||
.flutter-plugins
|
|
||||||
.flutter-plugins-dependencies
|
|
||||||
.packages
|
|
||||||
.pub-cache/
|
|
||||||
.pub/
|
|
||||||
/build/
|
|
||||||
|
|
||||||
# Web related
|
# typescript
|
||||||
lib/generated_plugin_registrant.dart
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
# 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
|
|
||||||
|
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 |