Merge branch 'master' of github.com:refilc/naplo

This commit is contained in:
Kima 2023-08-23 00:04:36 +02:00
commit 40ca5fe0a1
36 changed files with 1982 additions and 789 deletions

View File

@ -4,8 +4,8 @@
# This file should be version controlled. # This file should be version controlled.
version: version:
revision: 3c0bee85b8e43b860877922bdc411a7333db4d32 revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
channel: beta channel: stable
project_type: app project_type: app
@ -13,11 +13,11 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: 3c0bee85b8e43b860877922bdc411a7333db4d32 create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: 3c0bee85b8e43b860877922bdc411a7333db4d32 base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
- platform: macos - platform: web
create_revision: 3c0bee85b8e43b860877922bdc411a7333db4d32 create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: 3c0bee85b8e43b860877922bdc411a7333db4d32 base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
# User provided section # User provided section

View File

@ -62,6 +62,7 @@
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

View File

@ -13,6 +13,7 @@ import 'package:filcnaplo/theme/theme.dart';
import 'package:filcnaplo_kreta_api/client/client.dart'; import 'package:filcnaplo_kreta_api/client/client.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart'; import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:i18n_extension/i18n_widget.dart'; import 'package:i18n_extension/i18n_widget.dart';
@ -194,8 +195,19 @@ class App extends StatelessWidget {
} }
Route? rootNavigator(RouteSettings route) { Route? rootNavigator(RouteSettings route) {
// if platform == android || platform == ios if (kIsWeb) {
if (Platform.isAndroid || Platform.isIOS) { switch (route.name) {
case "login_back":
return CupertinoPageRoute(
builder: (context) => const desktop.LoginScreen(back: true));
case "login":
return _rootRoute(const desktop.LoginScreen());
case "navigation":
return _rootRoute(const desktop.NavigationScreen());
case "login_to_navigation":
return desktop.loginRoute(const desktop.NavigationScreen());
}
} else if (Platform.isAndroid || Platform.isIOS) {
switch (route.name) { switch (route.name) {
case "login_back": case "login_back":
return CupertinoPageRoute( return CupertinoPageRoute(

View File

@ -5,8 +5,10 @@ import 'dart:io';
import 'package:filcnaplo/api/providers/database_provider.dart'; import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/database/struct.dart'; import 'package:filcnaplo/database/struct.dart';
import 'package:filcnaplo/models/settings.dart'; import 'package:filcnaplo/models/settings.dart';
import 'package:flutter/foundation.dart';
// ignore: depend_on_referenced_packages // ignore: depend_on_referenced_packages
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart';
const settingsDB = DatabaseStruct("settings", { const settingsDB = DatabaseStruct("settings", {
"language": String, "start_page": int, "rounding": int, "theme": int, "language": String, "start_page": int, "rounding": int, "theme": int,
@ -48,7 +50,9 @@ Future<void> createTable(Database db, DatabaseStruct struct) =>
Future<Database> initDB(DatabaseProvider database) async { Future<Database> initDB(DatabaseProvider database) async {
Database db; Database db;
if (Platform.isLinux || Platform.isWindows) { if (kIsWeb) {
db = await databaseFactoryFfiWeb.openDatabase("app.db");
} else if (Platform.isLinux || Platform.isWindows) {
sqfliteFfiInit(); sqfliteFfiInit();
db = await databaseFactoryFfi.openDatabase("app.db"); db = await databaseFactoryFfi.openDatabase("app.db");
} else { } else {

View File

@ -6,6 +6,7 @@ import 'package:filcnaplo/api/client.dart';
import 'package:filcnaplo/helpers/storage_helper.dart'; import 'package:filcnaplo/helpers/storage_helper.dart';
import 'package:filcnaplo/models/release.dart'; import 'package:filcnaplo/models/release.dart';
import 'package:open_file/open_file.dart'; import 'package:open_file/open_file.dart';
import 'package:permission_handler/permission_handler.dart';
enum UpdateState { none, preparing, downloading, installing } enum UpdateState { none, preparing, downloading, installing }
@ -32,6 +33,10 @@ extension UpdateHelper on Release {
updateCallback(-1, UpdateState.installing); updateCallback(-1, UpdateState.installing);
var permStatus =
(await Permission.manageExternalStorage.request().isGranted &&
await Permission.requestInstallPackages.request().isGranted);
if (permStatus) {
var result = await OpenFile.open(apk.path); var result = await OpenFile.open(apk.path);
if (result.type != ResultType.done) { if (result.type != ResultType.done) {
@ -39,6 +44,7 @@ extension UpdateHelper on Release {
print("ERROR: installUpdate.openFile: ${result.message}"); print("ERROR: installUpdate.openFile: ${result.message}");
throw result.message; throw result.message;
} }
}
updateCallback(-1, UpdateState.none); updateCallback(-1, UpdateState.none);
} }

View File

@ -32,7 +32,8 @@ void main() async {
runApp(App( runApp(App(
database: startup.database, database: startup.database,
settings: startup.settings, settings: startup.settings,
user: startup.user)); user: startup.user,
));
} }
class Startup { class Startup {
@ -48,13 +49,17 @@ class Startup {
settings = await database.query.getSettings(database); settings = await database.query.getSettings(database);
user = await database.query.getUsers(settings); user = await database.query.getUsers(settings);
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
// Notifications setup // Notifications setup
if (!kIsWeb) {
initPlatformState(); initPlatformState();
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
FlutterLocalNotificationsPlugin(); }
// Get permission to show notifications // Get permission to show notifications
if (Platform.isAndroid) { if (kIsWeb) {
// do nothing
} else if (Platform.isAndroid) {
await flutterLocalNotificationsPlugin await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation< .resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()! AndroidFlutterLocalNotificationsPlugin>()!
@ -77,9 +82,12 @@ class Startup {
badge: true, badge: true,
sound: true, sound: true,
); );
} else if (Platform.isLinux) {
// no permissions are needed on linux
} }
// Platform specific settings // Platform specific settings
if (!kIsWeb) {
const DarwinInitializationSettings initializationSettingsDarwin = const DarwinInitializationSettings initializationSettingsDarwin =
DarwinInitializationSettings( DarwinInitializationSettings(
requestSoundPermission: true, requestSoundPermission: true,
@ -88,17 +96,22 @@ class Startup {
); );
const AndroidInitializationSettings initializationSettingsAndroid = const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('ic_notification'); AndroidInitializationSettings('ic_notification');
const LinuxInitializationSettings initializationSettingsLinux =
LinuxInitializationSettings(defaultActionName: 'Open notification');
const InitializationSettings initializationSettings = const InitializationSettings initializationSettings =
InitializationSettings( InitializationSettings(
android: initializationSettingsAndroid, android: initializationSettingsAndroid,
iOS: initializationSettingsDarwin, iOS: initializationSettingsDarwin,
macOS: initializationSettingsDarwin); macOS: initializationSettingsDarwin,
linux: initializationSettingsLinux,
);
// Initialize notifications // Initialize notifications
await flutterLocalNotificationsPlugin.initialize( await flutterLocalNotificationsPlugin.initialize(
initializationSettings, initializationSettings,
); );
} }
}
} }
bool errorShown = false; bool errorShown = false;

View File

@ -3,7 +3,7 @@ description: "Nem hivatalos e-napló alkalmazás az e-Kréta rendszerhez"
homepage: https://refilc.hu homepage: https://refilc.hu
publish_to: "none" publish_to: "none"
version: 4.1.1+215 version: 4.1.1+216
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"
@ -66,6 +66,8 @@ dependencies:
flutter_local_notifications: ^14.1.0 flutter_local_notifications: ^14.1.0
package_info_plus: ^4.0.2 package_info_plus: ^4.0.2
screenshot: ^2.1.0 screenshot: ^2.1.0
flutter_staggered_grid_view: ^0.7.0
sqflite_common_ffi_web: ^0.4.0
dev_dependencies: dev_dependencies:
flutter_lints: ^2.0.1 flutter_lints: ^2.0.1

View File

@ -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);
});
}

BIN
filcnaplo/web/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

59
filcnaplo/web/index.html Normal file
View File

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="filcnaplo">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>filcnaplo</title>
<link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
var serviceWorkerVersion = null;
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head>
<body>
<script>
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
},
onEntrypointLoaded: function(engineInitializer) {
engineInitializer.initializeEngine().then(function(appRunner) {
appRunner.runApp();
});
}
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,35 @@
{
"name": "filcnaplo",
"short_name": "filcnaplo",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}

17
filcnaplo/windows/.gitignore vendored Normal file
View File

@ -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/

View File

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

View File

@ -0,0 +1,66 @@
#include "flutter_window.h"
#include <optional>
#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<flutter::FlutterViewController>(
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<LRESULT> 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);
}

View File

@ -0,0 +1,33 @@
#ifndef RUNNER_FLUTTER_WINDOW_H_
#define RUNNER_FLUTTER_WINDOW_H_
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <memory>
#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::FlutterViewController> flutter_controller_;
};
#endif // RUNNER_FLUTTER_WINDOW_H_

View File

@ -0,0 +1,43 @@
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>
#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<std::string> 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;
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
</application>
</compatibility>
</assembly>

View File

@ -0,0 +1,65 @@
#include "utils.h"
#include <flutter_windows.h>
#include <io.h>
#include <stdio.h>
#include <windows.h>
#include <iostream>
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<std::string> 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::string>();
}
std::vector<std::string> 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;
}

View File

@ -0,0 +1,19 @@
#ifndef RUNNER_UTILS_H_
#define RUNNER_UTILS_H_
#include <string>
#include <vector>
// 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<std::string>,
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
std::vector<std::string> GetCommandLineArguments();
#endif // RUNNER_UTILS_H_

View File

@ -0,0 +1,288 @@
#include "win32_window.h"
#include <dwmapi.h>
#include <flutter_windows.h>
#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<int>(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<EnableNonClientDpiScaling*>(
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<LONG>(origin.x),
static_cast<LONG>(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<CREATESTRUCT*>(lparam);
SetWindowLongPtr(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
auto that = static_cast<Win32Window*>(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<RECT*>(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<Win32Window*>(
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));
}
}

View File

@ -0,0 +1,102 @@
#ifndef RUNNER_WIN32_WINDOW_H_
#define RUNNER_WIN32_WINDOW_H_
#include <windows.h>
#include <functional>
#include <memory>
#include <string>
// 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_

View File

@ -1,7 +1,7 @@
import 'package:filcnaplo/api/providers/user_provider.dart'; import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/models/settings.dart'; import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/ui/date_widget.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:flutter/material.dart';
import 'package:animated_list_plus/animated_list_plus.dart'; import 'package:animated_list_plus/animated_list_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -16,7 +16,8 @@ class HomePage extends StatefulWidget {
State<HomePage> createState() => _HomePageState(); State<HomePage> createState() => _HomePageState();
} }
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin { class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
late UserProvider user; late UserProvider user;
late SettingsProvider settings; late SettingsProvider settings;
@ -41,11 +42,15 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
user = Provider.of<UserProvider>(context, listen: false); user = Provider.of<UserProvider>(context, listen: false);
DateTime now = DateTime.now(); 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"; 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"; 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"; greeting = "merryxmas";
} else if (now.month == DateTime.january && now.day == 1) { } else if (now.month == DateTime.january && now.day == 1) {
greeting = "happynewyear"; greeting = "happynewyear";
@ -81,7 +86,8 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
children: [ children: [
// Greeting // Greeting
Padding( 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( child: Text(
greeting.i18n.fill([firstName]), greeting.i18n.fill([firstName]),
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
@ -107,8 +113,10 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
int selectedPage = _pageController.page!.round(); int selectedPage = _pageController.page!.round();
if (i == selectedPage) return; if (i == selectedPage) return;
if (_pageController.page?.roundToDouble() != _pageController.page) { if (_pageController.page?.roundToDouble() !=
_pageController.animateToPage(i, curve: Curves.easeIn, duration: kTabScrollDuration); _pageController.page) {
_pageController.animateToPage(i,
curve: Curves.easeIn, duration: kTabScrollDuration);
return; return;
} }
@ -132,27 +140,34 @@ class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin
(BuildContext context, int index) { (BuildContext context, int index) {
return FutureBuilder<List<DateWidget>>( return FutureBuilder<List<DateWidget>>(
key: ValueKey<String>(listOrder[index]), key: ValueKey<String>(listOrder[index]),
future: getFilterWidgets(homeFilters[index], context: context), future: getFilterWidgets(homeFilters[index],
builder: (context, dateWidgets) => dateWidgets.data != null context: context),
builder: (context, dateWidgets) => dateWidgets.data !=
null
? ImplicitlyAnimatedList<Widget>( ? ImplicitlyAnimatedList<Widget>(
items: sortDateWidgets(context, dateWidgets: dateWidgets.data!), items: sortDateWidgets(context,
dateWidgets: dateWidgets.data!),
itemBuilder: filterItemBuilder, itemBuilder: filterItemBuilder,
spawnIsolate: false, spawnIsolate: false,
areItemsTheSame: (a, b) => a.key == b.key, areItemsTheSame: (a, b) => a.key == b.key,
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), physics: const BouncingScrollPhysics(
padding: const EdgeInsets.symmetric(horizontal: 24.0), parent: AlwaysScrollableScrollPhysics()),
padding: const EdgeInsets.symmetric(
horizontal: 24.0),
) )
: Container(), : Container(),
); );
}, },
childCount: 4, childCount: 4,
findChildIndexCallback: (Key key) { findChildIndexCallback: (Key key) {
final ValueKey<String> valueKey = key as ValueKey<String>; final ValueKey<String> valueKey =
key as ValueKey<String>;
final String data = valueKey.value; final String data = valueKey.value;
return listOrder.indexOf(data); return listOrder.indexOf(data);
}, },
), ),
physics: const PageScrollPhysics().applyTo(const BouncingScrollPhysics()), physics: const PageScrollPhysics()
.applyTo(const BouncingScrollPhysics()),
), ),
), ),
], ],

View File

@ -23,12 +23,14 @@ import 'timetable_page.i18n.dart';
// todo: "fix" overflow (priority: -1) // todo: "fix" overflow (priority: -1)
class TimetablePage extends StatefulWidget { 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 DateTime? initialDay;
final Week? initialWeek; 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 // Go to timetable page with arguments
// NavigationScreen.of(context)?.customRoute(navigationPageRoute((context) => TimetablePage( // NavigationScreen.of(context)?.customRoute(navigationPageRoute((context) => TimetablePage(
// initialDay: lesson?.date ?? day, // initialDay: lesson?.date ?? day,
@ -49,7 +51,8 @@ class TimetablePage extends StatefulWidget {
_TimetablePageState createState() => _TimetablePageState(); _TimetablePageState createState() => _TimetablePageState();
} }
class _TimetablePageState extends State<TimetablePage> with TickerProviderStateMixin { class _TimetablePageState extends State<TimetablePage>
with TickerProviderStateMixin {
late UserProvider user; late UserProvider user;
late TimetableProvider timetableProvider; late TimetableProvider timetableProvider;
late UpdateProvider updateProvider; late UpdateProvider updateProvider;
@ -60,7 +63,9 @@ class _TimetablePageState extends State<TimetablePage> with TickerProviderStateM
int _getDayIndex(DateTime date) { int _getDayIndex(DateTime date) {
int index = 0; 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 // find the first day with upcoming lessons
index = _controller.days!.indexWhere((day) => day.last.end.isAfter(date)); index = _controller.days!.indexWhere((day) => day.last.end.isAfter(date));
@ -94,11 +99,14 @@ class _TimetablePageState extends State<TimetablePage> with TickerProviderStateM
_tabController = TabController( _tabController = TabController(
length: _controller.days!.length, length: _controller.days!.length,
vsync: this, 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) { if (initial ||
_tabController.animateTo(_getDayIndex(widget.initialDay ?? DateTime.now())); _controller.previousWeekId != _controller.currentWeekId) {
_tabController
.animateTo(_getDayIndex(widget.initialDay ?? DateTime.now()));
} }
initial = false; initial = false;
@ -111,7 +119,8 @@ class _TimetablePageState extends State<TimetablePage> with TickerProviderStateM
if (widget.initialWeek != null) { if (widget.initialWeek != null) {
_controller.jump(widget.initialWeek!, context: context, initial: true); _controller.jump(widget.initialWeek!, context: context, initial: true);
} else { } 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 // Listen for user changes
@ -131,7 +140,8 @@ class _TimetablePageState extends State<TimetablePage> with TickerProviderStateM
// Sometimes when changing weeks really fast, // Sometimes when changing weeks really fast,
// controller.days might be null or won't include index // controller.days might be null or won't include index
try { 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) { } catch (e) {
return "timetable".i18n; return "timetable".i18n;
} }
@ -181,9 +191,14 @@ class _TimetablePageState extends State<TimetablePage> with TickerProviderStateM
children: [ children: [
// Day Title // Day Title
Padding( 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( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
dayTitle(tab).capital(), dayTitle(tab).capital(),
@ -193,9 +208,13 @@ class _TimetablePageState extends State<TimetablePage> with TickerProviderStateM
), ),
), ),
Text( Text(
"${_controller.days![tab].first.date.day}".padLeft(2, '0') + ".", "${_controller.days![tab].first.date.day}"
.padLeft(2, '0') +
".",
style: TextStyle( style: TextStyle(
color: AppColors.of(context).text.withOpacity(.5), color: AppColors.of(context)
.text
.withOpacity(.5),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
@ -208,35 +227,59 @@ class _TimetablePageState extends State<TimetablePage> with TickerProviderStateM
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
itemCount: _controller.days![tab].length + 2, itemCount:
_controller.days![tab].length + 2,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (_controller.days == null) return Container(); if (_controller.days == null) {
return Container();
}
// Header // Header
if (index == 0) { if (index == 0) {
return const Padding( return const Padding(
padding: EdgeInsets.only(top: 8.0, left: 24.0, right: 24.0), padding: EdgeInsets.only(
child: PanelHeader(padding: EdgeInsets.only(top: 12.0)), top: 8.0,
left: 24.0,
right: 24.0),
child: PanelHeader(
padding: EdgeInsets.only(
top: 12.0)),
); );
} }
// Footer // Footer
if (index == _controller.days![tab].length + 1) { if (index ==
_controller.days![tab].length +
1) {
return const Padding( return const Padding(
padding: EdgeInsets.only(bottom: 8.0, left: 24.0, right: 24.0), padding: EdgeInsets.only(
child: PanelFooter(padding: EdgeInsets.only(top: 12.0)), bottom: 8.0,
left: 24.0,
right: 24.0),
child: PanelFooter(
padding: EdgeInsets.only(
top: 12.0)),
); );
} }
// Body // Body
final Lesson lesson = _controller.days![tab][index - 1]; final Lesson lesson =
final bool swapDescDay = _controller.days![tab].map((l) => l.swapDesc ? 1 : 0).reduce((a, b) => a + b) >= _controller.days![tab][index - 1];
_controller.days![tab].length * .5; final bool swapDescDay = _controller
.days![tab]
.map(
(l) => l.swapDesc ? 1 : 0)
.reduce((a, b) => a + b) >=
_controller.days![tab].length *
.5;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0), padding: const EdgeInsets.symmetric(
horizontal: 24.0),
child: PanelBody( child: PanelBody(
padding: const EdgeInsets.symmetric(horizontal: 10.0), padding:
const EdgeInsets.symmetric(
horizontal: 10.0),
child: LessonViewable( child: LessonViewable(
lesson, lesson,
swapDesc: swapDescDay, swapDesc: swapDescDay,
@ -264,7 +307,8 @@ class _TimetablePageState extends State<TimetablePage> with TickerProviderStateM
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -285,7 +329,10 @@ class _TimetablePageState extends State<TimetablePage> with TickerProviderStateM
onTap: () => setState(() { onTap: () => setState(() {
_controller.current(); _controller.current();
if (mounted) { 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( child: Padding(
@ -295,12 +342,22 @@ class _TimetablePageState extends State<TimetablePage> with TickerProviderStateM
"week".i18n + "week".i18n +
" (" + " (" +
// Week start // 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) I18n.of(context).locale.languageCode)
.format(_controller.currentWeek.start) + .format(_controller.currentWeek.start) +
" - " + " - " +
// Week end // 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) I18n.of(context).locale.languageCode)
.format(_controller.currentWeek.end) + .format(_controller.currentWeek.end) +
")", ")",

View File

@ -6,6 +6,7 @@ import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/utils/color.dart'; import 'package:filcnaplo/utils/color.dart';
import 'package:filcnaplo_desktop_ui/common/panel_button.dart'; import 'package:filcnaplo_desktop_ui/common/panel_button.dart';
import 'package:filcnaplo_desktop_ui/common/profile_image.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/navigation/sidebar_action.dart';
import 'package:filcnaplo_desktop_ui/screens/settings/settings_screen.dart'; import 'package:filcnaplo_desktop_ui/screens/settings/settings_screen.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/accounts/account_tile.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'; import 'package:filcnaplo/theme/colors/colors.dart';
class Sidebar extends StatefulWidget { 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 NavigatorState navigator;
final String selected; final String selected;
@ -71,12 +77,12 @@ class _SidebarState extends State<Sidebar> {
if (!settings.presentationMode) { if (!settings.presentationMode) {
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0]; firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
} else { } else {
firstName = "Béla"; firstName = "János";
} }
List<Widget> pageWidgets = [ List<Widget> pageWidgets = [
SidebarAction( SidebarAction(
title: const Text("Home"), title: Text("Home".i18n),
icon: const Icon(FilcIcons.home), icon: const Icon(FilcIcons.home),
selected: widget.selected == "home", selected: widget.selected == "home",
onTap: () { onTap: () {
@ -87,7 +93,7 @@ class _SidebarState extends State<Sidebar> {
}, },
), ),
SidebarAction( SidebarAction(
title: const Text("Grades"), title: Text("Grades".i18n),
icon: const Icon(FeatherIcons.bookmark), icon: const Icon(FeatherIcons.bookmark),
selected: widget.selected == "grades", selected: widget.selected == "grades",
onTap: () { onTap: () {
@ -98,7 +104,7 @@ class _SidebarState extends State<Sidebar> {
}, },
), ),
SidebarAction( SidebarAction(
title: const Text("Timetable"), title: Text("Timetable".i18n),
icon: const Icon(FeatherIcons.calendar), icon: const Icon(FeatherIcons.calendar),
selected: widget.selected == "timetable", selected: widget.selected == "timetable",
onTap: () { onTap: () {
@ -109,7 +115,7 @@ class _SidebarState extends State<Sidebar> {
}, },
), ),
SidebarAction( SidebarAction(
title: const Text("Messages"), title: Text("Messages".i18n),
icon: const Icon(FeatherIcons.messageSquare), icon: const Icon(FeatherIcons.messageSquare),
selected: widget.selected == "messages", selected: widget.selected == "messages",
onTap: () { onTap: () {
@ -120,7 +126,7 @@ class _SidebarState extends State<Sidebar> {
}, },
), ),
SidebarAction( SidebarAction(
title: const Text("Absences"), title: Text("Absences".i18n),
icon: const Icon(FeatherIcons.clock), icon: const Icon(FeatherIcons.clock),
selected: widget.selected == "absences", selected: widget.selected == "absences",
onTap: () { onTap: () {
@ -134,12 +140,15 @@ class _SidebarState extends State<Sidebar> {
List<Widget> bottomActions = [ List<Widget> bottomActions = [
SidebarAction( SidebarAction(
title: const Text("Settings"), title: Text("Settings".i18n),
selected: true, selected: true,
icon: const Icon(FeatherIcons.settings), icon: const Icon(FeatherIcons.settings),
onTap: () { onTap: () {
if (topNav != "settings") { 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"; topNav = "settings";
} }
}, },
@ -159,7 +168,7 @@ class _SidebarState extends State<Sidebar> {
onPressed: () { onPressed: () {
Navigator.of(context).pushNamed("login_back"); Navigator.of(context).pushNamed("login_back");
}, },
title: const Text("Add User"), title: Text("adduser".i18n),
leading: const Icon(FeatherIcons.userPlus), leading: const Icon(FeatherIcons.userPlus),
), ),
PanelButton( PanelButton(
@ -169,17 +178,20 @@ class _SidebarState extends State<Sidebar> {
// Delete User // Delete User
user.removeUser(userId); user.removeUser(userId);
await Provider.of<DatabaseProvider>(context, listen: false).store.removeUser(userId); await Provider.of<DatabaseProvider>(context, listen: false)
.store
.removeUser(userId);
// If no other Users left, go back to LoginScreen // If no other Users left, go back to LoginScreen
if (user.getUsers().isNotEmpty) { if (user.getUsers().isNotEmpty) {
user.setUser(user.getUsers().first.id); user.setUser(user.getUsers().first.id);
restore().then((_) => user.setUser(user.getUsers().first.id)); restore().then((_) => user.setUser(user.getUsers().first.id));
} else { } 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), leading: Icon(FeatherIcons.logOut, color: AppColors.of(context).red),
), ),
]; ];
@ -190,16 +202,37 @@ class _SidebarState extends State<Sidebar> {
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(left: 18.0, top: 18.0, bottom: 24.0, right: 8.0), 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( child: Row(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(right: 12.0), padding: const EdgeInsets.only(
right: 12.0,
left: 5.0,
top: 5.0,
bottom: 5.0,
),
child: ProfileImage( child: ProfileImage(
name: firstName, name: firstName,
radius: 18.0, radius: 18.0,
backgroundColor: backgroundColor: Theme.of(context)
!settings.presentationMode ? ColorUtils.stringToColor(user.name ?? "?") : Theme.of(context).colorScheme.secondary, .colorScheme
.primary, //!settings.presentationMode
// ? ColorUtils.stringToColor(user.name ?? "?")
// : Theme.of(context).colorScheme.secondary,
), ),
), ),
Expanded( Expanded(
@ -212,7 +245,8 @@ class _SidebarState extends State<Sidebar> {
), ),
), ),
PageTransitionSwitcher( PageTransitionSwitcher(
transitionBuilder: (child, primaryAnimation, secondaryAnimation) { transitionBuilder:
(child, primaryAnimation, secondaryAnimation) {
return FadeThroughTransition( return FadeThroughTransition(
fillColor: Colors.transparent, fillColor: Colors.transparent,
animation: primaryAnimation, animation: primaryAnimation,
@ -222,19 +256,25 @@ class _SidebarState extends State<Sidebar> {
}, },
child: IconButton( child: IconButton(
key: Key(expandAccount ? "accounts" : "pages"), key: Key(expandAccount ? "accounts" : "pages"),
icon: Icon(expandAccount ? FeatherIcons.chevronDown : FeatherIcons.chevronRight), icon: Icon(expandAccount
? FeatherIcons.chevronDown
: FeatherIcons.chevronRight),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
splashRadius: 20.0,
onPressed: () { onPressed: () {
setState(() { setState(() {
expandAccount = !expandAccount; expandAccount = !expandAccount;
}); });
}, },
splashColor: const Color(0x00000000),
focusColor: const Color(0x00000000),
hoverColor: const Color(0x00000000),
highlightColor: const Color(0x00000000),
), ),
), ),
], ],
), ),
), ),
),
// Pages // Pages
Expanded( Expanded(
@ -282,15 +322,19 @@ class _SidebarState extends State<Sidebar> {
if (!settings.presentationMode) { if (!settings.presentationMode) {
_firstName = _nameParts.length > 1 ? _nameParts[1] : _nameParts[0]; _firstName = _nameParts.length > 1 ? _nameParts[1] : _nameParts[0];
} else { } else {
_firstName = "Béla"; _firstName = "János";
} }
accountTiles.add(AccountTile( accountTiles.add(AccountTile(
name: Text(!settings.presentationMode ? account.name : "Béla", style: const TextStyle(fontWeight: FontWeight.w500)), name: Text(!settings.presentationMode ? account.name : "János",
username: Text(!settings.presentationMode ? account.username : "72469696969"), style: const TextStyle(fontWeight: FontWeight.w500)),
username:
Text(!settings.presentationMode ? account.username : "72469696969"),
profileImage: ProfileImage( profileImage: ProfileImage(
name: _firstName, 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, role: account.role,
), ),
onTap: () { onTap: () {

View File

@ -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<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@ -35,6 +35,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_custom_tabs/flutter_custom_tabs.dart' as tabs; import 'package:flutter_custom_tabs/flutter_custom_tabs.dart' as tabs;
import 'package:flutter_feather_icons/flutter_feather_icons.dart'; 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:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'settings_screen.i18n.dart'; import 'settings_screen.i18n.dart';
@ -86,11 +87,11 @@ class _SettingsScreenState extends State<SettingsScreen>
if (!settings.presentationMode) { if (!settings.presentationMode) {
_firstName = _nameParts.length > 1 ? _nameParts[1] : _nameParts[0]; _firstName = _nameParts.length > 1 ? _nameParts[1] : _nameParts[0];
} else { } else {
_firstName = "Béla"; _firstName = "János";
} }
accountTiles.add(AccountTile( 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)), style: const TextStyle(fontWeight: FontWeight.w500)),
username: username:
Text(!settings.presentationMode ? account.username : "72469696969"), Text(!settings.presentationMode ? account.username : "72469696969"),
@ -164,7 +165,7 @@ class _SettingsScreenState extends State<SettingsScreen>
if (!settings.presentationMode) { if (!settings.presentationMode) {
firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0]; firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0];
} else { } else {
firstName = "Béla"; firstName = "János";
} }
String startPageTitle = String startPageTitle =
@ -206,14 +207,21 @@ class _SettingsScreenState extends State<SettingsScreen>
animation: _hideContainersController, animation: _hideContainersController,
builder: (context, child) => Opacity( builder: (context, child) => Opacity(
opacity: 1 - _hideContainersController.value, opacity: 1 - _hideContainersController.value,
child: SingleChildScrollView(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
StaggeredGrid.extent(
// direction: Axis.horizontal,
// crossAxisCount: 3,
maxCrossAxisExtent: 600,
children: [ children: [
const SizedBox(height: 32.0), const SizedBox(height: 32.0),
// Updates // Updates
if (updateProvider.available) if (updateProvider.available)
Padding( Container(
constraints: const BoxConstraints(maxWidth: 500),
child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 24.0), vertical: 12.0, horizontal: 24.0),
child: Panel( child: Panel(
@ -225,8 +233,10 @@ class _SettingsScreenState extends State<SettingsScreen>
updateProvider.releases.first.tag, updateProvider.releases.first.tag,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: color: Theme.of(context)
Theme.of(context).colorScheme.secondary, .colorScheme
.secondary,
),
), ),
), ),
), ),
@ -246,7 +256,9 @@ class _SettingsScreenState extends State<SettingsScreen>
), ),
// General Settings // General Settings
Padding( Container(
constraints: const BoxConstraints(maxWidth: 500),
child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 24.0), vertical: 12.0, horizontal: 24.0),
child: Panel( child: Panel(
@ -277,7 +289,8 @@ class _SettingsScreenState extends State<SettingsScreen>
setState(() {}); setState(() {});
}, },
title: Text("rounding".i18n), title: Text("rounding".i18n),
leading: const Icon(FeatherIcons.gitCommit), leading:
const Icon(FeatherIcons.gitCommit),
trailing: Text((settings.rounding / 10) trailing: Text((settings.rounding / 10)
.toStringAsFixed(1)), .toStringAsFixed(1)),
), ),
@ -291,7 +304,8 @@ class _SettingsScreenState extends State<SettingsScreen>
trailing: Text(vibrateTitle), trailing: Text(vibrateTitle),
), ),
PanelButton( PanelButton(
padding: const EdgeInsets.only(left: 14.0), padding:
const EdgeInsets.only(left: 14.0),
onPressed: () { onPressed: () {
SettingsHelper.bellDelay(context); SettingsHelper.bellDelay(context);
setState(() {}); setState(() {});
@ -314,20 +328,24 @@ class _SettingsScreenState extends State<SettingsScreen>
.withOpacity(.25)), .withOpacity(.25)),
trailingDivider: true, trailingDivider: true,
trailing: Switch( trailing: Switch(
onChanged: (v) => onChanged: (v) => settings.update(
settings.update(bellDelayEnabled: v), bellDelayEnabled: v),
value: settings.bellDelayEnabled, value: settings.bellDelayEnabled,
activeColor: activeColor: Theme.of(context)
Theme.of(context).colorScheme.secondary, .colorScheme
.secondary,
), ),
), ),
], ],
), ),
), ),
), ),
),
if (kDebugMode) if (kDebugMode)
Padding( Container(
constraints: const BoxConstraints(maxWidth: 500),
child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 24.0), vertical: 12.0, horizontal: 24.0),
child: Panel( child: Panel(
@ -335,12 +353,15 @@ class _SettingsScreenState extends State<SettingsScreen>
child: Column( child: Column(
children: [ children: [
PanelButton( PanelButton(
title: const Text("Subject Icon Gallery"), title:
const Text("Subject Icon Gallery"),
leading: const Icon(CupertinoIcons leading: const Icon(CupertinoIcons
.rectangle_3_offgrid_fill), .rectangle_3_offgrid_fill),
trailing: const Icon(Icons.arrow_forward), trailing:
const Icon(Icons.arrow_forward),
onPressed: () { onPressed: () {
Navigator.of(context, rootNavigator: true) Navigator.of(context,
rootNavigator: true)
.push( .push(
CupertinoPageRoute( CupertinoPageRoute(
builder: (context) => builder: (context) =>
@ -352,10 +373,13 @@ class _SettingsScreenState extends State<SettingsScreen>
), ),
), ),
), ),
),
// Secret Settings // Secret Settings
if (__ss) if (__ss)
Padding( Container(
constraints: const BoxConstraints(maxWidth: 500),
child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 24.0), vertical: 12.0, horizontal: 24.0),
child: Panel( child: Panel(
@ -378,28 +402,35 @@ class _SettingsScreenState extends State<SettingsScreen>
if (v) { if (v) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => WillPopScope( builder: (context) =>
WillPopScope(
onWillPop: () async => false, onWillPop: () async => false,
child: AlertDialog( child: AlertDialog(
shape: RoundedRectangleBorder( shape:
RoundedRectangleBorder(
borderRadius: borderRadius:
BorderRadius.circular( BorderRadius
.circular(
12.0)), 12.0)),
title: Text("attention".i18n), title:
Text("attention".i18n),
content: Text( content: Text(
"goodstudent_disclaimer" "goodstudent_disclaimer"
.i18n), .i18n),
actions: [ actions: [
ActionButton( ActionButton(
label: "understand".i18n, label:
"understand".i18n,
onTap: () { onTap: () {
Navigator.of(context) Navigator.of(
context)
.pop(); .pop();
settings.update( settings.update(
goodStudent: v); goodStudent: v);
Provider.of<GradeProvider>( Provider.of<GradeProvider>(
context, context,
listen: false) listen:
false)
.fetch(); .fetch();
}) })
], ],
@ -408,7 +439,8 @@ class _SettingsScreenState extends State<SettingsScreen>
); );
} else { } else {
settings.update(goodStudent: v); settings.update(goodStudent: v);
Provider.of<GradeProvider>(context, Provider.of<GradeProvider>(
context,
listen: false) listen: false)
.fetch(); .fetch();
} }
@ -432,8 +464,8 @@ class _SettingsScreenState extends State<SettingsScreen>
title: const Text("Presentation Mode", title: const Text("Presentation Mode",
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w500)), fontWeight: FontWeight.w500)),
onChanged: (v) => onChanged: (v) => settings.update(
settings.update(presentationMode: v), presentationMode: v),
value: settings.presentationMode, value: settings.presentationMode,
activeColor: Theme.of(context) activeColor: Theme.of(context)
.colorScheme .colorScheme
@ -444,9 +476,12 @@ class _SettingsScreenState extends State<SettingsScreen>
), ),
), ),
), ),
),
// Theme Settings // Theme Settings
Padding( Container(
constraints: const BoxConstraints(maxWidth: 500),
child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 24.0), vertical: 12.0, horizontal: 24.0),
child: Panel( child: Panel(
@ -464,7 +499,8 @@ class _SettingsScreenState extends State<SettingsScreen>
), ),
PanelButton( PanelButton(
onPressed: () async { onPressed: () async {
await _hideContainersController.forward(); await _hideContainersController
.forward();
SettingsHelper.accentColor(context); SettingsHelper.accentColor(context);
setState(() {}); setState(() {});
_hideContainersController.reset(); _hideContainersController.reset();
@ -494,8 +530,8 @@ class _SettingsScreenState extends State<SettingsScreen>
children: List.generate( children: List.generate(
5, 5,
(i) => Container( (i) => Container(
margin: margin: const EdgeInsets.only(
const EdgeInsets.only(left: 2.0), left: 2.0),
width: 12.0, width: 12.0,
height: 12.0, height: 12.0,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -547,8 +583,9 @@ class _SettingsScreenState extends State<SettingsScreen>
onChanged: (v) => onChanged: (v) =>
settings.update(graphClassAvg: v), settings.update(graphClassAvg: v),
value: settings.graphClassAvg, value: settings.graphClassAvg,
activeColor: activeColor: Theme.of(context)
Theme.of(context).colorScheme.secondary, .colorScheme
.secondary,
), ),
), ),
const PremiumIconPackSelector(), const PremiumIconPackSelector(),
@ -556,9 +593,12 @@ class _SettingsScreenState extends State<SettingsScreen>
), ),
), ),
), ),
),
// Notifications // Notifications
Padding( Container(
constraints: const BoxConstraints(maxWidth: 500),
child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 24.0), vertical: 12.0, horizontal: 24.0),
child: Panel( child: Panel(
@ -569,7 +609,8 @@ class _SettingsScreenState extends State<SettingsScreen>
contentPadding: contentPadding:
const EdgeInsets.only(left: 12.0), const EdgeInsets.only(left: 12.0),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)), borderRadius:
BorderRadius.circular(12.0)),
title: Row( title: Row(
children: [ children: [
Icon( Icon(
@ -591,7 +632,8 @@ class _SettingsScreenState extends State<SettingsScreen>
fontSize: 16.0, fontSize: 16.0,
color: AppColors.of(context) color: AppColors.of(context)
.text .text
.withOpacity(settings.newsEnabled .withOpacity(
settings.newsEnabled
? 1.0 ? 1.0
: .5), : .5),
), ),
@ -608,9 +650,12 @@ class _SettingsScreenState extends State<SettingsScreen>
), ),
), ),
), ),
),
// Extras // Extras
Padding( Container(
constraints: const BoxConstraints(maxWidth: 500),
child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 24.0), vertical: 12.0, horizontal: 24.0),
child: Panel( child: Panel(
@ -657,16 +702,20 @@ class _SettingsScreenState extends State<SettingsScreen>
onChanged: (v) => onChanged: (v) =>
settings.update(gradeOpeningFun: v), settings.update(gradeOpeningFun: v),
value: settings.gradeOpeningFun, value: settings.gradeOpeningFun,
activeColor: activeColor: Theme.of(context)
Theme.of(context).colorScheme.secondary, .colorScheme
.secondary,
), ),
), ),
]), ]),
), ),
), ),
),
// About // About
Padding( Container(
constraints: const BoxConstraints(maxWidth: 500),
child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 24.0), vertical: 12.0, horizontal: 24.0),
child: Panel( child: Panel(
@ -676,7 +725,8 @@ class _SettingsScreenState extends State<SettingsScreen>
leading: const Icon(FeatherIcons.atSign), leading: const Icon(FeatherIcons.atSign),
title: const Text("Discord"), title: const Text("Discord"),
onPressed: () => launchUrl( onPressed: () => launchUrl(
Uri.parse("https://filcnaplo.hu/discord"), Uri.parse(
"https://filcnaplo.hu/discord"),
mode: LaunchMode.externalApplication), mode: LaunchMode.externalApplication),
), ),
PanelButton( PanelButton(
@ -765,9 +815,10 @@ class _SettingsScreenState extends State<SettingsScreen>
String newId; String newId;
if (v == false) { if (v == false) {
newId = "none"; newId = "none";
} else if (settings.xFilcId == "none") { } else if (settings.xFilcId ==
newId = "none") {
SettingsProvider.defaultSettings() newId = SettingsProvider
.defaultSettings()
.xFilcId; .xFilcId;
} else { } else {
newId = settings.xFilcId; newId = settings.xFilcId;
@ -775,16 +826,20 @@ class _SettingsScreenState extends State<SettingsScreen>
settings.update(xFilcId: newId); settings.update(xFilcId: newId);
}, },
value: settings.xFilcId != "none", value: settings.xFilcId != "none",
activeColor: activeColor: Theme.of(context)
Theme.of(context).colorScheme.secondary, .colorScheme
.secondary,
), ),
), ),
), ),
]), ]),
), ),
), ),
),
if (settings.developerMode) if (settings.developerMode)
Padding( Container(
constraints: const BoxConstraints(maxWidth: 500),
child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 12.0, horizontal: 24.0), vertical: 12.0, horizontal: 24.0),
child: Panel( child: Panel(
@ -802,8 +857,8 @@ class _SettingsScreenState extends State<SettingsScreen>
title: const Text("Developer Mode", title: const Text("Developer Mode",
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w500)), fontWeight: FontWeight.w500)),
onChanged: (v) => onChanged: (v) => settings.update(
settings.update(developerMode: false), developerMode: false),
value: settings.developerMode, value: settings.developerMode,
activeColor: Theme.of(context) activeColor: Theme.of(context)
.colorScheme .colorScheme
@ -820,28 +875,36 @@ class _SettingsScreenState extends State<SettingsScreen>
listen: false) listen: false)
.accessToken!)), .accessToken!)),
), ),
if (Provider.of<PremiumProvider>(context, // if (Provider.of<PremiumProvider>(context,
listen: false) // listen: false)
.hasPremium) // .hasPremium)
PanelButton( // PanelButton(
leading: const Icon(FeatherIcons.key), // leading: const Icon(FeatherIcons.key),
title: const Text("Remove Premium"), // title: const Text("Remove Premium"),
onPressed: () { // onPressed: () {
Provider.of<PremiumProvider>(context, // Provider.of<PremiumProvider>(
listen: false) // context,
.activate(removePremium: true); // listen: false)
settings.update( // .activate(removePremium: true);
accentColor: AccentColor.filc, // settings.update(
store: true); // accentColor: AccentColor.filc,
Provider.of<ThemeModeObserver>(context, // store: true);
listen: false) // Provider.of<ThemeModeObserver>(
.changeTheme(settings.theme); // context,
}, // listen: false)
), // .changeTheme(settings.theme);
// },
// ),
], ],
), ),
), ),
), ),
),
],
),
const SizedBox(
height: 40,
),
SafeArea( SafeArea(
top: false, top: false,
child: Center( child: Center(
@ -880,7 +943,6 @@ class _SettingsScreenState extends State<SettingsScreen>
), ),
), ),
), ),
)
], ],
), ),
), ),

View File

@ -25,6 +25,7 @@ dependencies:
auto_size_text: ^3.0.0 auto_size_text: ^3.0.0
flutter_acrylic: ^1.1.3 flutter_acrylic: ^1.1.3
elegant_notification: ^1.6.1 elegant_notification: ^1.6.1
flutter_staggered_grid_view: ^0.7.0
dev_dependencies: dev_dependencies:
flutter_lints: ^1.0.0 flutter_lints: ^1.0.0

View File

@ -169,13 +169,13 @@ class _PersonalityCardState extends State<PersonalityCard> {
} else if (mostCommonGrade.keys.toList()[0] == 1 && } else if (mostCommonGrade.keys.toList()[0] == 1 &&
mostCommonGrade.values.toList()[0] > 1) { mostCommonGrade.values.toList()[0] > 1) {
finalPersonality = PersonalityType.fallible; finalPersonality = PersonalityType.fallible;
} else if (absences.length < 10) { } else if (absences.length <= 12) {
finalPersonality = PersonalityType.healthy; finalPersonality = PersonalityType.healthy;
} else if (unexcusedAbsences >= 10) { } else if (unexcusedAbsences >= 8) {
finalPersonality = PersonalityType.quitter; finalPersonality = PersonalityType.quitter;
} else if (totalDelays > 50) { } else if (totalDelays > 50) {
finalPersonality = PersonalityType.late; finalPersonality = PersonalityType.late;
} else if (absences.length >= 100) { } else if (absences.length >= 120) {
finalPersonality = PersonalityType.sick; finalPersonality = PersonalityType.sick;
} else if (mostCommonGrade.keys.toList()[0] == 2) { } else if (mostCommonGrade.keys.toList()[0] == 2) {
finalPersonality = PersonalityType.acceptable; finalPersonality = PersonalityType.acceptable;

View File

@ -8,7 +8,9 @@ import 'package:flutter/material.dart';
import 'absence_group_tile.i18n.dart'; import 'absence_group_tile.i18n.dart';
class AbsenceGroupTile extends StatelessWidget { 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<AbsenceViewable> absences; final List<AbsenceViewable> absences;
final bool showDate; final bool showDate;
@ -16,10 +18,12 @@ class AbsenceGroupTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { 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); 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( return ClipRRect(
borderRadius: BorderRadius.circular(14.0), borderRadius: BorderRadius.circular(14.0),
@ -29,6 +33,8 @@ class AbsenceGroupTile extends StatelessWidget {
padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0), padding: padding ?? const EdgeInsets.symmetric(horizontal: 8.0),
child: AbsenceGroupContainer( child: AbsenceGroupContainer(
child: ExpansionTile( child: ExpansionTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
tilePadding: const EdgeInsets.symmetric(horizontal: 8.0), tilePadding: const EdgeInsets.symmetric(horizontal: 8.0),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
leading: Container( leading: Container(
@ -38,22 +44,33 @@ class AbsenceGroupTile extends StatelessWidget {
shape: BoxShape.circle, shape: BoxShape.circle,
color: color.withOpacity(.25), 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( title: Text.rich(TextSpan(
text: "${absences.where((a) => a.absence.state == state).length} ", text:
style: TextStyle(fontWeight: FontWeight.w700, color: AppColors.of(context).text), "${absences.where((a) => a.absence.state == state).length} ",
style: TextStyle(
fontWeight: FontWeight.w700,
color: AppColors.of(context).text),
children: [ children: [
TextSpan( TextSpan(
text: AbsenceTile.justificationName(state).fill(["absence".i18n]), text: AbsenceTile.justificationName(state)
style: TextStyle(fontWeight: FontWeight.w600, color: AppColors.of(context).text), .fill(["absence".i18n]),
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppColors.of(context).text),
), ),
], ],
)), )),
subtitle: showDate subtitle: showDate
? Text( ? Text(
absences.first.absence.date.format(context, weekday: true), absences.first.absence.date
style: TextStyle(fontWeight: FontWeight.w500, color: AppColors.of(context).text.withOpacity(0.8)), .format(context, weekday: true),
style: TextStyle(
fontWeight: FontWeight.w500,
color: AppColors.of(context).text.withOpacity(0.8)),
) )
: null, : null,
children: absences, children: absences,

View File

@ -118,10 +118,13 @@ class _AllSumBodyState extends State<AllSumBody> {
void getDelays() { void getDelays() {
var allDelays = absenceProvider.absences.where((a) => a.delay > 0); var allDelays = absenceProvider.absences.where((a) => a.delay > 0);
var totalDelayTime = (allDelays.map((a) { var delayTimeList = (allDelays.map((a) {
return a.delay; return a.delay;
}).toList()) }).toList());
.reduce((a, b) => a + b); var totalDelayTime = 0;
if (delayTimeList.isNotEmpty) {
totalDelayTime = delayTimeList.reduce((a, b) => a + b);
}
var unexcusedDelays = absenceProvider.absences var unexcusedDelays = absenceProvider.absences
.where((a) => a.state == Justification.unexcused && a.delay > 0); .where((a) => a.state == Justification.unexcused && a.delay > 0);