Merge branch 'dev' into 'master'
Frontend first working version See merge request odweta/hlaskovnik_reimagined!1
This commit is contained in:
@@ -0,0 +1 @@
|
||||
backend/app
|
||||
+11
-8
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
@@ -10,20 +11,22 @@ import (
|
||||
func main() {
|
||||
connectDB()
|
||||
|
||||
r := chi.NewRouter()
|
||||
router := chi.NewRouter()
|
||||
|
||||
r.Use(authMiddleware)
|
||||
router.Use(logMiddleware)
|
||||
router.Use(authMiddleware)
|
||||
|
||||
// routes
|
||||
r.Get("/quotes", getQuotes)
|
||||
r.Post("/quotes", createQuote)
|
||||
r.Put("/quotes/{id}", updateQuote)
|
||||
r.Delete("/quotes/{id}", deleteQuote)
|
||||
router.Get("/quotes", getQuotes)
|
||||
router.Post("/quotes", createQuote)
|
||||
router.Put("/quotes/{id}", updateQuote)
|
||||
router.Delete("/quotes/{id}", deleteQuote)
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
http.ListenAndServe(":"+port, r)
|
||||
}
|
||||
fmt.Println("started and listening on localhost:" + port)
|
||||
http.ListenAndServe(":"+port, router)
|
||||
}
|
||||
|
||||
+36
-1
@@ -1,8 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"crypto/subtle"
|
||||
)
|
||||
|
||||
func authMiddleware(next http.Handler) http.Handler {
|
||||
@@ -18,7 +24,7 @@ func authMiddleware(next http.Handler) http.Handler {
|
||||
|
||||
expectedHeader := "Bearer " + expectedKey
|
||||
|
||||
if authHeader != expectedHeader {
|
||||
if subtle.ConstantTimeCompare([]byte(authHeader), []byte(expectedHeader)) != 1 {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
@@ -26,3 +32,32 @@ func authMiddleware(next http.Handler) http.Handler {
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func logMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
reqMethod := r.Method
|
||||
reqUrl := r.URL.String()
|
||||
reqIp := r.RemoteAddr
|
||||
reqHeaderContentType := r.Header.Get("Content-Type")
|
||||
defer r.Body.Close()
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "[log] Failed to read body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n[%s] %s request on %s\n", reqIp, reqMethod, reqUrl)
|
||||
fmt.Println("Timestamp (UTC): " + time.Now().UTC().String())
|
||||
fmt.Println("Header Content-Type: " + reqHeaderContentType)
|
||||
if len(bodyBytes) == 0 {
|
||||
fmt.Println("Body: <empty>")
|
||||
} else {
|
||||
fmt.Println("Body: " + string(bodyBytes))
|
||||
}
|
||||
|
||||
// restore the request body for later
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
~/.local/test-envs/hlaskovnik-reimagined/activate.sh
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"dart.flutterSdkPath": "/opt/flutter"
|
||||
}
|
||||
+16
-7
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend/provider/theme_provider.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -13,14 +14,21 @@ import 'pages/list.dart';
|
||||
import 'pages/add.dart';
|
||||
import 'pages/flashcards.dart';
|
||||
import 'pages/leaderboard.dart';
|
||||
import 'pages/settings.dart';
|
||||
//import 'pages/settings.dart';
|
||||
|
||||
void main() {
|
||||
runApp(
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => QuoteProvider(),
|
||||
child: HlaskovnikFrontendApp(),
|
||||
),
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => QuoteProvider()
|
||||
),
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => ThemeProvider()
|
||||
)
|
||||
],
|
||||
child: HlaskovnikFrontendApp()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +46,7 @@ class HlaskovnikFrontendApp extends StatelessWidget {
|
||||
GoRoute(path: '/add', builder: (context, state) => AddPage()),
|
||||
GoRoute(path: '/flashcards', builder: (context, state) => FlashcardsPage()),
|
||||
GoRoute(path: '/leaderboard', builder: (context, state) => LeaderboardPage()),
|
||||
GoRoute(path: '/settings', builder: (context, state) => SettingsPage()),
|
||||
//GoRoute(path: '/settings', builder: (context, state) => SettingsPage()),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -46,6 +54,7 @@ class HlaskovnikFrontendApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final themeProvider = context.watch<ThemeProvider>();
|
||||
return MaterialApp.router(
|
||||
title: Strings.appName,
|
||||
theme: ThemeData(
|
||||
@@ -62,7 +71,7 @@ class HlaskovnikFrontendApp extends StatelessWidget {
|
||||
),
|
||||
useMaterial3: true,
|
||||
),
|
||||
themeMode: ThemeMode.system,
|
||||
themeMode: themeProvider.isDark ? ThemeMode.dark : ThemeMode.system,
|
||||
routerConfig: _router,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ class MainScaffold extends StatelessWidget {
|
||||
case '/add': return 1;
|
||||
case '/flashcards': return 2;
|
||||
case '/leaderboard': return 3;
|
||||
case '/settings': return 4;
|
||||
//case '/settings': return 4;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class MainScaffold extends StatelessWidget {
|
||||
case 1: context.go('/add'); break;
|
||||
case 2: context.go('/flashcards'); break;
|
||||
case 3: context.go('/leaderboard'); break;
|
||||
case 4: context.go('/settings'); break;
|
||||
//case 4: context.go('/settings'); break;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -45,7 +45,7 @@ class MainScaffold extends StatelessWidget {
|
||||
BottomNavigationBarItem(icon: Icon(Icons.add), label: Strings.add),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.repeat), label: Strings.flashcards),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.leaderboard), label: Strings.leaderboard),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.settings), label: Strings.settings),
|
||||
//BottomNavigationBarItem(icon: Icon(Icons.settings), label: Strings.settings),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -18,4 +18,9 @@ class Quote{
|
||||
body: json['body'],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "$body - $author";
|
||||
}
|
||||
}
|
||||
@@ -21,18 +21,9 @@ class AddPage extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(Constants.edgeInset),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
Strings.createQuote,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
TextField(
|
||||
autofocus: true,
|
||||
controller: bodyController,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:frontend/provider/quote_provider.dart';
|
||||
import 'package:frontend/model/quote.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -15,6 +14,8 @@ class FlashcardsPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
//return const Center(child: Text('flashcards! flip flip'));
|
||||
final provider = Provider.of<QuoteProvider>(context);
|
||||
if (provider.randomQuoteText == "dummy" ||
|
||||
provider.randomQuoteText == "quote text") { provider.nextRandomQuote(); } // initialize the random quote variable
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(Constants.edgeInset),
|
||||
child: Padding(
|
||||
@@ -22,22 +23,46 @@ class FlashcardsPage extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('insert random quote body here'),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text('flip around'),
|
||||
onPressed: () {
|
||||
// TODO: flip the card
|
||||
},
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(Constants.edgeInset*2),
|
||||
child: Text(provider.randomQuoteText,
|
||||
style: TextStyle(
|
||||
fontSize: 35,
|
||||
),
|
||||
TextButton(
|
||||
child: Text('next card'),
|
||||
onPressed: () {
|
||||
// TODO: next random card
|
||||
},
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(Constants.edgeInset),
|
||||
child: TextButton.icon(
|
||||
onPressed: provider.flipRandomQuote,
|
||||
icon: Icon(Icons.repeat,
|
||||
size: 35,
|
||||
),
|
||||
label: Text(Strings.flipFlashcard,
|
||||
style: TextStyle(
|
||||
fontSize: 35,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(Constants.edgeInset),
|
||||
child: TextButton.icon(
|
||||
onPressed: provider.nextRandomQuote,
|
||||
icon: Icon(Icons.arrow_right_alt,
|
||||
size: 35,
|
||||
),
|
||||
label: Text(Strings.nextFlashcard,
|
||||
style: TextStyle(
|
||||
fontSize: 35,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,10 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend/provider/quote_provider.dart';
|
||||
import 'package:frontend/values/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LeaderboardPage extends StatelessWidget {
|
||||
const LeaderboardPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(child: Text('top 3!'));
|
||||
final provider = context.watch<QuoteProvider>();
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.all(Constants.edgeInset),
|
||||
itemCount: provider.filteredQuotes.length,
|
||||
itemBuilder: (context, index) {
|
||||
return
|
||||
Card(
|
||||
color: provider.getLeaderboardEntryBgColor(context, index),
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: 0,
|
||||
horizontal: Constants.edgeInset,
|
||||
),
|
||||
title: Text(provider.sortedAuthorCounts[index].key,
|
||||
style: TextStyle(
|
||||
color: provider.getLeaderboardEntryFgColor(context, index)
|
||||
),
|
||||
),
|
||||
trailing: Text(provider.sortedAuthorCounts[index].value.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: provider.getLeaderboardEntryFgColor(context, index),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:frontend/provider/quote_provider.dart';
|
||||
import 'package:frontend/model/quote.dart';
|
||||
import 'package:frontend/provider/theme_provider.dart';
|
||||
import 'package:frontend/values/strings.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -13,15 +14,16 @@ class ListPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<QuoteProvider>();
|
||||
final themeProvider = context.watch<ThemeProvider>();
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await provider.readQuotes();
|
||||
},
|
||||
child: _bodyBuilder(context, provider),
|
||||
child: _bodyBuilder(context, provider, themeProvider),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _bodyBuilder(BuildContext context, QuoteProvider provider) {
|
||||
Widget _bodyBuilder(BuildContext context, QuoteProvider provider, ThemeProvider themeProvider) {
|
||||
if (provider.loading) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
@@ -51,7 +53,7 @@ class ListPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
filterBar(context, provider),
|
||||
filterBar(context, provider, themeProvider),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.all(Constants.edgeInset),
|
||||
@@ -220,7 +222,7 @@ class ListPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget filterBar(BuildContext context, QuoteProvider provider) {
|
||||
Widget filterBar(BuildContext context, QuoteProvider provider, ThemeProvider themeProvider) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(Constants.edgeInset),
|
||||
child: Row(
|
||||
@@ -246,7 +248,7 @@ class ListPage extends StatelessWidget {
|
||||
IconButton(
|
||||
icon: Icon(Icons.more_horiz),
|
||||
onPressed: () {
|
||||
_showOptionsBottomSheet(context, provider);
|
||||
_showOptionsBottomSheet(context, provider, themeProvider);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -254,7 +256,7 @@ class ListPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _showOptionsBottomSheet(BuildContext context, QuoteProvider provider) {
|
||||
void _showOptionsBottomSheet(BuildContext context, QuoteProvider provider, ThemeProvider themeProvider) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
@@ -274,8 +276,34 @@ class ListPage extends StatelessWidget {
|
||||
title: Text(Strings.exportSelectedQuotes),
|
||||
trailing: Icon(Icons.import_export),
|
||||
onTap: () {
|
||||
// TODO: trigger an os dialog FileSave
|
||||
},
|
||||
provider.exportQuotesToFile();
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(Strings.fileExportedSuccessfully),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
ListTile(
|
||||
title: Text(Strings.userPrefersDarkMode),
|
||||
onTap: () { themeProvider.setDarkMode(!themeProvider.isDark); },
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: Constants.edgeInset),
|
||||
child: Switch(
|
||||
onChanged: (value) {
|
||||
themeProvider.setDarkMode(value);
|
||||
},
|
||||
value: themeProvider.isDark
|
||||
),
|
||||
),
|
||||
Icon(Icons.dark_mode),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -296,43 +324,37 @@ class ListPage extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text(Strings.author),
|
||||
leading: Radio<int>(
|
||||
value: 0,
|
||||
groupValue: provider.filterBy,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
provider.setFilterBy(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
provider.setFilterBy(0);
|
||||
Navigator.pop(context);
|
||||
RadioGroup<int>(
|
||||
groupValue: provider.filterBy,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
provider.setFilterBy(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: const Text(Strings.quote),
|
||||
leading: Radio<int>(
|
||||
value: 1,
|
||||
groupValue: provider.filterBy,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
provider.setFilterBy(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text(Strings.author),
|
||||
leading: Radio<int>(value: 0),
|
||||
onTap: () {
|
||||
provider.setFilterBy(0);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text(Strings.quote),
|
||||
leading: Radio<int>(value: 1),
|
||||
onTap: () {
|
||||
provider.setFilterBy(1);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
provider.setFilterBy(1);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
// !!! MADE REDUNDANT !!!
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
//import 'package:frontend/provider/quote_provider.dart';
|
||||
|
||||
//import 'package:frontend/values/strings.dart';
|
||||
//import 'package:provider/provider.dart';
|
||||
|
||||
/// settings items
|
||||
/// 1. import quotes from file
|
||||
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(child: Text('let\'s set something up!'));
|
||||
//final provider = context.watch<QuoteProvider>();
|
||||
return const Center(
|
||||
child: Card(
|
||||
child: Row(
|
||||
children: [
|
||||
//Text(Strings.importQuotesFromFile),
|
||||
//IconButton(onPressed: provider.importQuotesFromFile, icon: icon)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
// gets all quotes from the database and returns them in a list
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:frontend/model/quote.dart';
|
||||
import 'package:frontend/repository/quote_repository.dart';
|
||||
import 'dart:math';
|
||||
import 'dart:io';
|
||||
|
||||
class QuoteProvider extends ChangeNotifier {
|
||||
List<Quote> _quotes = [];
|
||||
@@ -18,7 +21,7 @@ class QuoteProvider extends ChangeNotifier {
|
||||
List<Quote> get filteredQuotes {
|
||||
final query = filterController.text.toLowerCase();
|
||||
|
||||
return _quotes.where((quote) {
|
||||
final filtered = _quotes.where((quote) {
|
||||
switch (filterBy) {
|
||||
case 0:
|
||||
return quote.author.toLowerCase().contains(query);
|
||||
@@ -28,6 +31,28 @@ class QuoteProvider extends ChangeNotifier {
|
||||
return true;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
// sort descending
|
||||
filtered.sort((a, b) => b.id.compareTo(a.id));
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
List<MapEntry<String, int>> get sortedAuthorCounts {
|
||||
final counts = <String, int>{};
|
||||
|
||||
for (var quote in filteredQuotes) {
|
||||
counts.update(
|
||||
quote.author,
|
||||
(value) => value + 1,
|
||||
ifAbsent: () => 1,
|
||||
);
|
||||
}
|
||||
|
||||
final list = counts.entries.toList()
|
||||
..sort((a, b) => b.value.compareTo(a.value)); // descending
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
final QuoteRepository repo = QuoteRepository();
|
||||
@@ -39,6 +64,31 @@ class QuoteProvider extends ChangeNotifier {
|
||||
filterController.text = '';
|
||||
}
|
||||
|
||||
Quote randomQuote = Quote(
|
||||
id: 0,
|
||||
body: "quote text",
|
||||
author: "dummy"
|
||||
);
|
||||
bool randomQuoteDeterminator = true;
|
||||
String get randomQuoteText {
|
||||
if (randomQuoteDeterminator) {
|
||||
return randomQuote.body;
|
||||
} else {
|
||||
return " – ${randomQuote.author}";
|
||||
}
|
||||
}
|
||||
|
||||
void flipRandomQuote() {
|
||||
randomQuoteDeterminator = !randomQuoteDeterminator;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void nextRandomQuote() {
|
||||
final random = Random();
|
||||
randomQuote = filteredQuotes[random.nextInt(filteredQuotes.length)];
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void filterQuotes() {
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -118,4 +168,54 @@ class QuoteProvider extends ChangeNotifier {
|
||||
populateAuthors();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
Future<Directory> _getSaveDirectory() async {
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
final path = Platform.isWindows
|
||||
? Platform.environment['USERPROFILE']
|
||||
: Platform.environment['HOME'];
|
||||
|
||||
return Directory(path!);
|
||||
}
|
||||
|
||||
// Mobile fallback
|
||||
return await getApplicationDocumentsDirectory();
|
||||
}
|
||||
|
||||
Future<void> exportQuotesToFile() async {
|
||||
final dir = await _getSaveDirectory();
|
||||
|
||||
final datetime = DateTime.now().toUtc().toString()
|
||||
.replaceAll(" ", "T")
|
||||
.replaceAll(":", "-")
|
||||
.replaceAll(RegExp(r"\..*"), "");
|
||||
|
||||
final file = File('${dir.path}/hlasky_$datetime.txt');
|
||||
await file.writeAsString("${filteredQuotes.join("\n")}\n");
|
||||
}
|
||||
|
||||
Color getLeaderboardEntryBgColor(BuildContext context, int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return Colors.yellow.shade300; // 1st place
|
||||
case 1:
|
||||
return Colors.grey.shade300; // 2nd place
|
||||
case 2:
|
||||
return Colors.brown.shade300; // 3rd place
|
||||
default:
|
||||
return Theme.of(context).cardColor; // default for all other entries
|
||||
}
|
||||
}
|
||||
|
||||
Color getLeaderboardEntryFgColor(BuildContext context, int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
return Colors.black;
|
||||
default:
|
||||
return Theme.of(context).textTheme.bodyMedium!.color!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
Future<void> saveDarkMode(bool isDark) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('darkMode', isDark);
|
||||
}
|
||||
|
||||
Future<bool> loadDarkMode() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool('darkMode') ?? false; // default light
|
||||
}
|
||||
|
||||
class ThemeProvider extends ChangeNotifier {
|
||||
bool _isDark = false;
|
||||
|
||||
bool get isDark => _isDark;
|
||||
|
||||
ThemeProvider() {
|
||||
_load(); // load saved value on startup
|
||||
}
|
||||
|
||||
void _load() async {
|
||||
_isDark = await loadDarkMode();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setDarkMode(bool value) async {
|
||||
_isDark = value;
|
||||
notifyListeners();
|
||||
await saveDarkMode(value);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
// talks to the DB itself
|
||||
// TODO: implement the real one
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:frontend/model/quote.dart';
|
||||
|
||||
class QuoteRepository {
|
||||
static final String _apiBaseUrl = 'https://hlaskovnik-reimagined-latest.onrender.com';
|
||||
//static final String _realApiBaseUrl = 'https://hlaskovnik-reimagined-latest.onrender.com';
|
||||
static final String _testApiBaseUrl = 'http://localhost:8080';
|
||||
static final String _apiBaseUrl = _testApiBaseUrl;
|
||||
static final String _apiKey = "2ogD5kc4sw3JVZoouQSV0w3LGRjSj5IjVFnAnk7IRCIK0ktONCeoOYinbNLSIHuU";
|
||||
static final String _quotesEndpoint = '/quotes';
|
||||
|
||||
|
||||
@@ -19,10 +19,17 @@ class Strings {
|
||||
static const String quote = "Hláška";
|
||||
static const String author = "Autor";
|
||||
|
||||
static const String flipFlashcard = "Otočit flashkartu";
|
||||
static const String nextFlashcard = "Další flashkarta";
|
||||
|
||||
//static const String importQuotesFromFile = "Importovat hlášky ze souboru";
|
||||
static const String fileExportedSuccessfully = "Soubor byl úspěšně vyexportován.";
|
||||
static const String noUndo = "Tuto akci nelze odčinit.";
|
||||
static const String filterQuery = "Filtrovací požadavek";
|
||||
static const String allQuotesCount = "Celkový počet hlášek";
|
||||
static const String filterBy = "Filtrovat podle";
|
||||
static const String exportSelectedQuotes = "Exportovat vybrané hlášky";
|
||||
static const String byAttribute = "podle příznaku";
|
||||
|
||||
static const String userPrefersDarkMode = "Preferuji tmavý režim";
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import shared_preferences_foundation
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
}
|
||||
|
||||
+215
-7
@@ -5,10 +5,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
version: "2.13.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -33,6 +33,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -41,14 +49,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
version: "1.0.9"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -57,6 +73,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -80,14 +112,30 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
go_router:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: "7974313e217a7771557add6ff2238acb63f635317c35fa590d348fb238f00896"
|
||||
sha256: "48fb2f42ad057476fa4b733cb95e9f9ea7b0b010bb349ea491dca7dbdb18ffc4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "17.1.0"
|
||||
version: "17.2.0"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -168,6 +216,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: native_toolchain_c
|
||||
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.17.6"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -176,6 +232,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.3.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -184,6 +248,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.23"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -192,6 +320,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5+1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.5"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.23"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.6"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -277,6 +469,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.11.3 <4.0.0"
|
||||
flutter: ">=3.35.0"
|
||||
flutter: ">=3.38.4"
|
||||
|
||||
@@ -37,6 +37,8 @@ dependencies:
|
||||
provider:
|
||||
go_router:
|
||||
http: ^1.2.0
|
||||
path_provider:
|
||||
shared_preferences: ^2.1.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user