diff --git a/.metadata b/.metadata index 3fdf532..f28556b 100644 --- a/.metadata +++ b/.metadata @@ -4,8 +4,8 @@ # This file should be version controlled. version: - revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 - channel: stable + revision: e7ab3b07f88024b8f7e30a1e533134700fae1fb9 + channel: master project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 - base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 - - platform: android - create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 - base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 - - platform: ios - create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 - base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 - - platform: linux - create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 - base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 - - platform: macos - create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 - base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 - - platform: web - create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 - base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 + create_revision: e7ab3b07f88024b8f7e30a1e533134700fae1fb9 + base_revision: e7ab3b07f88024b8f7e30a1e533134700fae1fb9 - platform: windows - create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 - base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 + create_revision: e7ab3b07f88024b8f7e30a1e533134700fae1fb9 + base_revision: e7ab3b07f88024b8f7e30a1e533134700fae1fb9 # User provided section diff --git a/lib/main.dart b/lib/main.dart index dc6d24c..338d045 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:socket_io_client/socket_io_client.dart' as io; @@ -174,27 +176,209 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { late io.Socket socket; + late AuthData authData = Get.find(); + + var guessersPlayers = [].obs; + var suggestersPlayers = [].obs; + var incommingRequests = [].obs; @override void initState() { super.initState(); socket = Get.find(); + + socket.on("hello", (idky) { + socket.dispose(); + Get.offAllNamed("/auth"); + }); + + socket.on("update", (update) { + bool ok = update[0]; + if (ok) { + var data = update[1]; + guessersPlayers.value = (data["guessers"] as List? ?? []) + .map((e) => e.toString()) + .toList(growable: false); + suggestersPlayers.value = (data["suggesters"] as List? ?? []) + .map((e) => e.toString()) + .toList(growable: false); + } else { + Get.snackbar("Error", "Update failed with message: ${update[1]}"); + } + }); + + socket.on("updateNeeded", (data) { + socket.emit("getUpdate"); + }); + + socket.on("gameRequest", (requestFrom) { + setState(() { + incommingRequests.add(requestFrom); + }); + }); + + socket.emit("getUpdate"); } @override Widget build(BuildContext context) { + final shortestSide = MediaQuery.of(context).size.shortestSide; + final useMobileLayout = shortestSide < 600; + final elements = [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(16), + child: _buildPlayerList( + guessersPlayers, + "Guessers", + "guessers", + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.all(16), + child: _buildPlayerList( + suggestersPlayers, + "Suggesters", + "suggesters", + ), + ), + ), + ]; + return Scaffold( appBar: AppBar( title: const Text("HUACU"), ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text("Hello, ${Get.find().login}"), - ], - ), - ), + body: useMobileLayout + ? Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: elements, + ) + : Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: elements, + ), ); } + + Widget _buildPlayerList(RxList players, String title, String team) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title), + Obx(() => Text("Count: ${players.length}")), + Expanded( + child: ObxValue( + (data) => ListView.builder( + itemCount: data.length, + itemBuilder: (context, index) { + return Card( + child: ListTile( + title: Text( + "${data[index]} ${authData.login == data[index] ? "(you)" : ""}" + .trim(), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (incommingRequests.contains(data[index])) + InkResponse( + onTap: () => + _handleRequestResponseAction(data[index], true), + child: const Icon(Icons.check), + ), + if (incommingRequests.contains(data[index])) + InkResponse( + onTap: () => _handleRequestResponseAction( + data[index], false), + child: const Icon(Icons.close), + ), + if (authData.login != data[index]) + InkResponse( + onTap: () => _handleSendRequestClick(data[index]), + child: const Icon(Icons.connect_without_contact), + ), + ], + ), + ), + ); + }, + ), + players, + ), + ), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: ObxValue( + (data) => players.contains(authData.login) + ? ElevatedButton( + onPressed: () => _handleLeaveClick(team), + child: const Text("Leave"), + ) + : ElevatedButton( + onPressed: () => _handleJoinClick(team), + child: const Text("Join"), + ), + guessersPlayers, + ), + ), + ], + ), + ], + ); + } + + void _joinResponseHandler(dynamic data) { + bool ok = data[0]; + if (!ok) { + int status = data[1]; + Get.snackbar("Error", "Join failed with status $status"); + } + socket.off("joinResponse", _joinResponseHandler); + } + + void _handleJoinClick(String side) { + socket.on("joinResponse", _joinResponseHandler); + socket.emit("join", side); + } + + void _leaveResponseHandler(dynamic data) { + bool ok = data[0]; + if (!ok) { + int status = data[1]; + Get.snackbar("Error", "Leaving failed with status $status"); + } + socket.off("leaveResponse", _leaveResponseHandler); + } + + void _handleLeaveClick(String side) { + socket.on("leaveResponse", _leaveResponseHandler); + socket.emit("leave", side); + } + + void _sendRequestResponseHandler(dynamic data) { + bool ok = data[0]; + if (ok) { + Get.snackbar("Success", "Request sent"); + } else { + Get.snackbar("Error", "Request failed"); + } + socket.off("sendRequestResponse", _leaveResponseHandler); + } + + void _handleSendRequestClick(String player) { + socket.on("sendRequestResponse", _sendRequestResponseHandler); + socket.emit("sendRequest", player); + } + + void _handleRequestResponseAction(String data, bool bool) {} } diff --git a/pubspec.lock b/pubspec.lock index e9b6337..d24324f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,49 +5,56 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.1" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" source: hosted version: "1.0.5" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted version: "1.3.1" flutter: @@ -59,7 +66,8 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_test: @@ -71,58 +79,66 @@ packages: dependency: "direct main" description: name: get - url: "https://pub.dartlang.org" + sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" + url: "https://pub.dev" source: hosted version: "4.6.5" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" source: hosted version: "1.1.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: c94db23593b89766cda57aab9ac311e3616cf87c6fa4e9749df032f66f30dcb8 + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.14" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "12307e7f0605ce3da64cf0db90e5fcab0869f3ca03f76be6bb2991ce0a55e82b" + url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" sky_engine: dependency: transitive description: flutter @@ -132,64 +148,73 @@ packages: dependency: "direct main" description: name: socket_io_client - url: "https://pub.dartlang.org" + sha256: a9c589d3fe2658506be38ddb36f23348daab73a00ff1645533669d07a5111cfc + url: "https://pub.dev" source: hosted version: "2.0.1" socket_io_common: dependency: transitive description: name: socket_io_common - url: "https://pub.dartlang.org" + sha256: "5a218a784df4d1927ae713e17af619caa736cb2ebac287c59e4e24228b22da29" + url: "https://pub.dev" source: hosted version: "2.0.2" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "6182294da5abf431177fccc1ee02401f6df30f766bc6130a0852c6b6d7ee6b2d" + url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.4.18" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" sdks: - dart: ">=2.18.5 <3.0.0" + dart: ">=2.19.0 <4.0.0" diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 7d19f17..e323642 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -52,6 +52,7 @@ add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build; see runner/CMakeLists.txt. add_subdirectory("runner") + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt index 17411a8..394917c 100644 --- a/windows/runner/CMakeLists.txt +++ b/windows/runner/CMakeLists.txt @@ -33,6 +33,7 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # Add dependency libraries and include directories. Add any application-specific # dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") # Run the Flutter tool portions of the build. This must not be removed. diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index b43b909..b25e363 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -26,6 +26,11 @@ bool FlutterWindow::OnCreate() { } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + return true; } diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 1c9d3de..1cef843 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.CreateAndShow(L"huacu_mobile", origin, size)) { + if (!window.Create(L"huacu_mobile", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp index f5bf9fa..b2b0873 100644 --- a/windows/runner/utils.cpp +++ b/windows/runner/utils.cpp @@ -47,16 +47,17 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { } int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr); + -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()) { + 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, - -1, utf8_string.data(), - target_length, nullptr, nullptr); + input_length, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp index c10f08d..60608d0 100644 --- a/windows/runner/win32_window.cpp +++ b/windows/runner/win32_window.cpp @@ -1,13 +1,31 @@ #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; @@ -31,8 +49,8 @@ void EnableFullDpiSupportIfAvailable(HWND hwnd) { GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); - FreeLibrary(user32_module); } + FreeLibrary(user32_module); } } // namespace @@ -42,7 +60,7 @@ class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; - // Returns the singleton registar instance. + // Returns the singleton registrar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); @@ -102,9 +120,9 @@ Win32Window::~Win32Window() { Destroy(); } -bool Win32Window::CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size) { +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { Destroy(); const wchar_t* window_class = @@ -117,7 +135,7 @@ bool Win32Window::CreateAndShow(const std::wstring& title, double scale_factor = dpi / 96.0; HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + 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); @@ -126,9 +144,15 @@ bool Win32Window::CreateAndShow(const std::wstring& title, 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, @@ -188,6 +212,10 @@ Win32Window::MessageHandler(HWND hwnd, SetFocus(child_content_); } return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); @@ -243,3 +271,18 @@ bool Win32Window::OnCreate() { 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/windows/runner/win32_window.h b/windows/runner/win32_window.h index 17ba431..e901dde 100644 --- a/windows/runner/win32_window.h +++ b/windows/runner/win32_window.h @@ -28,15 +28,16 @@ class Win32Window { Win32Window(); virtual ~Win32Window(); - // Creates and shows a win32 window with |title| and position and size using + // 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 to will treat the width height passed in to this function - // as logical pixels and scale to appropriate for the default monitor. Returns - // true if the window was created successfully. - bool CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size); + // 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(); @@ -76,7 +77,7 @@ class Win32Window { // 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 - // responsponds to changes in DPI. All other messages are handled by + // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, @@ -86,6 +87,9 @@ class Win32Window { // 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.