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 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 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,