diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a440f0e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +backend/app diff --git a/backend/main.go b/backend/main.go index 213b0b6..ccef420 100644 --- a/backend/main.go +++ b/backend/main.go @@ -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) -} \ No newline at end of file + fmt.Println("started and listening on localhost:" + port) + http.ListenAndServe(":"+port, router) +} diff --git a/backend/middleware.go b/backend/middleware.go index fbf2d45..83ac4a3 100644 --- a/backend/middleware.go +++ b/backend/middleware.go @@ -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: ") + } else { + fmt.Println("Body: " + string(bodyBytes)) + } + + // restore the request body for later + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + next.ServeHTTP(w, r) + }) +} diff --git a/backend/run_test_env.sh b/backend/run_test_env.sh new file mode 100755 index 0000000..d597967 --- /dev/null +++ b/backend/run_test_env.sh @@ -0,0 +1 @@ +~/.local/test-envs/hlaskovnik-reimagined/activate.sh diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json new file mode 100644 index 0000000..f8acfe8 --- /dev/null +++ b/frontend/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dart.flutterSdkPath": "/opt/flutter" +} \ No newline at end of file diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 39a0d06..95cc867 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -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(); 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, ); } diff --git a/frontend/lib/main_scaffold.dart b/frontend/lib/main_scaffold.dart index 2e64928..62b0a08 100644 --- a/frontend/lib/main_scaffold.dart +++ b/frontend/lib/main_scaffold.dart @@ -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), ], ), ); diff --git a/frontend/lib/model/quote.dart b/frontend/lib/model/quote.dart index f50c252..7473cd0 100644 --- a/frontend/lib/model/quote.dart +++ b/frontend/lib/model/quote.dart @@ -18,4 +18,9 @@ class Quote{ body: json['body'], ); } + + @override + String toString() { + return "$body - $author"; + } } \ No newline at end of file diff --git a/frontend/lib/pages/add.dart b/frontend/lib/pages/add.dart index fb8be0e..bfb5e98 100644 --- a/frontend/lib/pages/add.dart +++ b/frontend/lib/pages/add.dart @@ -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, diff --git a/frontend/lib/pages/flashcards.dart b/frontend/lib/pages/flashcards.dart index f2973c4..703df0c 100644 --- a/frontend/lib/pages/flashcards.dart +++ b/frontend/lib/pages/flashcards.dart @@ -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(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, + ), + ), + ), ), ], ), diff --git a/frontend/lib/pages/leaderboard.dart b/frontend/lib/pages/leaderboard.dart index 5b99915..96b2cdf 100644 --- a/frontend/lib/pages/leaderboard.dart +++ b/frontend/lib/pages/leaderboard.dart @@ -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(); + 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), + ), + ), + ), + ); + }, + ), + ), + ], + ), + ); } } \ No newline at end of file diff --git a/frontend/lib/pages/list.dart b/frontend/lib/pages/list.dart index fa7487a..8a9ba4c 100644 --- a/frontend/lib/pages/list.dart +++ b/frontend/lib/pages/list.dart @@ -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(); + final themeProvider = context.watch(); 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( - value: 0, - groupValue: provider.filterBy, - onChanged: (value) { - if (value != null) { - provider.setFilterBy(value); - Navigator.pop(context); - } - }, - ), - onTap: () { - provider.setFilterBy(0); - Navigator.pop(context); + RadioGroup( + groupValue: provider.filterBy, + onChanged: (value) { + if (value != null) { + provider.setFilterBy(value); + Navigator.pop(context); + } }, - ), - - ListTile( - title: const Text(Strings.quote), - leading: Radio( - 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(value: 0), + onTap: () { + provider.setFilterBy(0); + Navigator.pop(context); + }, + ), + ListTile( + title: const Text(Strings.quote), + leading: Radio(value: 1), + onTap: () { + provider.setFilterBy(1); + Navigator.pop(context); + }, + ), + ], ), - onTap: () { - provider.setFilterBy(1); - Navigator.pop(context); - }, ), ], - ), + ) ); }, ); diff --git a/frontend/lib/pages/settings.dart b/frontend/lib/pages/settings.dart index 74b01b2..8da43c9 100644 --- a/frontend/lib/pages/settings.dart +++ b/frontend/lib/pages/settings.dart @@ -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(); + return const Center( + child: Card( + child: Row( + children: [ + //Text(Strings.importQuotesFromFile), + //IconButton(onPressed: provider.importQuotesFromFile, icon: icon) + ], + ), + ), + ); } } \ No newline at end of file diff --git a/frontend/lib/provider/quote_provider.dart b/frontend/lib/provider/quote_provider.dart index a21cee0..f9c35b8 100644 --- a/frontend/lib/provider/quote_provider.dart +++ b/frontend/lib/provider/quote_provider.dart @@ -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 _quotes = []; @@ -18,7 +21,7 @@ class QuoteProvider extends ChangeNotifier { List 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> get sortedAuthorCounts { + final counts = {}; + + 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 _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 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!; + } + } } \ No newline at end of file diff --git a/frontend/lib/provider/theme_provider.dart b/frontend/lib/provider/theme_provider.dart new file mode 100644 index 0000000..25e8560 --- /dev/null +++ b/frontend/lib/provider/theme_provider.dart @@ -0,0 +1,34 @@ + +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +Future saveDarkMode(bool isDark) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('darkMode', isDark); +} + +Future 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); + } +} \ No newline at end of file diff --git a/frontend/lib/repository/quote_repository.dart b/frontend/lib/repository/quote_repository.dart index 4440277..1996225 100644 --- a/frontend/lib/repository/quote_repository.dart +++ b/frontend/lib/repository/quote_repository.dart @@ -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'; diff --git a/frontend/lib/values/strings.dart b/frontend/lib/values/strings.dart index 306cd05..c7d8193 100644 --- a/frontend/lib/values/strings.dart +++ b/frontend/lib/values/strings.dart @@ -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"; } \ No newline at end of file diff --git a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..724bb2a 100644 --- a/frontend/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 8c4f5b3..ad637dc 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -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" diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 924c57f..ece9de9 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -37,6 +37,8 @@ dependencies: provider: go_router: http: ^1.2.0 + path_provider: + shared_preferences: ^2.1.1 dev_dependencies: flutter_test: