diff --git a/filcnaplo/.metadata b/filcnaplo/.metadata
index ebe4e24..bd1207f 100644
--- a/filcnaplo/.metadata
+++ b/filcnaplo/.metadata
@@ -1,30 +1,30 @@
-# 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: 3c0bee85b8e43b860877922bdc411a7333db4d32
- channel: beta
-
-project_type: app
-
-# Tracks metadata for the flutter migrate command
-migration:
- platforms:
- - platform: root
- create_revision: 3c0bee85b8e43b860877922bdc411a7333db4d32
- base_revision: 3c0bee85b8e43b860877922bdc411a7333db4d32
- - platform: macos
- create_revision: 3c0bee85b8e43b860877922bdc411a7333db4d32
- base_revision: 3c0bee85b8e43b860877922bdc411a7333db4d32
-
- # 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'
+# 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: f468f3366c26a5092eb964a230ce7892fda8f2f8
+ channel: stable
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
+ base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
+ - platform: web
+ create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
+ base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
+
+ # 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'
diff --git a/filcnaplo/android/app/src/main/AndroidManifest.xml b/filcnaplo/android/app/src/main/AndroidManifest.xml
index 2877685..1a19772 100644
--- a/filcnaplo/android/app/src/main/AndroidManifest.xml
+++ b/filcnaplo/android/app/src/main/AndroidManifest.xml
@@ -62,6 +62,7 @@
+
diff --git a/filcnaplo/lib/app.dart b/filcnaplo/lib/app.dart
index efc5414..581b4a0 100644
--- a/filcnaplo/lib/app.dart
+++ b/filcnaplo/lib/app.dart
@@ -13,6 +13,7 @@ import 'package:filcnaplo/theme/theme.dart';
import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:flutter/cupertino.dart';
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:i18n_extension/i18n_widget.dart';
@@ -194,8 +195,19 @@ class App extends StatelessWidget {
}
Route? rootNavigator(RouteSettings route) {
- // if platform == android || platform == ios
- if (Platform.isAndroid || Platform.isIOS) {
+ if (kIsWeb) {
+ switch (route.name) {
+ case "login_back":
+ return CupertinoPageRoute(
+ builder: (context) => const desktop.LoginScreen(back: true));
+ case "login":
+ return _rootRoute(const desktop.LoginScreen());
+ case "navigation":
+ return _rootRoute(const desktop.NavigationScreen());
+ case "login_to_navigation":
+ return desktop.loginRoute(const desktop.NavigationScreen());
+ }
+ } else if (Platform.isAndroid || Platform.isIOS) {
switch (route.name) {
case "login_back":
return CupertinoPageRoute(
diff --git a/filcnaplo/lib/database/init.dart b/filcnaplo/lib/database/init.dart
index a31c6a6..bce6d24 100644
--- a/filcnaplo/lib/database/init.dart
+++ b/filcnaplo/lib/database/init.dart
@@ -5,8 +5,10 @@ import 'dart:io';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/database/struct.dart';
import 'package:filcnaplo/models/settings.dart';
+import 'package:flutter/foundation.dart';
// ignore: depend_on_referenced_packages
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
+import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart';
const settingsDB = DatabaseStruct("settings", {
"language": String, "start_page": int, "rounding": int, "theme": int,
@@ -48,7 +50,9 @@ Future createTable(Database db, DatabaseStruct struct) =>
Future initDB(DatabaseProvider database) async {
Database db;
- if (Platform.isLinux || Platform.isWindows) {
+ if (kIsWeb) {
+ db = await databaseFactoryFfiWeb.openDatabase("app.db");
+ } else if (Platform.isLinux || Platform.isWindows) {
sqfliteFfiInit();
db = await databaseFactoryFfi.openDatabase("app.db");
} else {
diff --git a/filcnaplo/lib/helpers/update_helper.dart b/filcnaplo/lib/helpers/update_helper.dart
index 2199124..5aec576 100644
--- a/filcnaplo/lib/helpers/update_helper.dart
+++ b/filcnaplo/lib/helpers/update_helper.dart
@@ -6,6 +6,7 @@ import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/helpers/storage_helper.dart';
import 'package:filcnaplo/models/release.dart';
import 'package:open_file/open_file.dart';
+import 'package:permission_handler/permission_handler.dart';
enum UpdateState { none, preparing, downloading, installing }
@@ -32,12 +33,17 @@ extension UpdateHelper on Release {
updateCallback(-1, UpdateState.installing);
- var result = await OpenFile.open(apk.path);
+ var permStatus =
+ (await Permission.manageExternalStorage.request().isGranted &&
+ await Permission.requestInstallPackages.request().isGranted);
+ if (permStatus) {
+ var result = await OpenFile.open(apk.path);
- if (result.type != ResultType.done) {
- // ignore: avoid_print
- print("ERROR: installUpdate.openFile: ${result.message}");
- throw result.message;
+ if (result.type != ResultType.done) {
+ // ignore: avoid_print
+ print("ERROR: installUpdate.openFile: ${result.message}");
+ throw result.message;
+ }
}
updateCallback(-1, UpdateState.none);
diff --git a/filcnaplo/lib/main.dart b/filcnaplo/lib/main.dart
index a7ad317..9ceab63 100644
--- a/filcnaplo/lib/main.dart
+++ b/filcnaplo/lib/main.dart
@@ -30,9 +30,10 @@ void main() async {
// Run App
runApp(App(
- database: startup.database,
- settings: startup.settings,
- user: startup.user));
+ database: startup.database,
+ settings: startup.settings,
+ user: startup.user,
+ ));
}
class Startup {
@@ -48,13 +49,17 @@ class Startup {
settings = await database.query.getSettings(database);
user = await database.query.getUsers(settings);
+ late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
// Notifications setup
- initPlatformState();
- FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
- FlutterLocalNotificationsPlugin();
+ if (!kIsWeb) {
+ initPlatformState();
+ flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
+ }
// Get permission to show notifications
- if (Platform.isAndroid) {
+ if (kIsWeb) {
+ // do nothing
+ } else if (Platform.isAndroid) {
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()!
@@ -77,27 +82,35 @@ class Startup {
badge: true,
sound: true,
);
+ } else if (Platform.isLinux) {
+ // no permissions are needed on linux
}
// Platform specific settings
- const DarwinInitializationSettings initializationSettingsDarwin =
- DarwinInitializationSettings(
- requestSoundPermission: true,
- requestBadgePermission: true,
- requestAlertPermission: false,
- );
- const AndroidInitializationSettings initializationSettingsAndroid =
- AndroidInitializationSettings('ic_notification');
- const InitializationSettings initializationSettings =
- InitializationSettings(
- android: initializationSettingsAndroid,
- iOS: initializationSettingsDarwin,
- macOS: initializationSettingsDarwin);
+ if (!kIsWeb) {
+ const DarwinInitializationSettings initializationSettingsDarwin =
+ DarwinInitializationSettings(
+ requestSoundPermission: true,
+ requestBadgePermission: true,
+ requestAlertPermission: false,
+ );
+ const AndroidInitializationSettings initializationSettingsAndroid =
+ AndroidInitializationSettings('ic_notification');
+ const LinuxInitializationSettings initializationSettingsLinux =
+ LinuxInitializationSettings(defaultActionName: 'Open notification');
+ const InitializationSettings initializationSettings =
+ InitializationSettings(
+ android: initializationSettingsAndroid,
+ iOS: initializationSettingsDarwin,
+ macOS: initializationSettingsDarwin,
+ linux: initializationSettingsLinux,
+ );
- // Initialize notifications
- await flutterLocalNotificationsPlugin.initialize(
- initializationSettings,
- );
+ // Initialize notifications
+ await flutterLocalNotificationsPlugin.initialize(
+ initializationSettings,
+ );
+ }
}
}
diff --git a/filcnaplo/pubspec.yaml b/filcnaplo/pubspec.yaml
index 3e9cbc2..ef1dffd 100644
--- a/filcnaplo/pubspec.yaml
+++ b/filcnaplo/pubspec.yaml
@@ -3,7 +3,7 @@ description: "Nem hivatalos e-napló alkalmazás az e-Kréta rendszerhez"
homepage: https://refilc.hu
publish_to: "none"
-version: 4.1.1+215
+version: 4.1.1+216
environment:
sdk: ">=2.17.0 <3.0.0"
@@ -66,6 +66,8 @@ dependencies:
flutter_local_notifications: ^14.1.0
package_info_plus: ^4.0.2
screenshot: ^2.1.0
+ flutter_staggered_grid_view: ^0.7.0
+ sqflite_common_ffi_web: ^0.4.0
dev_dependencies:
flutter_lints: ^2.0.1
diff --git a/filcnaplo/test/widget_test.dart b/filcnaplo/test/widget_test.dart
new file mode 100644
index 0000000..a4b75df
--- /dev/null
+++ b/filcnaplo/test/widget_test.dart
@@ -0,0 +1,30 @@
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility in the flutter_test package. For example, you can send tap and scroll
+// gestures. You can also use WidgetTester to find child widgets in the widget
+// tree, read text, and verify that the values of widget properties are correct.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:filcnaplo/main.dart';
+
+void main() {
+ testWidgets('Counter increments smoke test', (WidgetTester tester) async {
+ // Build our app and trigger a frame.
+ await tester.pumpWidget(const MyApp());
+
+ // Verify that our counter starts at 0.
+ expect(find.text('0'), findsOneWidget);
+ expect(find.text('1'), findsNothing);
+
+ // Tap the '+' icon and trigger a frame.
+ await tester.tap(find.byIcon(Icons.add));
+ await tester.pump();
+
+ // Verify that our counter has incremented.
+ expect(find.text('0'), findsNothing);
+ expect(find.text('1'), findsOneWidget);
+ });
+}
diff --git a/filcnaplo/web/favicon.png b/filcnaplo/web/favicon.png
new file mode 100644
index 0000000..8aaa46a
Binary files /dev/null and b/filcnaplo/web/favicon.png differ
diff --git a/filcnaplo/web/icons/Icon-192.png b/filcnaplo/web/icons/Icon-192.png
new file mode 100644
index 0000000..b749bfe
Binary files /dev/null and b/filcnaplo/web/icons/Icon-192.png differ
diff --git a/filcnaplo/web/icons/Icon-512.png b/filcnaplo/web/icons/Icon-512.png
new file mode 100644
index 0000000..88cfd48
Binary files /dev/null and b/filcnaplo/web/icons/Icon-512.png differ
diff --git a/filcnaplo/web/icons/Icon-maskable-192.png b/filcnaplo/web/icons/Icon-maskable-192.png
new file mode 100644
index 0000000..eb9b4d7
Binary files /dev/null and b/filcnaplo/web/icons/Icon-maskable-192.png differ
diff --git a/filcnaplo/web/icons/Icon-maskable-512.png b/filcnaplo/web/icons/Icon-maskable-512.png
new file mode 100644
index 0000000..d69c566
Binary files /dev/null and b/filcnaplo/web/icons/Icon-maskable-512.png differ
diff --git a/filcnaplo/web/index.html b/filcnaplo/web/index.html
new file mode 100644
index 0000000..d806e3c
--- /dev/null
+++ b/filcnaplo/web/index.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ filcnaplo
+
+
+
+
+
+
+
+
+
+
diff --git a/filcnaplo/web/manifest.json b/filcnaplo/web/manifest.json
new file mode 100644
index 0000000..d43c553
--- /dev/null
+++ b/filcnaplo/web/manifest.json
@@ -0,0 +1,35 @@
+{
+ "name": "filcnaplo",
+ "short_name": "filcnaplo",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#0175C2",
+ "theme_color": "#0175C2",
+ "description": "A new Flutter project.",
+ "orientation": "portrait-primary",
+ "prefer_related_applications": false,
+ "icons": [
+ {
+ "src": "icons/Icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-maskable-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "icons/Icon-maskable-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ]
+}
diff --git a/filcnaplo/windows/.gitignore b/filcnaplo/windows/.gitignore
new file mode 100644
index 0000000..d492d0d
--- /dev/null
+++ b/filcnaplo/windows/.gitignore
@@ -0,0 +1,17 @@
+flutter/ephemeral/
+
+# Visual Studio user-specific files.
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Visual Studio build-related files.
+x64/
+x86/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
diff --git a/filcnaplo/windows/runner/Runner.rc b/filcnaplo/windows/runner/Runner.rc
new file mode 100644
index 0000000..51b3032
--- /dev/null
+++ b/filcnaplo/windows/runner/Runner.rc
@@ -0,0 +1,121 @@
+// Microsoft Visual C++ generated resource script.
+//
+#pragma code_page(65001)
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+// Icon with lowest ID value placed first to ensure application icon
+// remains consistent on all systems.
+IDI_APP_ICON ICON "resources\\app_icon.ico"
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
+#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
+#else
+#define VERSION_AS_NUMBER 1,0,0,0
+#endif
+
+#if defined(FLUTTER_VERSION)
+#define VERSION_AS_STRING FLUTTER_VERSION
+#else
+#define VERSION_AS_STRING "1.0.0"
+#endif
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION VERSION_AS_NUMBER
+ PRODUCTVERSION VERSION_AS_NUMBER
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_APP
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "CompanyName", "hu.refilc" "\0"
+ VALUE "FileDescription", "filcnaplo" "\0"
+ VALUE "FileVersion", VERSION_AS_STRING "\0"
+ VALUE "InternalName", "filcnaplo" "\0"
+ VALUE "LegalCopyright", "Copyright (C) 2023 hu.refilc. All rights reserved." "\0"
+ VALUE "OriginalFilename", "filcnaplo.exe" "\0"
+ VALUE "ProductName", "filcnaplo" "\0"
+ VALUE "ProductVersion", VERSION_AS_STRING "\0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1252
+ END
+END
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
diff --git a/filcnaplo/windows/runner/flutter_window.cpp b/filcnaplo/windows/runner/flutter_window.cpp
new file mode 100644
index 0000000..b25e363
--- /dev/null
+++ b/filcnaplo/windows/runner/flutter_window.cpp
@@ -0,0 +1,66 @@
+#include "flutter_window.h"
+
+#include
+
+#include "flutter/generated_plugin_registrant.h"
+
+FlutterWindow::FlutterWindow(const flutter::DartProject& project)
+ : project_(project) {}
+
+FlutterWindow::~FlutterWindow() {}
+
+bool FlutterWindow::OnCreate() {
+ if (!Win32Window::OnCreate()) {
+ return false;
+ }
+
+ RECT frame = GetClientArea();
+
+ // The size here must match the window dimensions to avoid unnecessary surface
+ // creation / destruction in the startup path.
+ flutter_controller_ = std::make_unique(
+ frame.right - frame.left, frame.bottom - frame.top, project_);
+ // Ensure that basic setup of the controller was successful.
+ if (!flutter_controller_->engine() || !flutter_controller_->view()) {
+ return false;
+ }
+ RegisterPlugins(flutter_controller_->engine());
+ SetChildContent(flutter_controller_->view()->GetNativeWindow());
+
+ flutter_controller_->engine()->SetNextFrameCallback([&]() {
+ this->Show();
+ });
+
+ return true;
+}
+
+void FlutterWindow::OnDestroy() {
+ if (flutter_controller_) {
+ flutter_controller_ = nullptr;
+ }
+
+ Win32Window::OnDestroy();
+}
+
+LRESULT
+FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ // Give Flutter, including plugins, an opportunity to handle window messages.
+ if (flutter_controller_) {
+ std::optional result =
+ flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
+ lparam);
+ if (result) {
+ return *result;
+ }
+ }
+
+ switch (message) {
+ case WM_FONTCHANGE:
+ flutter_controller_->engine()->ReloadSystemFonts();
+ break;
+ }
+
+ return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
+}
diff --git a/filcnaplo/windows/runner/flutter_window.h b/filcnaplo/windows/runner/flutter_window.h
new file mode 100644
index 0000000..6da0652
--- /dev/null
+++ b/filcnaplo/windows/runner/flutter_window.h
@@ -0,0 +1,33 @@
+#ifndef RUNNER_FLUTTER_WINDOW_H_
+#define RUNNER_FLUTTER_WINDOW_H_
+
+#include
+#include
+
+#include
+
+#include "win32_window.h"
+
+// A window that does nothing but host a Flutter view.
+class FlutterWindow : public Win32Window {
+ public:
+ // Creates a new FlutterWindow hosting a Flutter view running |project|.
+ explicit FlutterWindow(const flutter::DartProject& project);
+ virtual ~FlutterWindow();
+
+ protected:
+ // Win32Window:
+ bool OnCreate() override;
+ void OnDestroy() override;
+ LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
+ LPARAM const lparam) noexcept override;
+
+ private:
+ // The project to run.
+ flutter::DartProject project_;
+
+ // The Flutter instance hosted by this window.
+ std::unique_ptr flutter_controller_;
+};
+
+#endif // RUNNER_FLUTTER_WINDOW_H_
diff --git a/filcnaplo/windows/runner/main.cpp b/filcnaplo/windows/runner/main.cpp
new file mode 100644
index 0000000..2847838
--- /dev/null
+++ b/filcnaplo/windows/runner/main.cpp
@@ -0,0 +1,43 @@
+#include
+#include
+#include
+
+#include "flutter_window.h"
+#include "utils.h"
+
+int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
+ _In_ wchar_t *command_line, _In_ int show_command) {
+ // Attach to console when present (e.g., 'flutter run') or create a
+ // new console when running with a debugger.
+ if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
+ CreateAndAttachConsole();
+ }
+
+ // Initialize COM, so that it is available for use in the library and/or
+ // plugins.
+ ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+
+ flutter::DartProject project(L"data");
+
+ std::vector command_line_arguments =
+ GetCommandLineArguments();
+
+ project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
+
+ FlutterWindow window(project);
+ Win32Window::Point origin(10, 10);
+ Win32Window::Size size(800, 720);
+ if (!window.Create(L"reFilc", origin, size)) {
+ return EXIT_FAILURE;
+ }
+ window.SetQuitOnClose(true);
+
+ ::MSG msg;
+ while (::GetMessage(&msg, nullptr, 0, 0)) {
+ ::TranslateMessage(&msg);
+ ::DispatchMessage(&msg);
+ }
+
+ ::CoUninitialize();
+ return EXIT_SUCCESS;
+}
diff --git a/filcnaplo/windows/runner/resource.h b/filcnaplo/windows/runner/resource.h
new file mode 100644
index 0000000..66a65d1
--- /dev/null
+++ b/filcnaplo/windows/runner/resource.h
@@ -0,0 +1,16 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by Runner.rc
+//
+#define IDI_APP_ICON 101
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1001
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/filcnaplo/windows/runner/resources/app_icon.ico b/filcnaplo/windows/runner/resources/app_icon.ico
new file mode 100644
index 0000000..49c0c16
Binary files /dev/null and b/filcnaplo/windows/runner/resources/app_icon.ico differ
diff --git a/filcnaplo/windows/runner/runner.exe.manifest b/filcnaplo/windows/runner/runner.exe.manifest
new file mode 100644
index 0000000..a42ea76
--- /dev/null
+++ b/filcnaplo/windows/runner/runner.exe.manifest
@@ -0,0 +1,20 @@
+
+
+
+
+ PerMonitorV2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/filcnaplo/windows/runner/utils.cpp b/filcnaplo/windows/runner/utils.cpp
new file mode 100644
index 0000000..b2b0873
--- /dev/null
+++ b/filcnaplo/windows/runner/utils.cpp
@@ -0,0 +1,65 @@
+#include "utils.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+void CreateAndAttachConsole() {
+ if (::AllocConsole()) {
+ FILE *unused;
+ if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
+ _dup2(_fileno(stdout), 1);
+ }
+ if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
+ _dup2(_fileno(stdout), 2);
+ }
+ std::ios::sync_with_stdio();
+ FlutterDesktopResyncOutputStreams();
+ }
+}
+
+std::vector GetCommandLineArguments() {
+ // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
+ int argc;
+ wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
+ if (argv == nullptr) {
+ return std::vector();
+ }
+
+ std::vector command_line_arguments;
+
+ // Skip the first argument as it's the binary name.
+ for (int i = 1; i < argc; i++) {
+ command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
+ }
+
+ ::LocalFree(argv);
+
+ return command_line_arguments;
+}
+
+std::string Utf8FromUtf16(const wchar_t* utf16_string) {
+ if (utf16_string == nullptr) {
+ return std::string();
+ }
+ int target_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ -1, nullptr, 0, nullptr, nullptr)
+ -1; // remove the trailing null character
+ int input_length = (int)wcslen(utf16_string);
+ std::string utf8_string;
+ if (target_length <= 0 || target_length > utf8_string.max_size()) {
+ return utf8_string;
+ }
+ utf8_string.resize(target_length);
+ int converted_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ input_length, utf8_string.data(), target_length, nullptr, nullptr);
+ if (converted_length == 0) {
+ return std::string();
+ }
+ return utf8_string;
+}
diff --git a/filcnaplo/windows/runner/utils.h b/filcnaplo/windows/runner/utils.h
new file mode 100644
index 0000000..3879d54
--- /dev/null
+++ b/filcnaplo/windows/runner/utils.h
@@ -0,0 +1,19 @@
+#ifndef RUNNER_UTILS_H_
+#define RUNNER_UTILS_H_
+
+#include
+#include
+
+// Creates a console for the process, and redirects stdout and stderr to
+// it for both the runner and the Flutter library.
+void CreateAndAttachConsole();
+
+// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
+// encoded in UTF-8. Returns an empty std::string on failure.
+std::string Utf8FromUtf16(const wchar_t* utf16_string);
+
+// Gets the command line arguments passed in as a std::vector,
+// encoded in UTF-8. Returns an empty std::vector on failure.
+std::vector GetCommandLineArguments();
+
+#endif // RUNNER_UTILS_H_
diff --git a/filcnaplo/windows/runner/win32_window.cpp b/filcnaplo/windows/runner/win32_window.cpp
new file mode 100644
index 0000000..60608d0
--- /dev/null
+++ b/filcnaplo/windows/runner/win32_window.cpp
@@ -0,0 +1,288 @@
+#include "win32_window.h"
+
+#include
+#include
+
+#include "resource.h"
+
+namespace {
+
+/// Window attribute that enables dark mode window decorations.
+///
+/// Redefined in case the developer's machine has a Windows SDK older than
+/// version 10.0.22000.0.
+/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
+#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
+#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
+#endif
+
+constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
+
+/// Registry key for app theme preference.
+///
+/// A value of 0 indicates apps should use dark mode. A non-zero or missing
+/// value indicates apps should use light mode.
+constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
+constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
+
+// The number of Win32Window objects that currently exist.
+static int g_active_window_count = 0;
+
+using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
+
+// Scale helper to convert logical scaler values to physical using passed in
+// scale factor
+int Scale(int source, double scale_factor) {
+ return static_cast(source * scale_factor);
+}
+
+// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
+// This API is only needed for PerMonitor V1 awareness mode.
+void EnableFullDpiSupportIfAvailable(HWND hwnd) {
+ HMODULE user32_module = LoadLibraryA("User32.dll");
+ if (!user32_module) {
+ return;
+ }
+ auto enable_non_client_dpi_scaling =
+ reinterpret_cast(
+ GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
+ if (enable_non_client_dpi_scaling != nullptr) {
+ enable_non_client_dpi_scaling(hwnd);
+ }
+ FreeLibrary(user32_module);
+}
+
+} // namespace
+
+// Manages the Win32Window's window class registration.
+class WindowClassRegistrar {
+ public:
+ ~WindowClassRegistrar() = default;
+
+ // Returns the singleton registrar instance.
+ static WindowClassRegistrar* GetInstance() {
+ if (!instance_) {
+ instance_ = new WindowClassRegistrar();
+ }
+ return instance_;
+ }
+
+ // Returns the name of the window class, registering the class if it hasn't
+ // previously been registered.
+ const wchar_t* GetWindowClass();
+
+ // Unregisters the window class. Should only be called if there are no
+ // instances of the window.
+ void UnregisterWindowClass();
+
+ private:
+ WindowClassRegistrar() = default;
+
+ static WindowClassRegistrar* instance_;
+
+ bool class_registered_ = false;
+};
+
+WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
+
+const wchar_t* WindowClassRegistrar::GetWindowClass() {
+ if (!class_registered_) {
+ WNDCLASS window_class{};
+ window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
+ window_class.lpszClassName = kWindowClassName;
+ window_class.style = CS_HREDRAW | CS_VREDRAW;
+ window_class.cbClsExtra = 0;
+ window_class.cbWndExtra = 0;
+ window_class.hInstance = GetModuleHandle(nullptr);
+ window_class.hIcon =
+ LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
+ window_class.hbrBackground = 0;
+ window_class.lpszMenuName = nullptr;
+ window_class.lpfnWndProc = Win32Window::WndProc;
+ RegisterClass(&window_class);
+ class_registered_ = true;
+ }
+ return kWindowClassName;
+}
+
+void WindowClassRegistrar::UnregisterWindowClass() {
+ UnregisterClass(kWindowClassName, nullptr);
+ class_registered_ = false;
+}
+
+Win32Window::Win32Window() {
+ ++g_active_window_count;
+}
+
+Win32Window::~Win32Window() {
+ --g_active_window_count;
+ Destroy();
+}
+
+bool Win32Window::Create(const std::wstring& title,
+ const Point& origin,
+ const Size& size) {
+ Destroy();
+
+ const wchar_t* window_class =
+ WindowClassRegistrar::GetInstance()->GetWindowClass();
+
+ const POINT target_point = {static_cast(origin.x),
+ static_cast(origin.y)};
+ HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
+ UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
+ double scale_factor = dpi / 96.0;
+
+ HWND window = CreateWindow(
+ window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
+ Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
+ Scale(size.width, scale_factor), Scale(size.height, scale_factor),
+ nullptr, nullptr, GetModuleHandle(nullptr), this);
+
+ if (!window) {
+ return false;
+ }
+
+ UpdateTheme(window);
+
+ return OnCreate();
+}
+
+bool Win32Window::Show() {
+ return ShowWindow(window_handle_, SW_SHOWNORMAL);
+}
+
+// static
+LRESULT CALLBACK Win32Window::WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ if (message == WM_NCCREATE) {
+ auto window_struct = reinterpret_cast(lparam);
+ SetWindowLongPtr(window, GWLP_USERDATA,
+ reinterpret_cast(window_struct->lpCreateParams));
+
+ auto that = static_cast(window_struct->lpCreateParams);
+ EnableFullDpiSupportIfAvailable(window);
+ that->window_handle_ = window;
+ } else if (Win32Window* that = GetThisFromHandle(window)) {
+ return that->MessageHandler(window, message, wparam, lparam);
+ }
+
+ return DefWindowProc(window, message, wparam, lparam);
+}
+
+LRESULT
+Win32Window::MessageHandler(HWND hwnd,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ switch (message) {
+ case WM_DESTROY:
+ window_handle_ = nullptr;
+ Destroy();
+ if (quit_on_close_) {
+ PostQuitMessage(0);
+ }
+ return 0;
+
+ case WM_DPICHANGED: {
+ auto newRectSize = reinterpret_cast(lparam);
+ LONG newWidth = newRectSize->right - newRectSize->left;
+ LONG newHeight = newRectSize->bottom - newRectSize->top;
+
+ SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
+ newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
+
+ return 0;
+ }
+ case WM_SIZE: {
+ RECT rect = GetClientArea();
+ if (child_content_ != nullptr) {
+ // Size and position the child window.
+ MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
+ rect.bottom - rect.top, TRUE);
+ }
+ return 0;
+ }
+
+ case WM_ACTIVATE:
+ if (child_content_ != nullptr) {
+ SetFocus(child_content_);
+ }
+ return 0;
+
+ case WM_DWMCOLORIZATIONCOLORCHANGED:
+ UpdateTheme(hwnd);
+ return 0;
+ }
+
+ return DefWindowProc(window_handle_, message, wparam, lparam);
+}
+
+void Win32Window::Destroy() {
+ OnDestroy();
+
+ if (window_handle_) {
+ DestroyWindow(window_handle_);
+ window_handle_ = nullptr;
+ }
+ if (g_active_window_count == 0) {
+ WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
+ }
+}
+
+Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
+ return reinterpret_cast(
+ GetWindowLongPtr(window, GWLP_USERDATA));
+}
+
+void Win32Window::SetChildContent(HWND content) {
+ child_content_ = content;
+ SetParent(content, window_handle_);
+ RECT frame = GetClientArea();
+
+ MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
+ frame.bottom - frame.top, true);
+
+ SetFocus(child_content_);
+}
+
+RECT Win32Window::GetClientArea() {
+ RECT frame;
+ GetClientRect(window_handle_, &frame);
+ return frame;
+}
+
+HWND Win32Window::GetHandle() {
+ return window_handle_;
+}
+
+void Win32Window::SetQuitOnClose(bool quit_on_close) {
+ quit_on_close_ = quit_on_close;
+}
+
+bool Win32Window::OnCreate() {
+ // No-op; provided for subclasses.
+ return true;
+}
+
+void Win32Window::OnDestroy() {
+ // No-op; provided for subclasses.
+}
+
+void Win32Window::UpdateTheme(HWND const window) {
+ DWORD light_mode;
+ DWORD light_mode_size = sizeof(light_mode);
+ LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
+ kGetPreferredBrightnessRegValue,
+ RRF_RT_REG_DWORD, nullptr, &light_mode,
+ &light_mode_size);
+
+ if (result == ERROR_SUCCESS) {
+ BOOL enable_dark_mode = light_mode == 0;
+ DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
+ &enable_dark_mode, sizeof(enable_dark_mode));
+ }
+}
diff --git a/filcnaplo/windows/runner/win32_window.h b/filcnaplo/windows/runner/win32_window.h
new file mode 100644
index 0000000..e901dde
--- /dev/null
+++ b/filcnaplo/windows/runner/win32_window.h
@@ -0,0 +1,102 @@
+#ifndef RUNNER_WIN32_WINDOW_H_
+#define RUNNER_WIN32_WINDOW_H_
+
+#include
+
+#include
+#include
+#include
+
+// A class abstraction for a high DPI-aware Win32 Window. Intended to be
+// inherited from by classes that wish to specialize with custom
+// rendering and input handling
+class Win32Window {
+ public:
+ struct Point {
+ unsigned int x;
+ unsigned int y;
+ Point(unsigned int x, unsigned int y) : x(x), y(y) {}
+ };
+
+ struct Size {
+ unsigned int width;
+ unsigned int height;
+ Size(unsigned int width, unsigned int height)
+ : width(width), height(height) {}
+ };
+
+ Win32Window();
+ virtual ~Win32Window();
+
+ // Creates a win32 window with |title| that is positioned and sized using
+ // |origin| and |size|. New windows are created on the default monitor. Window
+ // sizes are specified to the OS in physical pixels, hence to ensure a
+ // consistent size this function will scale the inputted width and height as
+ // as appropriate for the default monitor. The window is invisible until
+ // |Show| is called. Returns true if the window was created successfully.
+ bool Create(const std::wstring& title, const Point& origin, const Size& size);
+
+ // Show the current window. Returns true if the window was successfully shown.
+ bool Show();
+
+ // Release OS resources associated with window.
+ void Destroy();
+
+ // Inserts |content| into the window tree.
+ void SetChildContent(HWND content);
+
+ // Returns the backing Window handle to enable clients to set icon and other
+ // window properties. Returns nullptr if the window has been destroyed.
+ HWND GetHandle();
+
+ // If true, closing this window will quit the application.
+ void SetQuitOnClose(bool quit_on_close);
+
+ // Return a RECT representing the bounds of the current client area.
+ RECT GetClientArea();
+
+ protected:
+ // Processes and route salient window messages for mouse handling,
+ // size change and DPI. Delegates handling of these to member overloads that
+ // inheriting classes can handle.
+ virtual LRESULT MessageHandler(HWND window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Called when CreateAndShow is called, allowing subclass window-related
+ // setup. Subclasses should return false if setup fails.
+ virtual bool OnCreate();
+
+ // Called when Destroy is called.
+ virtual void OnDestroy();
+
+ private:
+ friend class WindowClassRegistrar;
+
+ // OS callback called by message pump. Handles the WM_NCCREATE message which
+ // is passed when the non-client area is being created and enables automatic
+ // non-client DPI scaling so that the non-client area automatically
+ // responds to changes in DPI. All other messages are handled by
+ // MessageHandler.
+ static LRESULT CALLBACK WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Retrieves a class instance pointer for |window|
+ static Win32Window* GetThisFromHandle(HWND const window) noexcept;
+
+ // Update the window frame's theme to match the system theme.
+ static void UpdateTheme(HWND const window);
+
+ bool quit_on_close_ = false;
+
+ // window handle for top level window.
+ HWND window_handle_ = nullptr;
+
+ // window handle for hosted content.
+ HWND child_content_ = nullptr;
+};
+
+#endif // RUNNER_WIN32_WINDOW_H_
diff --git a/filcnaplo_desktop_ui/lib/pages/home/home_page.dart b/filcnaplo_desktop_ui/lib/pages/home/home_page.dart
index d0cc653..edfc433 100644
--- a/filcnaplo_desktop_ui/lib/pages/home/home_page.dart
+++ b/filcnaplo_desktop_ui/lib/pages/home/home_page.dart
@@ -1,7 +1,7 @@
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/ui/date_widget.dart';
-import 'package:filcnaplo_desktop_ui/common/filter_bar.dart';
+import 'package:filcnaplo_mobile_ui/common/filter_bar.dart';
import 'package:flutter/material.dart';
import 'package:animated_list_plus/animated_list_plus.dart';
import 'package:provider/provider.dart';
@@ -16,7 +16,8 @@ class HomePage extends StatefulWidget {
State createState() => _HomePageState();
}
-class _HomePageState extends State with SingleTickerProviderStateMixin {
+class _HomePageState extends State
+ with SingleTickerProviderStateMixin {
late UserProvider user;
late SettingsProvider settings;
@@ -41,11 +42,15 @@ class _HomePageState extends State with SingleTickerProviderStateMixin
user = Provider.of(context, listen: false);
DateTime now = DateTime.now();
- if (now.isBefore(DateTime(now.year, DateTime.august, 31)) && now.isAfter(DateTime(now.year, DateTime.june, 14))) {
+ if (now.isBefore(DateTime(now.year, DateTime.august, 31)) &&
+ now.isAfter(DateTime(now.year, DateTime.june, 14))) {
greeting = "goodrest";
- } else if (now.month == user.student?.birth.month && now.day == user.student?.birth.day) {
+ } else if (now.month == user.student?.birth.month &&
+ now.day == user.student?.birth.day) {
greeting = "happybirthday";
- } else if (now.month == DateTime.december && now.day >= 24 && now.day <= 26) {
+ } else if (now.month == DateTime.december &&
+ now.day >= 24 &&
+ now.day <= 26) {
greeting = "merryxmas";
} else if (now.month == DateTime.january && now.day == 1) {
greeting = "happynewyear";
@@ -81,7 +86,8 @@ class _HomePageState extends State with SingleTickerProviderStateMixin
children: [
// Greeting
Padding(
- padding: const EdgeInsets.only(left: 32.0, top: 24.0, bottom: 12.0),
+ padding: const EdgeInsets.only(
+ left: 32.0, top: 24.0, bottom: 12.0),
child: Text(
greeting.i18n.fill([firstName]),
overflow: TextOverflow.fade,
@@ -107,8 +113,10 @@ class _HomePageState extends State with SingleTickerProviderStateMixin
int selectedPage = _pageController.page!.round();
if (i == selectedPage) return;
- if (_pageController.page?.roundToDouble() != _pageController.page) {
- _pageController.animateToPage(i, curve: Curves.easeIn, duration: kTabScrollDuration);
+ if (_pageController.page?.roundToDouble() !=
+ _pageController.page) {
+ _pageController.animateToPage(i,
+ curve: Curves.easeIn, duration: kTabScrollDuration);
return;
}
@@ -132,27 +140,34 @@ class _HomePageState extends State with SingleTickerProviderStateMixin
(BuildContext context, int index) {
return FutureBuilder>(
key: ValueKey(listOrder[index]),
- future: getFilterWidgets(homeFilters[index], context: context),
- builder: (context, dateWidgets) => dateWidgets.data != null
+ future: getFilterWidgets(homeFilters[index],
+ context: context),
+ builder: (context, dateWidgets) => dateWidgets.data !=
+ null
? ImplicitlyAnimatedList(
- items: sortDateWidgets(context, dateWidgets: dateWidgets.data!),
+ items: sortDateWidgets(context,
+ dateWidgets: dateWidgets.data!),
itemBuilder: filterItemBuilder,
spawnIsolate: false,
areItemsTheSame: (a, b) => a.key == b.key,
- physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
- padding: const EdgeInsets.symmetric(horizontal: 24.0),
+ physics: const BouncingScrollPhysics(
+ parent: AlwaysScrollableScrollPhysics()),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 24.0),
)
: Container(),
);
},
childCount: 4,
findChildIndexCallback: (Key key) {
- final ValueKey valueKey = key as ValueKey;
+ final ValueKey valueKey =
+ key as ValueKey;
final String data = valueKey.value;
return listOrder.indexOf(data);
},
),
- physics: const PageScrollPhysics().applyTo(const BouncingScrollPhysics()),
+ physics: const PageScrollPhysics()
+ .applyTo(const BouncingScrollPhysics()),
),
),
],
diff --git a/filcnaplo_desktop_ui/lib/pages/timetable/timetable_page.dart b/filcnaplo_desktop_ui/lib/pages/timetable/timetable_page.dart
index b673689..8d6e5fb 100644
--- a/filcnaplo_desktop_ui/lib/pages/timetable/timetable_page.dart
+++ b/filcnaplo_desktop_ui/lib/pages/timetable/timetable_page.dart
@@ -23,12 +23,14 @@ import 'timetable_page.i18n.dart';
// todo: "fix" overflow (priority: -1)
class TimetablePage extends StatefulWidget {
- const TimetablePage({Key? key, this.initialDay, this.initialWeek}) : super(key: key);
+ const TimetablePage({Key? key, this.initialDay, this.initialWeek})
+ : super(key: key);
final DateTime? initialDay;
final Week? initialWeek;
- static void jump(BuildContext context, {Week? week, DateTime? day, Lesson? lesson}) {
+ static void jump(BuildContext context,
+ {Week? week, DateTime? day, Lesson? lesson}) {
// Go to timetable page with arguments
// NavigationScreen.of(context)?.customRoute(navigationPageRoute((context) => TimetablePage(
// initialDay: lesson?.date ?? day,
@@ -49,7 +51,8 @@ class TimetablePage extends StatefulWidget {
_TimetablePageState createState() => _TimetablePageState();
}
-class _TimetablePageState extends State with TickerProviderStateMixin {
+class _TimetablePageState extends State
+ with TickerProviderStateMixin {
late UserProvider user;
late TimetableProvider timetableProvider;
late UpdateProvider updateProvider;
@@ -60,7 +63,9 @@ class _TimetablePageState extends State with TickerProviderStateM
int _getDayIndex(DateTime date) {
int index = 0;
- if (_controller.days == null || (_controller.days?.isEmpty ?? true)) return index;
+ if (_controller.days == null || (_controller.days?.isEmpty ?? true)) {
+ return index;
+ }
// find the first day with upcoming lessons
index = _controller.days!.indexWhere((day) => day.last.end.isAfter(date));
@@ -94,11 +99,14 @@ class _TimetablePageState extends State with TickerProviderStateM
_tabController = TabController(
length: _controller.days!.length,
vsync: this,
- initialIndex: min(_tabController.index, max(_controller.days!.length - 1, 0)),
+ initialIndex:
+ min(_tabController.index, max(_controller.days!.length - 1, 0)),
);
- if (initial || _controller.previousWeekId != _controller.currentWeekId) {
- _tabController.animateTo(_getDayIndex(widget.initialDay ?? DateTime.now()));
+ if (initial ||
+ _controller.previousWeekId != _controller.currentWeekId) {
+ _tabController
+ .animateTo(_getDayIndex(widget.initialDay ?? DateTime.now()));
}
initial = false;
@@ -111,7 +119,8 @@ class _TimetablePageState extends State with TickerProviderStateM
if (widget.initialWeek != null) {
_controller.jump(widget.initialWeek!, context: context, initial: true);
} else {
- _controller.jump(_controller.currentWeek, context: context, initial: true, skip: true);
+ _controller.jump(_controller.currentWeek,
+ context: context, initial: true, skip: true);
}
}
// Listen for user changes
@@ -131,7 +140,8 @@ class _TimetablePageState extends State with TickerProviderStateM
// Sometimes when changing weeks really fast,
// controller.days might be null or won't include index
try {
- return DateFormat("EEEE", I18n.of(context).locale.languageCode).format(_controller.days![index].first.date);
+ return DateFormat("EEEE", I18n.of(context).locale.languageCode)
+ .format(_controller.days![index].first.date);
} catch (e) {
return "timetable".i18n;
}
@@ -181,9 +191,14 @@ class _TimetablePageState extends State with TickerProviderStateM
children: [
// Day Title
Padding(
- padding: const EdgeInsets.only(left: 24.0, right: 28.0, top: 18.0, bottom: 8.0),
+ padding: const EdgeInsets.only(
+ left: 24.0,
+ right: 28.0,
+ top: 18.0,
+ bottom: 8.0),
child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ mainAxisAlignment:
+ MainAxisAlignment.spaceBetween,
children: [
Text(
dayTitle(tab).capital(),
@@ -193,9 +208,13 @@ class _TimetablePageState extends State with TickerProviderStateM
),
),
Text(
- "${_controller.days![tab].first.date.day}".padLeft(2, '0') + ".",
+ "${_controller.days![tab].first.date.day}"
+ .padLeft(2, '0') +
+ ".",
style: TextStyle(
- color: AppColors.of(context).text.withOpacity(.5),
+ color: AppColors.of(context)
+ .text
+ .withOpacity(.5),
fontWeight: FontWeight.w500,
),
),
@@ -208,35 +227,59 @@ class _TimetablePageState extends State with TickerProviderStateM
child: ListView.builder(
padding: EdgeInsets.zero,
physics: const BouncingScrollPhysics(),
- itemCount: _controller.days![tab].length + 2,
+ itemCount:
+ _controller.days![tab].length + 2,
itemBuilder: (context, index) {
- if (_controller.days == null) return Container();
+ if (_controller.days == null) {
+ return Container();
+ }
// Header
if (index == 0) {
return const Padding(
- padding: EdgeInsets.only(top: 8.0, left: 24.0, right: 24.0),
- child: PanelHeader(padding: EdgeInsets.only(top: 12.0)),
+ padding: EdgeInsets.only(
+ top: 8.0,
+ left: 24.0,
+ right: 24.0),
+ child: PanelHeader(
+ padding: EdgeInsets.only(
+ top: 12.0)),
);
}
// Footer
- if (index == _controller.days![tab].length + 1) {
+ if (index ==
+ _controller.days![tab].length +
+ 1) {
return const Padding(
- padding: EdgeInsets.only(bottom: 8.0, left: 24.0, right: 24.0),
- child: PanelFooter(padding: EdgeInsets.only(top: 12.0)),
+ padding: EdgeInsets.only(
+ bottom: 8.0,
+ left: 24.0,
+ right: 24.0),
+ child: PanelFooter(
+ padding: EdgeInsets.only(
+ top: 12.0)),
);
}
// Body
- final Lesson lesson = _controller.days![tab][index - 1];
- final bool swapDescDay = _controller.days![tab].map((l) => l.swapDesc ? 1 : 0).reduce((a, b) => a + b) >=
- _controller.days![tab].length * .5;
+ final Lesson lesson =
+ _controller.days![tab][index - 1];
+ final bool swapDescDay = _controller
+ .days![tab]
+ .map(
+ (l) => l.swapDesc ? 1 : 0)
+ .reduce((a, b) => a + b) >=
+ _controller.days![tab].length *
+ .5;
return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 24.0),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 24.0),
child: PanelBody(
- padding: const EdgeInsets.symmetric(horizontal: 10.0),
+ padding:
+ const EdgeInsets.symmetric(
+ horizontal: 10.0),
child: LessonViewable(
lesson,
swapDesc: swapDescDay,
@@ -264,7 +307,8 @@ class _TimetablePageState extends State with TickerProviderStateM
),
),
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
+ padding:
+ const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -285,7 +329,10 @@ class _TimetablePageState extends State with TickerProviderStateM
onTap: () => setState(() {
_controller.current();
if (mounted) {
- _controller.jump(_controller.currentWeek, context: context, loader: _controller.currentWeekId != _controller.previousWeekId);
+ _controller.jump(_controller.currentWeek,
+ context: context,
+ loader: _controller.currentWeekId !=
+ _controller.previousWeekId);
}
}),
child: Padding(
@@ -295,12 +342,22 @@ class _TimetablePageState extends State with TickerProviderStateM
"week".i18n +
" (" +
// Week start
- DateFormat((_controller.currentWeek.start.year != DateTime.now().year ? "yy. " : "") + "MMM d.",
+ DateFormat(
+ (_controller.currentWeek.start.year !=
+ DateTime.now().year
+ ? "yy. "
+ : "") +
+ "MMM d.",
I18n.of(context).locale.languageCode)
.format(_controller.currentWeek.start) +
" - " +
// Week end
- DateFormat((_controller.currentWeek.start.year != DateTime.now().year ? "yy. " : "") + "MMM d.",
+ DateFormat(
+ (_controller.currentWeek.start.year !=
+ DateTime.now().year
+ ? "yy. "
+ : "") +
+ "MMM d.",
I18n.of(context).locale.languageCode)
.format(_controller.currentWeek.end) +
")",
diff --git a/filcnaplo_desktop_ui/lib/screens/navigation/sidebar.dart b/filcnaplo_desktop_ui/lib/screens/navigation/sidebar.dart
index 0815557..1c0f5fa 100644
--- a/filcnaplo_desktop_ui/lib/screens/navigation/sidebar.dart
+++ b/filcnaplo_desktop_ui/lib/screens/navigation/sidebar.dart
@@ -6,6 +6,7 @@ import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/utils/color.dart';
import 'package:filcnaplo_desktop_ui/common/panel_button.dart';
import 'package:filcnaplo_desktop_ui/common/profile_image.dart';
+import 'package:filcnaplo_desktop_ui/screens/navigation/sidebar.i18n.dart';
import 'package:filcnaplo_desktop_ui/screens/navigation/sidebar_action.dart';
import 'package:filcnaplo_desktop_ui/screens/settings/settings_screen.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/accounts/account_tile.dart';
@@ -25,7 +26,12 @@ import 'package:provider/provider.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
class Sidebar extends StatefulWidget {
- const Sidebar({Key? key, required this.navigator, required this.onRouteChange, this.selected = "home"}) : super(key: key);
+ const Sidebar(
+ {Key? key,
+ required this.navigator,
+ required this.onRouteChange,
+ this.selected = "home"})
+ : super(key: key);
final NavigatorState navigator;
final String selected;
@@ -71,12 +77,12 @@ class _SidebarState extends State {
if (!settings.presentationMode) {
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
} else {
- firstName = "Béla";
+ firstName = "János";
}
List pageWidgets = [
SidebarAction(
- title: const Text("Home"),
+ title: Text("Home".i18n),
icon: const Icon(FilcIcons.home),
selected: widget.selected == "home",
onTap: () {
@@ -87,7 +93,7 @@ class _SidebarState extends State {
},
),
SidebarAction(
- title: const Text("Grades"),
+ title: Text("Grades".i18n),
icon: const Icon(FeatherIcons.bookmark),
selected: widget.selected == "grades",
onTap: () {
@@ -98,7 +104,7 @@ class _SidebarState extends State {
},
),
SidebarAction(
- title: const Text("Timetable"),
+ title: Text("Timetable".i18n),
icon: const Icon(FeatherIcons.calendar),
selected: widget.selected == "timetable",
onTap: () {
@@ -109,7 +115,7 @@ class _SidebarState extends State {
},
),
SidebarAction(
- title: const Text("Messages"),
+ title: Text("Messages".i18n),
icon: const Icon(FeatherIcons.messageSquare),
selected: widget.selected == "messages",
onTap: () {
@@ -120,7 +126,7 @@ class _SidebarState extends State {
},
),
SidebarAction(
- title: const Text("Absences"),
+ title: Text("Absences".i18n),
icon: const Icon(FeatherIcons.clock),
selected: widget.selected == "absences",
onTap: () {
@@ -134,12 +140,15 @@ class _SidebarState extends State {
List bottomActions = [
SidebarAction(
- title: const Text("Settings"),
+ title: Text("Settings".i18n),
selected: true,
icon: const Icon(FeatherIcons.settings),
onTap: () {
if (topNav != "settings") {
- widget.navigator.push(CupertinoPageRoute(builder: (context) => const SettingsScreen())).then((value) => topNav = "");
+ widget.navigator
+ .push(CupertinoPageRoute(
+ builder: (context) => const SettingsScreen()))
+ .then((value) => topNav = "");
topNav = "settings";
}
},
@@ -159,7 +168,7 @@ class _SidebarState extends State {
onPressed: () {
Navigator.of(context).pushNamed("login_back");
},
- title: const Text("Add User"),
+ title: Text("adduser".i18n),
leading: const Icon(FeatherIcons.userPlus),
),
PanelButton(
@@ -169,17 +178,20 @@ class _SidebarState extends State {
// Delete User
user.removeUser(userId);
- await Provider.of(context, listen: false).store.removeUser(userId);
+ await Provider.of(context, listen: false)
+ .store
+ .removeUser(userId);
// If no other Users left, go back to LoginScreen
if (user.getUsers().isNotEmpty) {
user.setUser(user.getUsers().first.id);
restore().then((_) => user.setUser(user.getUsers().first.id));
} else {
- Navigator.of(context).pushNamedAndRemoveUntil("login", (_) => false);
+ Navigator.of(context)
+ .pushNamedAndRemoveUntil("login", (_) => false);
}
},
- title: const Text("Log Out"),
+ title: Text("logout".i18n),
leading: Icon(FeatherIcons.logOut, color: AppColors.of(context).red),
),
];
@@ -190,49 +202,77 @@ class _SidebarState extends State {
child: Column(
children: [
Padding(
- padding: const EdgeInsets.only(left: 18.0, top: 18.0, bottom: 24.0, right: 8.0),
- child: Row(
- children: [
- Padding(
- padding: const EdgeInsets.only(right: 12.0),
- child: ProfileImage(
- name: firstName,
- radius: 18.0,
- backgroundColor:
- !settings.presentationMode ? ColorUtils.stringToColor(user.name ?? "?") : Theme.of(context).colorScheme.secondary,
- ),
- ),
- Expanded(
- child: Text(
- firstName,
- style: const TextStyle(
- fontSize: 16.0,
- fontWeight: FontWeight.w600,
+ padding: const EdgeInsets.only(
+ left: 12.0,
+ top: 18.0,
+ bottom: 24.0,
+ right: 12.0,
+ ),
+ child: InkWell(
+ customBorder: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10)),
+ onTap: () {
+ setState(() {
+ expandAccount = !expandAccount;
+ });
+ },
+ child: Row(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ right: 12.0,
+ left: 5.0,
+ top: 5.0,
+ bottom: 5.0,
+ ),
+ child: ProfileImage(
+ name: firstName,
+ radius: 18.0,
+ backgroundColor: Theme.of(context)
+ .colorScheme
+ .primary, //!settings.presentationMode
+ // ? ColorUtils.stringToColor(user.name ?? "?")
+ // : Theme.of(context).colorScheme.secondary,
),
),
- ),
- PageTransitionSwitcher(
- transitionBuilder: (child, primaryAnimation, secondaryAnimation) {
- return FadeThroughTransition(
- fillColor: Colors.transparent,
- animation: primaryAnimation,
- secondaryAnimation: secondaryAnimation,
- child: child,
- );
- },
- child: IconButton(
- key: Key(expandAccount ? "accounts" : "pages"),
- icon: Icon(expandAccount ? FeatherIcons.chevronDown : FeatherIcons.chevronRight),
- padding: EdgeInsets.zero,
- splashRadius: 20.0,
- onPressed: () {
- setState(() {
- expandAccount = !expandAccount;
- });
- },
+ Expanded(
+ child: Text(
+ firstName,
+ style: const TextStyle(
+ fontSize: 16.0,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
),
- ),
- ],
+ PageTransitionSwitcher(
+ transitionBuilder:
+ (child, primaryAnimation, secondaryAnimation) {
+ return FadeThroughTransition(
+ fillColor: Colors.transparent,
+ animation: primaryAnimation,
+ secondaryAnimation: secondaryAnimation,
+ child: child,
+ );
+ },
+ child: IconButton(
+ key: Key(expandAccount ? "accounts" : "pages"),
+ icon: Icon(expandAccount
+ ? FeatherIcons.chevronDown
+ : FeatherIcons.chevronRight),
+ padding: EdgeInsets.zero,
+ onPressed: () {
+ setState(() {
+ expandAccount = !expandAccount;
+ });
+ },
+ splashColor: const Color(0x00000000),
+ focusColor: const Color(0x00000000),
+ hoverColor: const Color(0x00000000),
+ highlightColor: const Color(0x00000000),
+ ),
+ ),
+ ],
+ ),
),
),
@@ -282,15 +322,19 @@ class _SidebarState extends State {
if (!settings.presentationMode) {
_firstName = _nameParts.length > 1 ? _nameParts[1] : _nameParts[0];
} else {
- _firstName = "Béla";
+ _firstName = "János";
}
accountTiles.add(AccountTile(
- name: Text(!settings.presentationMode ? account.name : "Béla", style: const TextStyle(fontWeight: FontWeight.w500)),
- username: Text(!settings.presentationMode ? account.username : "72469696969"),
+ name: Text(!settings.presentationMode ? account.name : "János",
+ style: const TextStyle(fontWeight: FontWeight.w500)),
+ username:
+ Text(!settings.presentationMode ? account.username : "72469696969"),
profileImage: ProfileImage(
name: _firstName,
- backgroundColor: !settings.presentationMode ? ColorUtils.stringToColor(account.name) : Theme.of(context).colorScheme.secondary,
+ backgroundColor: !settings.presentationMode
+ ? ColorUtils.stringToColor(account.name)
+ : Theme.of(context).colorScheme.secondary,
role: account.role,
),
onTap: () {
diff --git a/filcnaplo_desktop_ui/lib/screens/navigation/sidebar.i18n.dart b/filcnaplo_desktop_ui/lib/screens/navigation/sidebar.i18n.dart
new file mode 100644
index 0000000..43cfb8a
--- /dev/null
+++ b/filcnaplo_desktop_ui/lib/screens/navigation/sidebar.i18n.dart
@@ -0,0 +1,42 @@
+import 'package:i18n_extension/i18n_extension.dart';
+
+extension SettingsLocalization on String {
+ static final _t = Translations.byLocale("hu_hu") +
+ {
+ "en_en": {
+ "Home": "Home",
+ "Grades": "Grades",
+ "Timetable": "Timetable",
+ "Messages": "Messages",
+ "Absences": "Absences",
+ "Settings": "Settings",
+ "adduser": "Add User",
+ "logout": "Log Out",
+ },
+ "hu_hu": {
+ "Home": "Kezdőlap",
+ "Grades": "Jegyek",
+ "Timetable": "Órarend",
+ "Messages": "Üzenetek",
+ "Absences": "Hiányzások",
+ "Settings": "Beállítások",
+ "adduser": "Fiók hozzáadása",
+ "logout": "Kilépés",
+ },
+ "de_de": {
+ "Home": "Zuhause",
+ "Grades": "Noten",
+ "Timetable": "Zeitplan",
+ "Messages": "Mitteilungen",
+ "Absences": "Fehlen",
+ "Settings": "Einstellungen",
+ "adduser": "Benutzer hinzufügen",
+ "logout": "Abmelden",
+ },
+ };
+
+ String get i18n => localize(this, _t);
+ String fill(List