From 5523a2a919dfc17a31e0ab575c2d9b79d5fc918b Mon Sep 17 00:00:00 2001 From: Kima Date: Mon, 31 Jul 2023 23:20:30 +0200 Subject: [PATCH] fixed lot of ui things --- filcnaplo/.metadata | 63 +- filcnaplo/pubspec.yaml | 1 + filcnaplo/test/widget_test.dart | 30 + filcnaplo/windows/.gitignore | 17 + filcnaplo/windows/runner/Runner.rc | 121 ++ filcnaplo/windows/runner/flutter_window.cpp | 66 + filcnaplo/windows/runner/flutter_window.h | 33 + filcnaplo/windows/runner/main.cpp | 43 + filcnaplo/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 25735 bytes filcnaplo/windows/runner/runner.exe.manifest | 20 + filcnaplo/windows/runner/utils.cpp | 65 + filcnaplo/windows/runner/utils.h | 19 + filcnaplo/windows/runner/win32_window.cpp | 288 ++++ filcnaplo/windows/runner/win32_window.h | 102 ++ .../lib/pages/home/home_page.dart | 45 +- .../lib/pages/timetable/timetable_page.dart | 115 +- .../lib/screens/navigation/sidebar.dart | 158 +- .../lib/screens/navigation/sidebar.i18n.dart | 42 + .../lib/screens/settings/settings_screen.dart | 1280 +++++++++-------- filcnaplo_desktop_ui/pubspec.yaml | 1 + .../absence_group/absence_group_tile.dart | 37 +- 22 files changed, 1812 insertions(+), 750 deletions(-) create mode 100644 filcnaplo/test/widget_test.dart create mode 100644 filcnaplo/windows/.gitignore create mode 100644 filcnaplo/windows/runner/Runner.rc create mode 100644 filcnaplo/windows/runner/flutter_window.cpp create mode 100644 filcnaplo/windows/runner/flutter_window.h create mode 100644 filcnaplo/windows/runner/main.cpp create mode 100644 filcnaplo/windows/runner/resource.h create mode 100644 filcnaplo/windows/runner/resources/app_icon.ico create mode 100644 filcnaplo/windows/runner/runner.exe.manifest create mode 100644 filcnaplo/windows/runner/utils.cpp create mode 100644 filcnaplo/windows/runner/utils.h create mode 100644 filcnaplo/windows/runner/win32_window.cpp create mode 100644 filcnaplo/windows/runner/win32_window.h create mode 100644 filcnaplo_desktop_ui/lib/screens/navigation/sidebar.i18n.dart diff --git a/filcnaplo/.metadata b/filcnaplo/.metadata index ebe4e24..6b7daaf 100644 --- a/filcnaplo/.metadata +++ b/filcnaplo/.metadata @@ -1,30 +1,33 @@ -# 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: linux + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: windows + 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/pubspec.yaml b/filcnaplo/pubspec.yaml index 183792a..5cd1e2d 100644 --- a/filcnaplo/pubspec.yaml +++ b/filcnaplo/pubspec.yaml @@ -66,6 +66,7 @@ dependencies: flutter_local_notifications: ^14.1.0 package_info_plus: ^4.0.2 screenshot: ^2.1.0 + flutter_staggered_grid_view: ^0.7.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/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 0000000000000000000000000000000000000000..49c0c16b1a866dacc4af8c0864b25b0a60ad6998 GIT binary patch literal 25735 zcmV)#K##uw00962000000096X0C8jh02TlM0EtjeM-2)Z3IG5A4M|8uQUCw}00001 z00;&E003NasAd2FWH?DgK~#9!?Y()tWo3Ei`+e5l=bW0Ws49vA3M>i`0c})7MrQ$y z>5gXVzR7L7qlr<2KyGfw3^#)&h$h`>O=!){r*CqTPqcHBj*_UY42lTIq$rWO1PZF4 z2B<2ixz0IzJ@=2b_t|@`^$yRw_9-}3?B%EA>@~jYUGMMrKJzP#wmG*g{_sCD|KtPg zd0-)`+TE+r{Zk~*P+6EOOF>rv$AGQ|jzu{JbQQ1y)n&j^g@vfj113Pnfi_ATRJ!+8 zRi=Sy&`IDRst15Qz%EpuQ`io=4cLmZ8FUNiR$x2uJgR#Y$@}e8dJWdck4H}4$n@X+ z!sz49u}wx@@ZxR@e(7rnIf$V70M=E>TAJk@76C_tu0dG`oD8}aSc9@k(W8Kcp!2%G zmNE!?e-02yZyVS*^JnjPg?I+FF^T|ZXdlN89rz5ml`67y0{8Z3asd6UhslXc0qfizU z-%JOv=mT#5ZzlohAQ^-Rp!iz@*Y0USe zkMAE1kP!hKdE5LCT*tTy)6xe^C(p>FvjkWRdKPdFs%KHm@GF4{^#H9uD-VFw1x_6} z!$bhCV~GUdxVHPpLEu?ceH6GCxEu6dlnou#T`lRqM@QO><&#W(@dKm5Ga`V)b6fDs z*P|^Z>tur<7)Lol;WeNa3cOxbPX$&0Srw!)0&q(JWdi8$YAONLQ~(bF=)i9Ovj*Cd zgKkGz58MTMEAV~LjjBxbF3hK<$l7i4FOA4xL;!PqTlAqDnONH9;Lc7LR0_fZR8Irm z0K5g&*8s;UIz~Y3)`-qtKF58JbOP`^t408o=T^x`J*4F&H3gUgo<{W^;9Cl}fIf_} zZ>EittB~<+BQh8fz}(pu{?heGt5u9)>5Xt%0D8KjZ$r5l^wq%162Kkcr~=C`mU~nH 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 KMNUMnLSTZnkCXQR literal 0 HcmV?d00001 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 params) => localizeFill(this, params); + String plural(int value) => localizePlural(value, this, _t); + String version(Object modifier) => localizeVersion(modifier, this, _t); +} diff --git a/filcnaplo_desktop_ui/lib/screens/settings/settings_screen.dart b/filcnaplo_desktop_ui/lib/screens/settings/settings_screen.dart index 83504f6..aef41ff 100644 --- a/filcnaplo_desktop_ui/lib/screens/settings/settings_screen.dart +++ b/filcnaplo_desktop_ui/lib/screens/settings/settings_screen.dart @@ -35,6 +35,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_custom_tabs/flutter_custom_tabs.dart' as tabs; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'settings_screen.i18n.dart'; @@ -86,11 +87,11 @@ class _SettingsScreenState 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", + name: Text(!settings.presentationMode ? account.name : "János", style: const TextStyle(fontWeight: FontWeight.w500)), username: Text(!settings.presentationMode ? account.username : "72469696969"), @@ -164,7 +165,7 @@ class _SettingsScreenState extends State if (!settings.presentationMode) { firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0]; } else { - firstName = "Béla"; + firstName = "János"; } String startPageTitle = @@ -206,307 +207,403 @@ class _SettingsScreenState extends State animation: _hideContainersController, builder: (context, child) => Opacity( opacity: 1 - _hideContainersController.value, - child: SingleChildScrollView( - child: Column( - children: [ - const SizedBox(height: 32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + StaggeredGrid.extent( + // direction: Axis.horizontal, + // crossAxisCount: 3, + maxCrossAxisExtent: 600, + children: [ + const SizedBox(height: 32.0), - // Updates - if (updateProvider.available) - Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0, horizontal: 24.0), - child: Panel( - child: PanelButton( - onPressed: () => _openUpdates(context), - title: Text("update_available".i18n), - leading: const Icon(FeatherIcons.download), - trailing: Text( - updateProvider.releases.first.tag, - style: TextStyle( - fontWeight: FontWeight.w500, - color: - Theme.of(context).colorScheme.secondary, - ), - ), - ), - ), - ), - - // const Padding( - // padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0), - // child: PremiumBannerButton(), - // ), - if (!Provider.of(context).hasPremium) - const ClipRect( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 12.0), - child: PremiumButton(), - ), - ), - - // General Settings - Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0, horizontal: 24.0), - child: Panel( - title: Text("general".i18n), - child: Column( - children: [ - PanelButton( - onPressed: () { - SettingsHelper.language(context); - setState(() {}); - }, - title: Text("language".i18n), - leading: const Icon(FeatherIcons.globe), - trailing: Text(languageText), - ), - PanelButton( - onPressed: () { - SettingsHelper.startPage(context); - setState(() {}); - }, - title: Text("startpage".i18n), - leading: const Icon(FeatherIcons.play), - trailing: Text(startPageTitle.capital()), - ), - PanelButton( - onPressed: () { - SettingsHelper.rounding(context); - setState(() {}); - }, - title: Text("rounding".i18n), - leading: const Icon(FeatherIcons.gitCommit), - trailing: Text((settings.rounding / 10) - .toStringAsFixed(1)), - ), - PanelButton( - onPressed: () { - SettingsHelper.vibrate(context); - setState(() {}); - }, - title: Text("vibrate".i18n), - leading: const Icon(FeatherIcons.radio), - trailing: Text(vibrateTitle), - ), - PanelButton( - padding: const EdgeInsets.only(left: 14.0), - onPressed: () { - SettingsHelper.bellDelay(context); - setState(() {}); - }, - title: Text( - "bell_delay".i18n, - style: TextStyle( - color: AppColors.of(context) - .text - .withOpacity( - settings.bellDelayEnabled - ? 1.0 - : .5)), - ), - leading: settings.bellDelayEnabled - ? const Icon(FeatherIcons.bell) - : Icon(FeatherIcons.bellOff, - color: AppColors.of(context) - .text - .withOpacity(.25)), - trailingDivider: true, - trailing: Switch( - onChanged: (v) => - settings.update(bellDelayEnabled: v), - value: settings.bellDelayEnabled, - activeColor: - Theme.of(context).colorScheme.secondary, - ), - ), - ], - ), - ), - ), - - if (kDebugMode) - Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0, horizontal: 24.0), - child: Panel( - title: const Text("Debug"), - child: Column( - children: [ - PanelButton( - title: const Text("Subject Icon Gallery"), - leading: const Icon(CupertinoIcons - .rectangle_3_offgrid_fill), - trailing: const Icon(Icons.arrow_forward), - onPressed: () { - Navigator.of(context, rootNavigator: true) - .push( - CupertinoPageRoute( - builder: (context) => - const SubjectIconGallery()), - ); - }, - ) - ], - ), - ), - ), - - // Secret Settings - if (__ss) - Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0, horizontal: 24.0), - child: Panel( - title: Text("secret".i18n), - child: Column( - children: [ - // Good student mode - Material( - type: MaterialType.transparency, - child: SwitchListTile( - contentPadding: - const EdgeInsets.only(left: 12.0), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(12.0)), - title: Text("goodstudent".i18n, - style: const TextStyle( - fontWeight: FontWeight.w500)), - onChanged: (v) { - if (v) { - showDialog( - context: context, - builder: (context) => WillPopScope( - onWillPop: () async => false, - child: AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular( - 12.0)), - title: Text("attention".i18n), - content: Text( - "goodstudent_disclaimer" - .i18n), - actions: [ - ActionButton( - label: "understand".i18n, - onTap: () { - Navigator.of(context) - .pop(); - settings.update( - goodStudent: v); - Provider.of( - context, - listen: false) - .fetch(); - }) - ], - ), - ), - ); - } else { - settings.update(goodStudent: v); - Provider.of(context, - listen: false) - .fetch(); - } - }, - value: settings.goodStudent, - activeColor: Theme.of(context) - .colorScheme - .secondary, - ), - ), - - // Presentation mode - Material( - type: MaterialType.transparency, - child: SwitchListTile( - contentPadding: - const EdgeInsets.only(left: 12.0), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(12.0)), - title: const Text("Presentation Mode", - style: TextStyle( - fontWeight: FontWeight.w500)), - onChanged: (v) => - settings.update(presentationMode: v), - value: settings.presentationMode, - activeColor: Theme.of(context) - .colorScheme - .secondary, - ), - ), - ], - ), - ), - ), - - // Theme Settings - Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0, horizontal: 24.0), - child: Panel( - title: Text("appearance".i18n), - child: Column( - children: [ - PanelButton( - onPressed: () { - SettingsHelper.theme(context); - setState(() {}); - }, - title: Text("theme".i18n), - leading: const Icon(FeatherIcons.sun), - trailing: Text(themeModeText), - ), - PanelButton( - onPressed: () async { - await _hideContainersController.forward(); - SettingsHelper.accentColor(context); - setState(() {}); - _hideContainersController.reset(); - }, - title: Text("color".i18n), - leading: const Icon(FeatherIcons.droplet), - trailing: Container( - width: 12.0, - height: 12.0, - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .secondary, - shape: BoxShape.circle, - ), - ), - ), - PanelButton( - onPressed: () { - SettingsHelper.gradeColors(context); - setState(() {}); - }, - title: Text("grade_colors".i18n), - leading: const Icon(FeatherIcons.star), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: List.generate( - 5, - (i) => Container( - margin: - const EdgeInsets.only(left: 2.0), - width: 12.0, - height: 12.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: settings.gradeColors[i], - ), + // Updates + if (updateProvider.available) + Container( + constraints: const BoxConstraints(maxWidth: 500), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 24.0), + child: Panel( + child: PanelButton( + onPressed: () => _openUpdates(context), + title: Text("update_available".i18n), + leading: const Icon(FeatherIcons.download), + trailing: Text( + updateProvider.releases.first.tag, + style: TextStyle( + fontWeight: FontWeight.w500, + color: Theme.of(context) + .colorScheme + .secondary, ), ), ), ), - Material( + ), + ), + + // const Padding( + // padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0), + // child: PremiumBannerButton(), + // ), + if (!Provider.of(context).hasPremium) + const ClipRect( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12.0), + child: PremiumButton(), + ), + ), + + // General Settings + Container( + constraints: const BoxConstraints(maxWidth: 500), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 24.0), + child: Panel( + title: Text("general".i18n), + child: Column( + children: [ + PanelButton( + onPressed: () { + SettingsHelper.language(context); + setState(() {}); + }, + title: Text("language".i18n), + leading: const Icon(FeatherIcons.globe), + trailing: Text(languageText), + ), + PanelButton( + onPressed: () { + SettingsHelper.startPage(context); + setState(() {}); + }, + title: Text("startpage".i18n), + leading: const Icon(FeatherIcons.play), + trailing: Text(startPageTitle.capital()), + ), + PanelButton( + onPressed: () { + SettingsHelper.rounding(context); + setState(() {}); + }, + title: Text("rounding".i18n), + leading: + const Icon(FeatherIcons.gitCommit), + trailing: Text((settings.rounding / 10) + .toStringAsFixed(1)), + ), + PanelButton( + onPressed: () { + SettingsHelper.vibrate(context); + setState(() {}); + }, + title: Text("vibrate".i18n), + leading: const Icon(FeatherIcons.radio), + trailing: Text(vibrateTitle), + ), + PanelButton( + padding: + const EdgeInsets.only(left: 14.0), + onPressed: () { + SettingsHelper.bellDelay(context); + setState(() {}); + }, + title: Text( + "bell_delay".i18n, + style: TextStyle( + color: AppColors.of(context) + .text + .withOpacity( + settings.bellDelayEnabled + ? 1.0 + : .5)), + ), + leading: settings.bellDelayEnabled + ? const Icon(FeatherIcons.bell) + : Icon(FeatherIcons.bellOff, + color: AppColors.of(context) + .text + .withOpacity(.25)), + trailingDivider: true, + trailing: Switch( + onChanged: (v) => settings.update( + bellDelayEnabled: v), + value: settings.bellDelayEnabled, + activeColor: Theme.of(context) + .colorScheme + .secondary, + ), + ), + ], + ), + ), + ), + ), + + if (kDebugMode) + Container( + constraints: const BoxConstraints(maxWidth: 500), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 24.0), + child: Panel( + title: const Text("Debug"), + child: Column( + children: [ + PanelButton( + title: + const Text("Subject Icon Gallery"), + leading: const Icon(CupertinoIcons + .rectangle_3_offgrid_fill), + trailing: + const Icon(Icons.arrow_forward), + onPressed: () { + Navigator.of(context, + rootNavigator: true) + .push( + CupertinoPageRoute( + builder: (context) => + const SubjectIconGallery()), + ); + }, + ) + ], + ), + ), + ), + ), + + // Secret Settings + if (__ss) + Container( + constraints: const BoxConstraints(maxWidth: 500), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 24.0), + child: Panel( + title: Text("secret".i18n), + child: Column( + children: [ + // Good student mode + Material( + type: MaterialType.transparency, + child: SwitchListTile( + contentPadding: + const EdgeInsets.only(left: 12.0), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12.0)), + title: Text("goodstudent".i18n, + style: const TextStyle( + fontWeight: FontWeight.w500)), + onChanged: (v) { + if (v) { + showDialog( + context: context, + builder: (context) => + WillPopScope( + onWillPop: () async => false, + child: AlertDialog( + shape: + RoundedRectangleBorder( + borderRadius: + BorderRadius + .circular( + 12.0)), + title: + Text("attention".i18n), + content: Text( + "goodstudent_disclaimer" + .i18n), + actions: [ + ActionButton( + label: + "understand".i18n, + onTap: () { + Navigator.of( + context) + .pop(); + settings.update( + goodStudent: v); + Provider.of( + context, + listen: + false) + .fetch(); + }) + ], + ), + ), + ); + } else { + settings.update(goodStudent: v); + Provider.of( + context, + listen: false) + .fetch(); + } + }, + value: settings.goodStudent, + activeColor: Theme.of(context) + .colorScheme + .secondary, + ), + ), + + // Presentation mode + Material( + type: MaterialType.transparency, + child: SwitchListTile( + contentPadding: + const EdgeInsets.only(left: 12.0), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12.0)), + title: const Text("Presentation Mode", + style: TextStyle( + fontWeight: FontWeight.w500)), + onChanged: (v) => settings.update( + presentationMode: v), + value: settings.presentationMode, + activeColor: Theme.of(context) + .colorScheme + .secondary, + ), + ), + ], + ), + ), + ), + ), + + // Theme Settings + Container( + constraints: const BoxConstraints(maxWidth: 500), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 24.0), + child: Panel( + title: Text("appearance".i18n), + child: Column( + children: [ + PanelButton( + onPressed: () { + SettingsHelper.theme(context); + setState(() {}); + }, + title: Text("theme".i18n), + leading: const Icon(FeatherIcons.sun), + trailing: Text(themeModeText), + ), + PanelButton( + onPressed: () async { + await _hideContainersController + .forward(); + SettingsHelper.accentColor(context); + setState(() {}); + _hideContainersController.reset(); + }, + title: Text("color".i18n), + leading: const Icon(FeatherIcons.droplet), + trailing: Container( + width: 12.0, + height: 12.0, + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .secondary, + shape: BoxShape.circle, + ), + ), + ), + PanelButton( + onPressed: () { + SettingsHelper.gradeColors(context); + setState(() {}); + }, + title: Text("grade_colors".i18n), + leading: const Icon(FeatherIcons.star), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: List.generate( + 5, + (i) => Container( + margin: const EdgeInsets.only( + left: 2.0), + width: 12.0, + height: 12.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: settings.gradeColors[i], + ), + ), + ), + ), + ), + Material( + type: MaterialType.transparency, + child: SwitchListTile( + contentPadding: + const EdgeInsets.only(left: 12.0), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12.0)), + title: Row( + children: [ + Icon( + FeatherIcons.barChart, + color: settings.graphClassAvg + ? Theme.of(context) + .colorScheme + .secondary + : AppColors.of(context) + .text + .withOpacity(.25), + ), + const SizedBox(width: 24.0), + Expanded( + child: Text( + "graph_class_avg".i18n, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.0, + color: AppColors.of(context) + .text + .withOpacity( + settings.graphClassAvg + ? 1.0 + : .5), + ), + ), + ), + ], + ), + onChanged: (v) => + settings.update(graphClassAvg: v), + value: settings.graphClassAvg, + activeColor: Theme.of(context) + .colorScheme + .secondary, + ), + ), + const PremiumIconPackSelector(), + ], + ), + ), + ), + ), + + // Notifications + Container( + constraints: const BoxConstraints(maxWidth: 500), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 24.0), + child: Panel( + title: Text("notifications".i18n), + child: Material( type: MaterialType.transparency, child: SwitchListTile( contentPadding: @@ -517,8 +614,8 @@ class _SettingsScreenState extends State title: Row( children: [ Icon( - FeatherIcons.barChart, - color: settings.graphClassAvg + Icons.newspaper_outlined, + color: settings.newsEnabled ? Theme.of(context) .colorScheme .secondary @@ -529,14 +626,14 @@ class _SettingsScreenState extends State const SizedBox(width: 24.0), Expanded( child: Text( - "graph_class_avg".i18n, + "news".i18n, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16.0, color: AppColors.of(context) .text .withOpacity( - settings.graphClassAvg + settings.newsEnabled ? 1.0 : .5), ), @@ -545,252 +642,25 @@ class _SettingsScreenState extends State ], ), onChanged: (v) => - settings.update(graphClassAvg: v), - value: settings.graphClassAvg, - activeColor: - Theme.of(context).colorScheme.secondary, - ), - ), - const PremiumIconPackSelector(), - ], - ), - ), - ), - - // Notifications - Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0, horizontal: 24.0), - child: Panel( - title: Text("notifications".i18n), - child: Material( - type: MaterialType.transparency, - child: SwitchListTile( - contentPadding: - const EdgeInsets.only(left: 12.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0)), - title: Row( - children: [ - Icon( - Icons.newspaper_outlined, - color: settings.newsEnabled - ? Theme.of(context) - .colorScheme - .secondary - : AppColors.of(context) - .text - .withOpacity(.25), - ), - const SizedBox(width: 24.0), - Expanded( - child: Text( - "news".i18n, - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16.0, - color: AppColors.of(context) - .text - .withOpacity(settings.newsEnabled - ? 1.0 - : .5), - ), - ), - ), - ], - ), - onChanged: (v) => - settings.update(newsEnabled: v), - value: settings.newsEnabled, - activeColor: - Theme.of(context).colorScheme.secondary, - ), - ), - ), - ), - - // Extras - Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0, horizontal: 24.0), - child: Panel( - title: Text("extras".i18n), - child: Column(children: [ - Material( - type: MaterialType.transparency, - child: SwitchListTile( - contentPadding: - const EdgeInsets.only(left: 12.0), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(12.0)), - title: Row( - children: [ - Icon( - FeatherIcons.gift, - color: settings.gradeOpeningFun - ? Theme.of(context) - .colorScheme - .secondary - : AppColors.of(context) - .text - .withOpacity(.25), - ), - const SizedBox(width: 24.0), - Expanded( - child: Text( - "surprise_grades".i18n, - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16.0, - color: AppColors.of(context) - .text - .withOpacity( - settings.gradeOpeningFun - ? 1.0 - : .5), - ), - ), - ), - ], - ), - onChanged: (v) => - settings.update(gradeOpeningFun: v), - value: settings.gradeOpeningFun, - activeColor: - Theme.of(context).colorScheme.secondary, - ), - ), - ]), - ), - ), - - // About - Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0, horizontal: 24.0), - child: Panel( - title: Text("about".i18n), - child: Column(children: [ - PanelButton( - leading: const Icon(FeatherIcons.atSign), - title: const Text("Discord"), - onPressed: () => launchUrl( - Uri.parse("https://filcnaplo.hu/discord"), - mode: LaunchMode.externalApplication), - ), - PanelButton( - leading: const Icon(FeatherIcons.globe), - title: const Text("www.filcnaplo.hu"), - onPressed: () => launchUrl( - Uri.parse("https://filcnaplo.hu"), - mode: LaunchMode.externalApplication), - ), - PanelButton( - leading: const Icon(FeatherIcons.github), - title: const Text("Github"), - onPressed: () => launchUrl( - Uri.parse("https://github.com/filc"), - mode: LaunchMode.externalApplication), - ), - PanelButton( - leading: const Icon(FeatherIcons.mail), - title: Text("news".i18n), - onPressed: () => _openNews(context), - ), - PanelButton( - leading: const Icon(FeatherIcons.lock), - title: Text("privacy".i18n), - onPressed: () => _openPrivacy(context), - ), - PanelButton( - leading: const Icon(FeatherIcons.award), - title: Text("licenses".i18n), - onPressed: () => - showLicensePage(context: context), - ), - Tooltip( - message: "data_collected".i18n, - padding: const EdgeInsets.all(4.0), - textStyle: TextStyle( - fontWeight: FontWeight.w500, - color: AppColors.of(context).text), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .background), - child: Material( - type: MaterialType.transparency, - child: SwitchListTile( - contentPadding: - const EdgeInsets.only(left: 12.0), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(12.0)), - secondary: Icon( - FeatherIcons.barChart2, - color: settings.xFilcId != "none" - ? Theme.of(context) - .colorScheme - .secondary - : AppColors.of(context) - .text - .withOpacity(.25), - ), - title: Text( - "Analytics".i18n, - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16.0, - color: AppColors.of(context) - .text - .withOpacity( - settings.xFilcId != "none" - ? 1.0 - : .5), - ), - ), - subtitle: Text( - "Anonymous Usage Analytics".i18n, - style: TextStyle( - color: AppColors.of(context) - .text - .withOpacity( - settings.xFilcId != "none" - ? .5 - : .2), - ), - ), - onChanged: (v) { - String newId; - if (v == false) { - newId = "none"; - } else if (settings.xFilcId == "none") { - newId = - SettingsProvider.defaultSettings() - .xFilcId; - } else { - newId = settings.xFilcId; - } - settings.update(xFilcId: newId); - }, - value: settings.xFilcId != "none", + settings.update(newsEnabled: v), + value: settings.newsEnabled, activeColor: Theme.of(context).colorScheme.secondary, ), ), ), - ]), + ), ), - ), - if (settings.developerMode) - Padding( - padding: const EdgeInsets.symmetric( - vertical: 12.0, horizontal: 24.0), - child: Panel( - title: const Text("Developer Settings"), - child: Column( - children: [ + + // Extras + Container( + constraints: const BoxConstraints(maxWidth: 500), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 24.0), + child: Panel( + title: Text("extras".i18n), + child: Column(children: [ Material( type: MaterialType.transparency, child: SwitchListTile( @@ -799,88 +669,280 @@ class _SettingsScreenState extends State shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0)), - title: const Text("Developer Mode", - style: TextStyle( - fontWeight: FontWeight.w500)), + title: Row( + children: [ + Icon( + FeatherIcons.gift, + color: settings.gradeOpeningFun + ? Theme.of(context) + .colorScheme + .secondary + : AppColors.of(context) + .text + .withOpacity(.25), + ), + const SizedBox(width: 24.0), + Expanded( + child: Text( + "surprise_grades".i18n, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.0, + color: AppColors.of(context) + .text + .withOpacity( + settings.gradeOpeningFun + ? 1.0 + : .5), + ), + ), + ), + ], + ), onChanged: (v) => - settings.update(developerMode: false), - value: settings.developerMode, + settings.update(gradeOpeningFun: v), + value: settings.gradeOpeningFun, activeColor: Theme.of(context) .colorScheme .secondary, ), ), - PanelButton( - leading: const Icon(FeatherIcons.copy), - title: const Text("Copy JWT"), - onPressed: () => Clipboard.setData( - ClipboardData( - text: Provider.of( - context, - listen: false) - .accessToken!)), - ), - if (Provider.of(context, - listen: false) - .hasPremium) - PanelButton( - leading: const Icon(FeatherIcons.key), - title: const Text("Remove Premium"), - onPressed: () { - Provider.of(context, - listen: false) - .activate(removePremium: true); - settings.update( - accentColor: AccentColor.filc, - store: true); - Provider.of(context, - listen: false) - .changeTheme(settings.theme); - }, - ), - ], + ]), ), ), ), - SafeArea( - top: false, - child: Center( - child: GestureDetector( - child: const Panel( - title: Text("v" + - String.fromEnvironment("APPVER", - defaultValue: "?"))), - onTap: () { - if (devmodeCountdown > 0) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar( - duration: const Duration(milliseconds: 200), - content: Text( - "You are $devmodeCountdown taps away from Developer Mode."), - )); - setState(() => devmodeCountdown--); - } else if (devmodeCountdown == 0) { - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar( - content: Text( - "Developer Mode successfully activated."), - )); - - settings.update(developerMode: true); - - setState(() => devmodeCountdown--); - } - }, + // About + Container( + constraints: const BoxConstraints(maxWidth: 500), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 24.0), + child: Panel( + title: Text("about".i18n), + child: Column(children: [ + PanelButton( + leading: const Icon(FeatherIcons.atSign), + title: const Text("Discord"), + onPressed: () => launchUrl( + Uri.parse( + "https://filcnaplo.hu/discord"), + mode: LaunchMode.externalApplication), + ), + PanelButton( + leading: const Icon(FeatherIcons.globe), + title: const Text("www.filcnaplo.hu"), + onPressed: () => launchUrl( + Uri.parse("https://filcnaplo.hu"), + mode: LaunchMode.externalApplication), + ), + PanelButton( + leading: const Icon(FeatherIcons.github), + title: const Text("Github"), + onPressed: () => launchUrl( + Uri.parse("https://github.com/filc"), + mode: LaunchMode.externalApplication), + ), + PanelButton( + leading: const Icon(FeatherIcons.mail), + title: Text("news".i18n), + onPressed: () => _openNews(context), + ), + PanelButton( + leading: const Icon(FeatherIcons.lock), + title: Text("privacy".i18n), + onPressed: () => _openPrivacy(context), + ), + PanelButton( + leading: const Icon(FeatherIcons.award), + title: Text("licenses".i18n), + onPressed: () => + showLicensePage(context: context), + ), + Tooltip( + message: "data_collected".i18n, + padding: const EdgeInsets.all(4.0), + textStyle: TextStyle( + fontWeight: FontWeight.w500, + color: AppColors.of(context).text), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .background), + child: Material( + type: MaterialType.transparency, + child: SwitchListTile( + contentPadding: + const EdgeInsets.only(left: 12.0), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12.0)), + secondary: Icon( + FeatherIcons.barChart2, + color: settings.xFilcId != "none" + ? Theme.of(context) + .colorScheme + .secondary + : AppColors.of(context) + .text + .withOpacity(.25), + ), + title: Text( + "Analytics".i18n, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.0, + color: AppColors.of(context) + .text + .withOpacity( + settings.xFilcId != "none" + ? 1.0 + : .5), + ), + ), + subtitle: Text( + "Anonymous Usage Analytics".i18n, + style: TextStyle( + color: AppColors.of(context) + .text + .withOpacity( + settings.xFilcId != "none" + ? .5 + : .2), + ), + ), + onChanged: (v) { + String newId; + if (v == false) { + newId = "none"; + } else if (settings.xFilcId == + "none") { + newId = SettingsProvider + .defaultSettings() + .xFilcId; + } else { + newId = settings.xFilcId; + } + settings.update(xFilcId: newId); + }, + value: settings.xFilcId != "none", + activeColor: Theme.of(context) + .colorScheme + .secondary, + ), + ), + ), + ]), + ), ), ), + if (settings.developerMode) + Container( + constraints: const BoxConstraints(maxWidth: 500), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 24.0), + child: Panel( + title: const Text("Developer Settings"), + child: Column( + children: [ + Material( + type: MaterialType.transparency, + child: SwitchListTile( + contentPadding: + const EdgeInsets.only(left: 12.0), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12.0)), + title: const Text("Developer Mode", + style: TextStyle( + fontWeight: FontWeight.w500)), + onChanged: (v) => settings.update( + developerMode: false), + value: settings.developerMode, + activeColor: Theme.of(context) + .colorScheme + .secondary, + ), + ), + PanelButton( + leading: const Icon(FeatherIcons.copy), + title: const Text("Copy JWT"), + onPressed: () => Clipboard.setData( + ClipboardData( + text: Provider.of( + context, + listen: false) + .accessToken!)), + ), + // if (Provider.of(context, + // listen: false) + // .hasPremium) + // PanelButton( + // leading: const Icon(FeatherIcons.key), + // title: const Text("Remove Premium"), + // onPressed: () { + // Provider.of( + // context, + // listen: false) + // .activate(removePremium: true); + // settings.update( + // accentColor: AccentColor.filc, + // store: true); + // Provider.of( + // context, + // listen: false) + // .changeTheme(settings.theme); + // }, + // ), + ], + ), + ), + ), + ), + ], + ), + const SizedBox( + height: 40, + ), + SafeArea( + top: false, + child: Center( + child: GestureDetector( + child: const Panel( + title: Text("v" + + String.fromEnvironment("APPVER", + defaultValue: "?"))), + onTap: () { + if (devmodeCountdown > 0) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar( + duration: const Duration(milliseconds: 200), + content: Text( + "You are $devmodeCountdown taps away from Developer Mode."), + )); + + setState(() => devmodeCountdown--); + } else if (devmodeCountdown == 0) { + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar( + content: Text( + "Developer Mode successfully activated."), + )); + + settings.update(developerMode: true); + + setState(() => devmodeCountdown--); + } + }, + ), ), - ], - ), + ), + ], ), ), ), - ) + ), ], ), ), diff --git a/filcnaplo_desktop_ui/pubspec.yaml b/filcnaplo_desktop_ui/pubspec.yaml index 676f882..2a1645a 100644 --- a/filcnaplo_desktop_ui/pubspec.yaml +++ b/filcnaplo_desktop_ui/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: auto_size_text: ^3.0.0 flutter_acrylic: ^1.1.3 elegant_notification: ^1.6.1 + flutter_staggered_grid_view: ^0.7.0 dev_dependencies: flutter_lints: ^1.0.0 diff --git a/filcnaplo_mobile_ui/lib/common/widgets/absence_group/absence_group_tile.dart b/filcnaplo_mobile_ui/lib/common/widgets/absence_group/absence_group_tile.dart index 9207f72..8ba3974 100755 --- a/filcnaplo_mobile_ui/lib/common/widgets/absence_group/absence_group_tile.dart +++ b/filcnaplo_mobile_ui/lib/common/widgets/absence_group/absence_group_tile.dart @@ -8,7 +8,9 @@ import 'package:flutter/material.dart'; import 'absence_group_tile.i18n.dart'; class AbsenceGroupTile extends StatelessWidget { - const AbsenceGroupTile(this.absences, {Key? key, this.showDate = false, this.padding}) : super(key: key); + const AbsenceGroupTile(this.absences, + {Key? key, this.showDate = false, this.padding}) + : super(key: key); final List absences; final bool showDate; @@ -16,10 +18,12 @@ class AbsenceGroupTile extends StatelessWidget { @override Widget build(BuildContext context) { - Justification state = getState(absences.map((e) => e.absence.state).toList()); + Justification state = + getState(absences.map((e) => e.absence.state).toList()); Color color = AbsenceTile.justificationColor(state, context: context); - absences.sort((a, b) => a.absence.lessonIndex?.compareTo(b.absence.lessonIndex ?? 0) ?? -1); + absences.sort((a, b) => + a.absence.lessonIndex?.compareTo(b.absence.lessonIndex ?? 0) ?? -1); return ClipRRect( borderRadius: BorderRadius.circular(14.0), @@ -29,6 +33,8 @@ class AbsenceGroupTile extends StatelessWidget { padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0), child: AbsenceGroupContainer( child: ExpansionTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), tilePadding: const EdgeInsets.symmetric(horizontal: 8.0), backgroundColor: Colors.transparent, leading: Container( @@ -38,22 +44,33 @@ class AbsenceGroupTile extends StatelessWidget { shape: BoxShape.circle, color: color.withOpacity(.25), ), - child: Center(child: Icon(AbsenceTile.justificationIcon(state), color: color)), + child: Center( + child: Icon(AbsenceTile.justificationIcon(state), + color: color)), ), title: Text.rich(TextSpan( - text: "${absences.where((a) => a.absence.state == state).length} ", - style: TextStyle(fontWeight: FontWeight.w700, color: AppColors.of(context).text), + text: + "${absences.where((a) => a.absence.state == state).length} ", + style: TextStyle( + fontWeight: FontWeight.w700, + color: AppColors.of(context).text), children: [ TextSpan( - text: AbsenceTile.justificationName(state).fill(["absence".i18n]), - style: TextStyle(fontWeight: FontWeight.w600, color: AppColors.of(context).text), + text: AbsenceTile.justificationName(state) + .fill(["absence".i18n]), + style: TextStyle( + fontWeight: FontWeight.w600, + color: AppColors.of(context).text), ), ], )), subtitle: showDate ? Text( - absences.first.absence.date.format(context, weekday: true), - style: TextStyle(fontWeight: FontWeight.w500, color: AppColors.of(context).text.withOpacity(0.8)), + absences.first.absence.date + .format(context, weekday: true), + style: TextStyle( + fontWeight: FontWeight.w500, + color: AppColors.of(context).text.withOpacity(0.8)), ) : null, children: absences,