From baec76c29fd6757408dd2ef582703626ccbfde70 Mon Sep 17 00:00:00 2001
From: ReinerRego <59338514+ReinerRego@users.noreply.github.com>
Date: Fri, 26 May 2023 21:50:08 +0200
Subject: [PATCH] Add files via upload

---
 filcnaplo_premium/README.md                   |   3 +
 filcnaplo_premium/analysis_options.yaml       |  28 +
 .../android/database/DBManager.java           | 119 +++
 .../android/database/SQLiteHelper.java        |  36 +
 filcnaplo_premium/android/local.properties    |   2 +
 filcnaplo_premium/android/utils/Utils.java    |  36 +
 filcnaplo_premium/android/utils/Week.java     |  65 ++
 .../widget_timetable/WidgetTimetable.java     | 397 +++++++++
 .../WidgetTimetableDataProvider.java          | 354 ++++++++
 .../WidgetTimetableService.java               |  12 +
 filcnaplo_premium/lib/api/auth.dart           | 120 +++
 .../lib/models/premium_result.dart            |  19 +
 .../lib/models/premium_scopes.dart            |  32 +
 .../lib/providers/premium_provider.dart       |  28 +
 .../flutter_colorpicker/block_picker.dart     | 137 +++
 .../flutter_colorpicker/colorpicker.dart      | 348 ++++++++
 .../ui/mobile/flutter_colorpicker/colors.dart | 174 ++++
 .../mobile/flutter_colorpicker/palette.dart   | 785 ++++++++++++++++++
 .../ui/mobile/flutter_colorpicker/utils.dart  | 220 +++++
 .../ui/mobile/goal_planner/goal_input.dart    | 156 ++++
 .../ui/mobile/goal_planner/goal_planner.dart  | 172 ++++
 .../ui/mobile/goal_planner/grade_display.dart |  30 +
 .../ui/mobile/goal_planner/route_option.dart  | 126 +++
 .../lib/ui/mobile/goal_planner/test.dart      | 209 +++++
 .../ui/mobile/grades/average_selector.dart    |  92 ++
 .../activation_view/activation_dashboard.dart | 182 ++++
 .../activation_view/activation_view.dart      |  67 ++
 .../lib/ui/mobile/premium/premium_inline.dart |  66 ++
 .../lib/ui/mobile/premium/upsell.dart         | 164 ++++
 .../lib/ui/mobile/settings/icon_pack.dart     |  34 +
 .../mobile/settings/modify_subject_names.dart | 383 +++++++++
 .../settings/modify_subject_names.i18n.dart   |  45 +
 .../lib/ui/mobile/settings/nickname.dart      |  93 +++
 .../lib/ui/mobile/settings/profile_pic.dart   | 208 +++++
 .../lib/ui/mobile/settings/theme.dart         | 657 +++++++++++++++
 .../lib/ui/mobile/settings/theme.i18n.dart    |  33 +
 .../lib/ui/mobile/timetable/fs_timetable.dart | 179 ++++
 .../mobile/timetable/fs_timetable_button.dart |  45 +
 filcnaplo_premium/pubspec.yaml                |  36 +
 39 files changed, 5892 insertions(+)
 create mode 100644 filcnaplo_premium/README.md
 create mode 100644 filcnaplo_premium/analysis_options.yaml
 create mode 100644 filcnaplo_premium/android/database/DBManager.java
 create mode 100644 filcnaplo_premium/android/database/SQLiteHelper.java
 create mode 100644 filcnaplo_premium/android/local.properties
 create mode 100644 filcnaplo_premium/android/utils/Utils.java
 create mode 100644 filcnaplo_premium/android/utils/Week.java
 create mode 100644 filcnaplo_premium/android/widget_timetable/WidgetTimetable.java
 create mode 100644 filcnaplo_premium/android/widget_timetable/WidgetTimetableDataProvider.java
 create mode 100644 filcnaplo_premium/android/widget_timetable/WidgetTimetableService.java
 create mode 100644 filcnaplo_premium/lib/api/auth.dart
 create mode 100644 filcnaplo_premium/lib/models/premium_result.dart
 create mode 100644 filcnaplo_premium/lib/models/premium_scopes.dart
 create mode 100644 filcnaplo_premium/lib/providers/premium_provider.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/block_picker.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/colorpicker.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/colors.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/palette.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/utils.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/goal_planner/goal_input.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/goal_planner/grade_display.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/goal_planner/route_option.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/goal_planner/test.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/grades/average_selector.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/premium/activation_view/activation_dashboard.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/premium/activation_view/activation_view.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/premium/premium_inline.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/premium/upsell.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/settings/icon_pack.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/settings/modify_subject_names.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/settings/modify_subject_names.i18n.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/settings/nickname.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/settings/profile_pic.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/settings/theme.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/settings/theme.i18n.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/timetable/fs_timetable.dart
 create mode 100644 filcnaplo_premium/lib/ui/mobile/timetable/fs_timetable_button.dart
 create mode 100644 filcnaplo_premium/pubspec.yaml

diff --git a/filcnaplo_premium/README.md b/filcnaplo_premium/README.md
new file mode 100644
index 0000000..979e950
--- /dev/null
+++ b/filcnaplo_premium/README.md
@@ -0,0 +1,3 @@
+# Premium ✨
+
+A collection of features only accessible for premium subscribers.
diff --git a/filcnaplo_premium/analysis_options.yaml b/filcnaplo_premium/analysis_options.yaml
new file mode 100644
index 0000000..fd16f92
--- /dev/null
+++ b/filcnaplo_premium/analysis_options.yaml
@@ -0,0 +1,28 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+  # The lint rules applied to this project can be customized in the
+  # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+  # included above or to enable additional rules. A list of all available lints
+  # and their documentation is published at
+  # https://dart-lang.github.io/linter/lints/index.html.
+  #
+  # Instead of disabling a lint rule for the entire project in the
+  # section below, it can also be suppressed for a single line of code
+  # or a specific dart file by using the `// ignore: name_of_lint` and
+  # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+  # producing the lint.
+  rules:
+    # avoid_print: false  # Uncomment to disable the `avoid_print` rule
+    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/filcnaplo_premium/android/database/DBManager.java b/filcnaplo_premium/android/database/DBManager.java
new file mode 100644
index 0000000..34f4bd2
--- /dev/null
+++ b/filcnaplo_premium/android/database/DBManager.java
@@ -0,0 +1,119 @@
+package hu.filc.naplo.database;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import java.sql.SQLException;
+
+import hu.filc.naplo.database.SQLiteHelper;
+
+public class DBManager {
+    private Context context;
+    private SQLiteDatabase database;
+    private SQLiteHelper dbHelper;
+
+    public DBManager(Context c) {
+        this.context = c;
+    }
+
+    public DBManager open() throws SQLException {
+        this.dbHelper = new SQLiteHelper(this.context);
+        this.database = this.dbHelper.getWritableDatabase();
+        return this;
+    }
+
+    public void close() {
+        this.dbHelper.close();
+    }
+
+    public Cursor fetchWidget(int wid) {
+        Cursor cursor = this.database.query(SQLiteHelper.TABLE_NAME_WIDGETS, new String[]{SQLiteHelper._ID, SQLiteHelper.DAY_SEL}, SQLiteHelper._ID + " = " + wid, null, null, null, null);
+        if (cursor != null) {
+            cursor.moveToFirst();
+        }
+        return cursor;
+    }
+
+    public Cursor fetchTimetable() {
+        Cursor cursor = this.database.query(SQLiteHelper.TABLE_NAME_USER_DATA, new String[]{SQLiteHelper.TIMETABLE}, null, null, null, null, null);
+        if (cursor != null) {
+            cursor.moveToFirst();
+        }
+        return cursor;
+    }
+
+    public Cursor fetchLastUser() {
+        Cursor cursor = this.database.query(SQLiteHelper.TABLE_NAME_SETTINGS, new String[]{SQLiteHelper.LAST_ACCOUNT_ID}, null, null, null, null, null);
+        if (cursor != null) {
+            cursor.moveToFirst();
+        }
+        return cursor;
+    }
+
+    public Cursor fetchTheme() {
+        Cursor cursor = this.database.query(SQLiteHelper.TABLE_NAME_SETTINGS, new String[]{SQLiteHelper.THEME, SQLiteHelper.ACCENT_COLOR}, null, null, null, null, null);
+        if (cursor != null) {
+            cursor.moveToFirst();
+        }
+        return cursor;
+    }
+
+    public Cursor fetchPremiumToken() {
+        Cursor cursor = this.database.query(SQLiteHelper.TABLE_NAME_SETTINGS, new String[]{SQLiteHelper.PREMIUM_TOKEN}, null, null, null, null, null);
+        if (cursor != null) {
+            cursor.moveToFirst();
+        }
+        return cursor;
+    }
+
+    public Cursor fetchPremiumScopes() {
+        Cursor cursor = this.database.query(SQLiteHelper.TABLE_NAME_SETTINGS, new String[]{SQLiteHelper.PREMIUM_SCOPES}, null, null, null, null, null);
+        if (cursor != null) {
+            cursor.moveToFirst();
+        }
+        return cursor;
+    }
+
+    public Cursor fetchLocale() {
+        Cursor cursor = this.database.query(SQLiteHelper.TABLE_NAME_SETTINGS, new String[]{SQLiteHelper.LOCALE}, null, null, null, null, null);
+        if (cursor != null) {
+            cursor.moveToFirst();
+        }
+        return cursor;
+    }
+
+    public void deleteWidget(int _id) {
+        this.database.delete(SQLiteHelper.TABLE_NAME_WIDGETS, "_id=" + _id, null);
+    }
+
+    /*public void changeSettings(int _id, Map<String, String> map) {
+        ContentValues con = new ContentValues();
+        for(Map.Entry<String, String> e: map.entrySet()){
+            con.put(e.getKey(), e.getValue());
+        }
+        this.database.update(SQLiteHelper.TABLE_NAME_WIDGETS, con, "_id = " + _id, null);
+    }
+    public void insertSettings(int _id, Map<String, String> map) {
+        ContentValues con = new ContentValues();
+        for(Map.Entry<String, String> e: map.entrySet()){
+            con.put(e.getKey(), e.getValue());
+            //Log.d("Settings added", e.getKey() + " - " + e.getValue());
+        }
+        this.database.insert(SQLiteHelper.TABLE_NAME_WIDGETS, null, con);
+    }*/
+
+    public void insertSelDay(int _id, int day_sel) {
+        ContentValues con = new ContentValues();
+        con.put(SQLiteHelper._ID, _id);
+        con.put(SQLiteHelper.DAY_SEL, day_sel);
+        this.database.insert(SQLiteHelper.TABLE_NAME_WIDGETS, null, con);
+    }
+
+    public int update(int _id, int day_sel) {
+        ContentValues con = new ContentValues();
+        con.put(SQLiteHelper.DAY_SEL, day_sel);
+        return this.database.update(SQLiteHelper.TABLE_NAME_WIDGETS, con, SQLiteHelper._ID + " = " + _id, null);
+    }
+}
\ No newline at end of file
diff --git a/filcnaplo_premium/android/database/SQLiteHelper.java b/filcnaplo_premium/android/database/SQLiteHelper.java
new file mode 100644
index 0000000..e3e7147
--- /dev/null
+++ b/filcnaplo_premium/android/database/SQLiteHelper.java
@@ -0,0 +1,36 @@
+package hu.filc.naplo.database;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+public class SQLiteHelper extends SQLiteOpenHelper {
+    private static final String CREATE_TABLE_WIDGET = " create table widgets ( _id INTEGER NOT NULL, day_sel INTEGER NOT NULL);";
+    private static final String DB_NAME = "app.db";
+    private static final int DB_VERSION = 1;
+    public static final String _ID = "_id";
+    public static final String DAY_SEL = "day_sel";
+    public static final String TIMETABLE = "timetable";
+    public static final String LAST_ACCOUNT_ID = "last_account_id";
+    public static final String THEME = "theme";
+    public static final String PREMIUM_TOKEN = "premium_token";
+    public static final String PREMIUM_SCOPES = "premium_scopes";
+    public static final String LOCALE = "language";
+    public static final String ACCENT_COLOR = "accent_color";
+    public static final String TABLE_NAME_WIDGETS = "widgets";
+    public static final String TABLE_NAME_USER_DATA = "user_data";
+    public static final String TABLE_NAME_SETTINGS = "settings";
+
+    public SQLiteHelper(Context context) {
+        super(context, DB_NAME, null, 7);
+    }
+
+    public void onCreate(SQLiteDatabase db) {
+        db.execSQL(CREATE_TABLE_WIDGET);
+    }
+
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        db.execSQL("DROP TABLE IF EXISTS widgets");
+        onCreate(db);
+    }
+}
\ No newline at end of file
diff --git a/filcnaplo_premium/android/local.properties b/filcnaplo_premium/android/local.properties
new file mode 100644
index 0000000..4c14d41
--- /dev/null
+++ b/filcnaplo_premium/android/local.properties
@@ -0,0 +1,2 @@
+sdk.dir=/Users/unknown/Library/Android/sdk
+flutter.sdk=/Users/unknown/flutter
\ No newline at end of file
diff --git a/filcnaplo_premium/android/utils/Utils.java b/filcnaplo_premium/android/utils/Utils.java
new file mode 100644
index 0000000..e8dbfc3
--- /dev/null
+++ b/filcnaplo_premium/android/utils/Utils.java
@@ -0,0 +1,36 @@
+package hu.filc.naplo.utils;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+
+import java.util.Calendar;
+import java.util.Date;
+
+public class Utils {
+    public static boolean hasNetwork(Context context) {
+        ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        NetworkInfo netInfo = cm.getActiveNetworkInfo();
+        if (netInfo != null && netInfo.isConnectedOrConnecting()) {
+            return true;
+        }
+        return false;
+    }
+
+    public static Date getWeekStartDate() {
+        Calendar calendar = Calendar.getInstance();
+        while (calendar.get(Calendar.DAY_OF_WEEK) != Calendar.MONDAY) {
+            calendar.add(Calendar.DATE, -1);
+        }
+        return calendar.getTime();
+    }
+
+    public static Date getWeekEndDate() {
+        Calendar calendar = Calendar.getInstance();
+        while (calendar.get(Calendar.DAY_OF_WEEK) != Calendar.MONDAY) {
+            calendar.add(Calendar.DATE, 1);
+        }
+        calendar.add(Calendar.DATE, -1);
+        return calendar.getTime();
+    }
+}
diff --git a/filcnaplo_premium/android/utils/Week.java b/filcnaplo_premium/android/utils/Week.java
new file mode 100644
index 0000000..076c3ba
--- /dev/null
+++ b/filcnaplo_premium/android/utils/Week.java
@@ -0,0 +1,65 @@
+package hu.filc.naplo.utils;
+
+import java.time.DayOfWeek;
+import java.time.Duration;
+import java.time.LocalDate;
+
+public class Week {
+    private final LocalDate start;
+    private final LocalDate end;
+
+    private Week(LocalDate start, LocalDate end) {
+        this.start = start;
+        this.end = end;
+    }
+
+    public static Week current() {
+        return fromDate(LocalDate.now());
+    }
+
+    public static Week fromId(int id) {
+        LocalDate _now = getYearStart().plusDays(id * 7L);
+        return new Week(_now.minusDays(_now.getDayOfWeek().getValue() - 1), _now.plusDays(7 - _now.getDayOfWeek().getValue()));
+    }
+
+    public static Week fromDate(LocalDate date) {
+
+        return new Week(date.minusDays(date.getDayOfWeek().getValue() - 1), date.plusDays(7 - date.getDayOfWeek().getValue()));
+    }
+
+    public Week next() {
+        return Week.fromDate(start.plusDays(8));
+    }
+
+    public int id() {
+        return (int) Math.ceil(Duration.between(getYearStart().atStartOfDay(), start.atStartOfDay()).toDays() / 7f);
+    }
+
+    private static LocalDate getYearStart() {
+        LocalDate now = LocalDate.now();
+        LocalDate start = getYearStart(now.getYear());
+        return start.isBefore(now) ? start : getYearStart(now.getYear() -1);
+    }
+
+    private static LocalDate getYearStart(int year) {
+        LocalDate time = LocalDate.of(year, 9, 1);
+        if (time.getDayOfWeek() == DayOfWeek.SATURDAY)
+            return time.plusDays(2);
+        else if (time.getDayOfWeek() == DayOfWeek.SUNDAY)
+            return time.plusDays(1);
+        return time;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Week week = (Week) o;
+        return this.id() == week.id();
+    }
+
+    @Override
+    public int hashCode() {
+        return id();
+    }
+}
\ No newline at end of file
diff --git a/filcnaplo_premium/android/widget_timetable/WidgetTimetable.java b/filcnaplo_premium/android/widget_timetable/WidgetTimetable.java
new file mode 100644
index 0000000..7bb33b4
--- /dev/null
+++ b/filcnaplo_premium/android/widget_timetable/WidgetTimetable.java
@@ -0,0 +1,397 @@
+package hu.filc.naplo.widget_timetable;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+import android.view.View;
+import android.widget.RemoteViews;
+import android.widget.Toast;
+
+import org.joda.time.DateTime;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.time.DayOfWeek;
+import java.time.format.TextStyle;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+
+import hu.filc.naplo.database.DBManager;
+import hu.filc.naplo.MainActivity;
+import hu.filc.naplo.R;
+
+import hu.filc.naplo.utils.Week;
+
+import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
+
+import es.antonborri.home_widget.HomeWidgetBackgroundIntent;
+import es.antonborri.home_widget.HomeWidgetLaunchIntent;
+import es.antonborri.home_widget.HomeWidgetProvider;
+
+public class WidgetTimetable extends HomeWidgetProvider {
+
+    private static final String ACTION_WIDGET_CLICK_NAV_LEFT = "list_widget.ACTION_WIDGET_CLICK_NAV_LEFT";
+    private static final String ACTION_WIDGET_CLICK_NAV_RIGHT = "list_widget.ACTION_WIDGET_CLICK_NAV_RIGHT";
+    private static final String ACTION_WIDGET_CLICK_NAV_TODAY = "list_widget.ACTION_WIDGET_CLICK_NAV_TODAY";
+    private static final String ACTION_WIDGET_CLICK_NAV_REFRESH = "list_widget.ACTION_WIDGET_CLICK_NAV_REFRESH";
+    private static final String ACTION_WIDGET_CLICK_BUY_PREMIUM = "list_widget.ACTION_WIDGET_CLICK_BUY_PREMIUM";
+
+    @Override
+    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, SharedPreferences widgetData) {
+        for (int i = 0; i < appWidgetIds.length; i++) {
+            RemoteViews views = generateView(context, appWidgetIds[i]);
+
+            if(premiumEnabled(context) && userLoggedIn(context)) {
+                int rday = selectDay(context, appWidgetIds[i], 0, true);
+                views.setTextViewText(R.id.nav_current, convertDayOfWeek(context, rday));
+            }
+
+            pushUpdate(context, views, appWidgetIds[i]);
+        }
+    }
+
+    public static void pushUpdate(Context context, RemoteViews remoteViews, int appWidgetSingleId) {
+        AppWidgetManager manager = AppWidgetManager.getInstance(context);
+
+        manager.updateAppWidget(appWidgetSingleId, remoteViews);
+        manager.notifyAppWidgetViewDataChanged(appWidgetSingleId, R.id.widget_list);
+    }
+
+    public static RemoteViews generateView(Context context, int appId) {
+        Intent serviceIntent = new Intent(context, WidgetTimetableService.class);
+        serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appId);
+        serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME)));
+
+        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_timetable);
+
+        views.setViewVisibility(R.id.need_premium, View.GONE);
+        views.setViewVisibility(R.id.need_login, View.GONE);
+        views.setViewVisibility(R.id.tt_grid_cont, View.GONE);
+
+        if(!userLoggedIn(context)) {
+            views.setViewVisibility(R.id.need_login, View.VISIBLE);
+            views.setOnClickPendingIntent(R.id.open_login, makePending(context, ACTION_WIDGET_CLICK_BUY_PREMIUM, appId));
+        } else if(premiumEnabled(context)) {
+            views.setViewVisibility(R.id.tt_grid_cont, View.VISIBLE);
+            views.setOnClickPendingIntent(R.id.nav_to_left, makePending(context, ACTION_WIDGET_CLICK_NAV_LEFT, appId));
+            views.setOnClickPendingIntent(R.id.nav_to_right, makePending(context, ACTION_WIDGET_CLICK_NAV_RIGHT, appId));
+            views.setOnClickPendingIntent(R.id.nav_current, makePending(context, ACTION_WIDGET_CLICK_NAV_TODAY, appId));
+            views.setOnClickPendingIntent(R.id.nav_refresh, makePending(context, ACTION_WIDGET_CLICK_NAV_REFRESH, appId));
+            views.setRemoteAdapter(R.id.widget_list, serviceIntent);
+            views.setEmptyView(R.id.widget_list, R.id.empty_view);
+        } else  {
+            views.setViewVisibility(R.id.need_premium, View.VISIBLE);
+            views.setOnClickPendingIntent(R.id.buy_premium, makePending(context, ACTION_WIDGET_CLICK_BUY_PREMIUM, appId));
+        }
+
+        return views;
+    }
+
+    static PendingIntent makePending(Context context, String action, int appWidgetId) {
+        Intent activebtnnext = new Intent(context, WidgetTimetable.class);
+        activebtnnext.setAction(action);
+        activebtnnext.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+        return PendingIntent.getBroadcast(context, appWidgetId, activebtnnext , PendingIntent.FLAG_IMMUTABLE);
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        super.onReceive(context, intent);
+
+        if(intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
+            int appId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
+            RemoteViews views = generateView(context, appId);
+
+            try {
+                if(premiumEnabled(context) && userLoggedIn(context)) {
+                    if (intent.getAction().equals(ACTION_WIDGET_CLICK_NAV_LEFT)) {
+                        int rday = selectDay(context, appId, -1, false);
+                        views.setTextViewText(R.id.nav_current, convertDayOfWeek(context, rday));
+
+                        pushUpdate(context, views, appId);
+                    } else if (intent.getAction().equals(ACTION_WIDGET_CLICK_NAV_RIGHT)) {
+                        int rday = selectDay(context, appId, 1, false);
+                        views.setTextViewText(R.id.nav_current, convertDayOfWeek(context, rday));
+
+                        pushUpdate(context, views, appId);
+                    } else if (intent.getAction().equals(ACTION_WIDGET_CLICK_NAV_TODAY)) {
+                        int rday = getToday(context);
+                        setSelectedDay(context, appId, rday);
+
+                        views.setTextViewText(R.id.nav_current, convertDayOfWeek(context, rday));
+
+                        pushUpdate(context, views, appId);
+                    } else if (intent.getAction().equals(ACTION_WIDGET_CLICK_NAV_REFRESH)) {
+                        PendingIntent pendingIntent = HomeWidgetLaunchIntent.INSTANCE.getActivity(context, MainActivity.class, Uri.parse("timetable://refresh"));
+                        pendingIntent.send();
+                    } else if (intent.getAction().equals("android.appwidget.action.APPWIDGET_DELETED")) {
+                        DBManager dbManager = new DBManager(context.getApplicationContext());
+
+                        try {
+                            dbManager.open();
+                            dbManager.deleteWidget(appId);
+                            dbManager.close();
+                        } catch (Exception e) {
+                            e.printStackTrace();
+                        }
+                    }
+                }
+
+                if(intent.getAction().equals(ACTION_WIDGET_CLICK_BUY_PREMIUM)) {
+                    PendingIntent pendingIntent = HomeWidgetLaunchIntent.INSTANCE.getActivity(context, MainActivity.class, Uri.parse("settings://premium"));
+                    pendingIntent.send();
+                }
+            }
+            catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    public static String convertDayOfWeek(Context context, int rday) {
+
+        /*if(rday == -1) return DayOfWeek.of(1).getDisplayName(TextStyle.FULL, new Locale("hu", "HU"));
+
+        String dayOfWeek = DayOfWeek.of(rday + 1).getDisplayName(TextStyle.FULL, new Locale("hu", "HU"));*/
+
+        String dayOfWeek = "Unknown";
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            Locale loc = getLocale(context);
+
+            if (rday == -1)
+                return DayOfWeek.of(1).getDisplayName(TextStyle.FULL, loc);
+
+            dayOfWeek = DayOfWeek.of(rday + 1).getDisplayName(TextStyle.FULL, loc);
+        }
+
+        return dayOfWeek.substring(0, 1).toUpperCase() + dayOfWeek.substring(1).toLowerCase();
+    }
+
+    public static void setSelectedDay(Context context, int wid, int day) {
+        DBManager dbManager = new DBManager(context.getApplicationContext());
+
+        try {
+            dbManager.open();
+            dbManager.update(wid, day);
+            dbManager.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public static int getToday(Context context) {
+        int rday = new DateTime().getDayOfWeek() - 1;
+        List<JSONArray> s = genJsonDays(context);
+
+        try {
+            if(checkIsAfter(s, rday)) rday += 1;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return retDay(rday, s.size());
+    }
+
+    public static int selectDay(Context context, int wid, int add, Boolean afterSubjects) {
+        DBManager dbManager = new DBManager(context.getApplicationContext());
+
+        try {
+            dbManager.open();
+            Cursor cursor = dbManager.fetchWidget(wid);
+
+            List<JSONArray> s = genJsonDays(context);
+            int retday = new DateTime().getDayOfWeek() - 1;
+
+            if(cursor.getCount() != 0) retday = retDay(cursor.getInt(1) + add, s.size());
+
+            if(afterSubjects) if(checkIsAfter(s, retday)) retday += 1;
+            retday = retDay(retday, s.size());
+
+            if(cursor.getCount() == 0) dbManager.insertSelDay(wid, retday);
+            else dbManager.update(wid, retday);
+
+            dbManager.close();
+
+            return retday;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return 0;
+    }
+
+    public static Boolean checkIsAfter(List<JSONArray> s, int retday) throws Exception {
+        retday = retDay(retday, s.size());
+
+        String vegIdopont = s.get(retday).getJSONObject(s.get(retday).length() - 1).getString("VegIdopont");
+
+        return new DateTime().isAfter(new DateTime(vegIdopont));
+    }
+
+    public static int retDay(int retday, int size) {
+        if (retday < 0) retday = size - 1;
+        else if (retday > size - 1) retday = 0;
+
+        return retday;
+    }
+
+    public static List<JSONArray> genJsonDays(Context context) {
+        List<JSONArray> gen_days = new ArrayList<>();
+
+        DBManager dbManager = new DBManager(context.getApplicationContext());
+        try {
+            dbManager.open();
+            Cursor ct = dbManager.fetchTimetable();
+            dbManager.close();
+
+            if(ct.getCount() == 0) {
+                return gen_days;
+            }
+
+            JSONObject fecthtt = new JSONObject(ct.getString(0));
+
+            JSONArray dayArray = new JSONArray();
+            String currday = "";
+
+            String currweek = String.valueOf(Week.current().id());
+
+            JSONArray week = fecthtt.getJSONArray(currweek);
+
+            for (int i=0; i < week.length(); i++)
+            {
+                try {
+                    if(i == 0) currday = week.getJSONObject(0).getString("Datum");
+                    JSONObject oraObj = week.getJSONObject(i);
+
+                    if(!currday.equals(oraObj.getString("Datum"))) {
+                        gen_days.add(dayArray);
+                        currday = week.getJSONObject(i).getString("Datum");
+                        dayArray = new JSONArray();
+                    }
+
+                    dayArray.put(oraObj);
+                    if(i == week.length() - 1) {
+                        gen_days.add(dayArray);
+                    }
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        Collections.sort(gen_days, new Comparator<JSONArray>() {
+
+            public int compare(JSONArray a, JSONArray b) {
+                long valA = 0;
+                long valB = 0;
+
+                try {
+                    valA = (long) new DateTime( a.getJSONObject(0).getString("Datum")).getMillis();
+                    valB = (long) new DateTime( b.getJSONObject(0).getString("Datum")).getMillis();
+                }
+                catch (JSONException ignored) {
+                }
+
+                return (int) (valA - valB);
+            }
+        });
+
+        return gen_days;
+    }
+
+    public static String zeroPad(int value, int padding){
+        StringBuilder b = new StringBuilder();
+        b.append(value);
+        while(b.length() < padding){
+            b.insert(0,"0");
+        }
+        return b.toString();
+    }
+
+    public static Locale getLocale(Context context) {
+        DBManager dbManager = new DBManager(context.getApplicationContext());
+
+        try {
+            dbManager.open();
+            String loc = dbManager.fetchLocale().getString(0);
+            dbManager.close();
+
+            if(loc.equals("hu") || loc.equals("de")) {
+                return new Locale(loc, loc.toUpperCase());
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return new Locale("en", "GB");
+    }
+
+    public static boolean premiumEnabled(Context context) {
+        DBManager dbManager = new DBManager(context.getApplicationContext());
+
+        try {
+            dbManager.open();
+            String premium_token = dbManager.fetchPremiumToken().getString(0);
+            String premium_scopes_raw = dbManager.fetchPremiumScopes().getString(0);
+            dbManager.close();
+
+            JSONArray arr = new JSONArray(premium_scopes_raw);
+            List<String> premium_scopes = new ArrayList<>();
+            for(int i = 0; i < arr.length(); i++){
+                String scope = arr.getString(i);
+                premium_scopes.add(scope.substring(scope.lastIndexOf('.')  + 1));
+            }
+
+            if(!premium_token.equals("") && (premium_scopes.contains("*") || premium_scopes.contains("TIMETALBE_WIDGET"))) {
+                return true;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return false;
+    }
+
+    public static boolean userLoggedIn(Context context) {
+        return !lastUserId(context).equals("");
+    }
+
+    public static String lastUserId(Context context) {
+        DBManager dbManager = new DBManager(context.getApplicationContext());
+        try {
+            dbManager.open();
+            Cursor cursor = dbManager.fetchLastUser();
+            dbManager.close();
+
+            if(cursor != null && !cursor.getString(0).equals("")) {
+                String last_user = cursor.getString(0);
+                return last_user;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return "";
+    }
+
+    @Override
+    public void onEnabled(Context context) {
+    }
+
+    @Override
+    public void onDisabled(Context context) {
+    }
+}
\ No newline at end of file
diff --git a/filcnaplo_premium/android/widget_timetable/WidgetTimetableDataProvider.java b/filcnaplo_premium/android/widget_timetable/WidgetTimetableDataProvider.java
new file mode 100644
index 0000000..451ceab
--- /dev/null
+++ b/filcnaplo_premium/android/widget_timetable/WidgetTimetableDataProvider.java
@@ -0,0 +1,354 @@
+package hu.filc.naplo.widget_timetable;
+
+import android.appwidget.AppWidgetManager;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Build;
+import android.util.Log;
+import android.view.View;
+import android.widget.RemoteViews;
+import android.widget.RemoteViewsService;
+
+import org.joda.time.DateTime;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import hu.filc.naplo.database.DBManager;
+import hu.filc.naplo.R;
+
+public class WidgetTimetableDataProvider implements RemoteViewsService.RemoteViewsFactory {
+
+    private Context context;
+    private int appWidgetId;
+
+    private int rday = 0;
+
+    private int theme;
+
+    private Integer[] colorValues;
+
+    List<Lesson> day_subjects = new ArrayList<>();
+    List<Integer> lessonIndexes = new ArrayList<>();
+
+    Item witem;
+
+    /* Default values */
+
+    static class Item {
+        int Layout;
+
+        int NumVisibility;
+        int NameVisibility;
+        int NameNodescVisibility;
+        int DescVisibility;
+        int RoomVisibility;
+        int TimeVisibility;
+
+        int NumColor;
+        int NameColor;
+        int NameNodescColor;
+        int DescColor;
+
+        Integer[] NameNodescPadding = {0, 0, 0, 0};
+
+        public Item(int Layout, int NumVisibility,int NameVisibility,int NameNodescVisibility,int DescVisibility,int RoomVisibility,int TimeVisibility,int NumColor,int NameColor,int NameNodescColor,int DescColor) {
+            this.Layout = Layout;
+            this.NumVisibility = NumVisibility;
+            this.NameVisibility = NameVisibility;
+            this.NameNodescVisibility = NameNodescVisibility;
+            this.DescVisibility = DescVisibility;
+            this.RoomVisibility = RoomVisibility;
+            this.TimeVisibility = TimeVisibility;
+
+            this.NumColor = NumColor;
+            this.NameColor = NameColor;
+            this.NameNodescColor = NameNodescColor;
+            this.DescColor = DescColor;
+        }
+    }
+
+    static class Lesson {
+        String status;
+        String lessonIndex;
+        String lessonName;
+        String lessonTopic;
+        String lessonRoom;
+        long lessonStart;
+        long lessonEnd;
+        String substituteTeacher;
+
+        public Lesson(String status, String lessonIndex,String lessonName,String lessonTopic, String lessonRoom,long lessonStart,long lessonEnd,String substituteTeacher) {
+            this.status = status;
+            this.lessonIndex = lessonIndex;
+            this.lessonName = lessonName;
+            this.lessonTopic = lessonTopic;
+            this.lessonRoom = lessonRoom;
+            this.lessonStart = lessonStart;
+            this.lessonEnd = lessonEnd;
+            this.substituteTeacher = substituteTeacher;
+        }
+    }
+
+    Integer[] itemNameNodescPadding = {0, 0, 0, 0};
+
+    public WidgetTimetableDataProvider(Context context, Intent intent) {
+        this.context = context;
+        this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
+
+        this.theme = getThemeAccent(context);
+
+        this.colorValues = new Integer[]{R.color.filc,
+                R.color.blue_shade300,
+                R.color.green_shade300,
+                R.color.lime_shade300,
+                R.color.yellow_shade300,
+                R.color.orange_shade300,
+                R.color.red_shade300,
+                R.color.pink_shade300,
+                R.color.purple_shade300};
+
+    }
+
+    @Override
+    public void onCreate() {
+        initData();
+    }
+
+    @Override
+    public void onDataSetChanged() {
+        initData();
+    }
+
+    @Override
+    public void onDestroy() {
+
+    }
+
+    @Override
+    public int getCount() {
+
+        return day_subjects.size();
+    }
+
+    public void setLayout(final RemoteViews view) {
+        /* Visibilities */
+        view.setViewVisibility(R.id.tt_item_num, witem.NumVisibility);
+        view.setViewVisibility(R.id.tt_item_name, witem.NameVisibility);
+        view.setViewVisibility(R.id.tt_item_name_nodesc, witem.NameNodescVisibility);
+        view.setViewVisibility(R.id.tt_item_desc, witem.DescVisibility);
+        view.setViewVisibility(R.id.tt_item_room, witem.RoomVisibility);
+        view.setViewVisibility(R.id.tt_item_time, witem.TimeVisibility);
+
+        /* backgroundResources */
+        view.setInt(R.id.main_lay, "setBackgroundResource", witem.Layout);
+
+        /* Paddings */
+        view.setViewPadding(R.id.tt_item_name_nodesc, witem.NameNodescPadding[0], witem.NameNodescPadding[1], witem.NameNodescPadding[2], witem.NameNodescPadding[3]);
+
+        /* Text Colors */
+        view.setInt(R.id.tt_item_num, "setTextColor", getColor(context, witem.NumColor));
+        view.setInt(R.id.tt_item_name, "setTextColor",  getColor(context, witem.NameColor));
+        view.setInt(R.id.tt_item_name_nodesc, "setTextColor",  getColor(context, witem.NameNodescColor));
+        view.setInt(R.id.tt_item_desc, "setTextColor",  getColor(context, witem.DescColor));
+    }
+
+    public int getColor(Context context, int color) {
+        return context.getResources().getColor(color);
+    }
+
+    @Override
+    public RemoteViews getViewAt(int position) {
+        RemoteViews view = new RemoteViews(context.getPackageName(), R.layout.timetable_item);
+
+        witem = defaultItem(theme);
+
+        Lesson curr_subject = day_subjects.get(position);
+
+        if (curr_subject.status.equals("empty")) {
+            witem.NumColor = R.color.text_miss_num;
+
+            witem.TimeVisibility = View.GONE;
+            witem.RoomVisibility = View.GONE;
+
+            witem.NameNodescColor = R.color.text_miss;
+        }
+
+        if (!curr_subject.substituteTeacher.equals("null")) {
+            witem.NumColor = R.color.yellow;
+            witem.Layout = R.drawable.card_layout_tile_helyetesitett;
+        }
+
+        if (curr_subject.status.equals("Elmaradt")) {
+            witem.NumColor = R.color.red;
+            witem.Layout = R.drawable.card_layout_tile_elmarad;
+        } else if (curr_subject.status.equals("TanevRendjeEsemeny")) {
+            witem.NumVisibility = View.GONE;
+            witem.TimeVisibility = View.GONE;
+            witem.RoomVisibility = View.GONE;
+
+            witem.NameNodescPadding[0] = 50;
+            witem.NameNodescPadding[2] = 50;
+
+            witem.NameNodescColor = R.color.text_miss;
+        }
+
+        if (curr_subject.lessonTopic.equals("null")) {
+            witem.DescVisibility = View.GONE;
+            witem.NameVisibility = View.GONE;
+
+            witem.NameNodescVisibility = View.VISIBLE;
+        }
+
+        setLayout(view);
+
+        String lessonIndexTrailing = curr_subject.lessonIndex.equals("+") ? "" : ".";
+
+        view.setTextViewText(R.id.tt_item_num, curr_subject.lessonIndex + lessonIndexTrailing);
+        view.setTextViewText(R.id.tt_item_name, curr_subject.lessonName);
+        view.setTextViewText(R.id.tt_item_name_nodesc, curr_subject.lessonName);
+        view.setTextViewText(R.id.tt_item_desc, curr_subject.lessonTopic);
+        view.setTextViewText(R.id.tt_item_room, curr_subject.lessonRoom);
+        if(curr_subject.lessonStart != 0 && curr_subject.lessonEnd != 0)
+            view.setTextViewText(R.id.tt_item_time, WidgetTimetable.zeroPad(new DateTime(curr_subject.lessonStart).getHourOfDay(), 2) + ":" + WidgetTimetable.zeroPad(new DateTime(curr_subject.lessonStart).getMinuteOfHour(), 2) +
+                    "\n" + WidgetTimetable.zeroPad(new DateTime(curr_subject.lessonEnd).getHourOfDay(), 2) + ":" + WidgetTimetable.zeroPad(new DateTime(curr_subject.lessonEnd).getMinuteOfHour(),2));
+
+        return view;
+    }
+
+    @Override
+    public RemoteViews getLoadingView() {
+        return null;
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        return 1;
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public boolean hasStableIds() {
+        return true;
+    }
+
+    private void initData() {
+
+        theme = getThemeAccent(context);
+
+        rday = WidgetTimetable.selectDay(context, appWidgetId, 0, false);
+
+        day_subjects.clear();
+        lessonIndexes.clear();
+
+        try {
+            List<JSONArray> arr = WidgetTimetable.genJsonDays(context);
+
+            if(arr.isEmpty()) {
+                return;
+            }
+            JSONArray arr_lessons = WidgetTimetable.genJsonDays(context).get(rday);
+
+            for (int i = 0; i < arr_lessons.length(); i++) {
+                JSONObject obj_lessons = arr_lessons.getJSONObject(i);
+
+                day_subjects.add(jsonToLesson(obj_lessons));
+            }
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+
+        if(day_subjects.size() > 0) {
+            Collections.sort(day_subjects, new Comparator<Lesson>() {
+                public int compare(Lesson o1, Lesson o2) {
+                    return new DateTime(o1.lessonStart).compareTo(new DateTime(o2.lessonStart));
+                }
+            });
+
+            for (int i = 0; i < day_subjects.size(); i++) {
+                if(!day_subjects.get(i).lessonIndex.equals("+")) {
+                    lessonIndexes.add(Integer.valueOf(day_subjects.get(i).lessonIndex));
+                }
+            }
+
+            if(lessonIndexes.size() > 0) {
+
+                int lessonsChecked = Collections.min(lessonIndexes);
+                int i = 0;
+
+                while(lessonsChecked < Collections.max(lessonIndexes)) {
+                    if(!lessonIndexes.contains(lessonsChecked)) {
+                        day_subjects.add(i, emptyLesson(lessonsChecked));
+                    }
+                    lessonsChecked++;
+                    i++;
+                }
+            }
+        }
+    }
+
+    public static Integer getThemeAccent(Context context) {
+        DBManager dbManager = new DBManager(context.getApplicationContext());
+
+        try {
+            dbManager.open();
+            Cursor cursor = dbManager.fetchTheme();
+            dbManager.close();
+
+            return cursor.getInt(1);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return 0;
+    }
+
+    public Item defaultItem(int theme) {
+        return new Item(
+                R.drawable.card_layout_tile,
+                View.VISIBLE,
+                View.VISIBLE,
+                View.INVISIBLE,
+                View.VISIBLE,
+                View.VISIBLE,
+                View.VISIBLE,
+                colorValues[theme >= colorValues.length ? 0 : theme],
+                R.color.text,
+                R.color.text,
+                R.color.text_desc
+        );
+    }
+
+    public Lesson emptyLesson(int lessonIndex) {
+        return new Lesson("empty", String.valueOf(lessonIndex), "Lyukasóra", "null", "null", 0, 0, "null");
+    }
+
+    public Lesson jsonToLesson(JSONObject json) {
+        try {
+            return new Lesson(
+                    json.getJSONObject("Allapot").getString("Nev"),
+                    !json.getString("Oraszam").equals("null") ? json.getString("Oraszam") : "+",
+                    json.getString("Nev"),
+                    json.getString("Tema"),
+                    json.getString("TeremNeve"),
+                    new DateTime(json.getString("KezdetIdopont")).getMillis(),
+                    new DateTime(json.getString("VegIdopont")).getMillis(),
+                    json.getString("HelyettesTanarNeve")
+            );
+        }catch (Exception e) {
+            Log.d("Filc", "exception: " + e);
+        };
+
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/filcnaplo_premium/android/widget_timetable/WidgetTimetableService.java b/filcnaplo_premium/android/widget_timetable/WidgetTimetableService.java
new file mode 100644
index 0000000..40adb41
--- /dev/null
+++ b/filcnaplo_premium/android/widget_timetable/WidgetTimetableService.java
@@ -0,0 +1,12 @@
+package hu.filc.naplo.widget_timetable;
+
+import android.content.Intent;
+import android.os.Build;
+import android.widget.RemoteViewsService;
+
+public class WidgetTimetableService extends RemoteViewsService {
+    @Override
+    public RemoteViewsFactory onGetViewFactory(Intent intent) {
+        return new WidgetTimetableDataProvider(getApplicationContext(), intent);
+    }
+}
diff --git a/filcnaplo_premium/lib/api/auth.dart b/filcnaplo_premium/lib/api/auth.dart
new file mode 100644
index 0000000..e3d5c5f
--- /dev/null
+++ b/filcnaplo_premium/lib/api/auth.dart
@@ -0,0 +1,120 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:developer';
+import 'dart:io';
+
+import 'package:filcnaplo/api/client.dart';
+import 'package:filcnaplo/models/settings.dart';
+import 'package:filcnaplo_premium/models/premium_result.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:url_launcher/url_launcher.dart';
+import 'package:uni_links/uni_links.dart';
+import 'package:http/http.dart' as http;
+import 'package:home_widget/home_widget.dart';
+
+class PremiumAuth {
+  final SettingsProvider _settings;
+  StreamSubscription? _sub;
+
+  PremiumAuth({required SettingsProvider settings}) : _settings = settings;
+
+  initAuth() {
+    try {
+      _sub ??= uriLinkStream.listen(
+        (Uri? uri) {
+          if (uri != null) {
+            final accessToken = uri.queryParameters['access_token'];
+            if (accessToken != null) {
+              finishAuth(accessToken);
+            }
+          }
+        },
+        onError: (err) {
+          log("ERROR: initAuth: $err");
+        },
+      );
+
+      launchUrl(
+        Uri.parse("https://api.filcnaplo.hu/oauth"),
+        mode: LaunchMode.externalApplication,
+      );
+    } catch (err, sta) {
+      log("ERROR: initAuth: $err\n$sta");
+    }
+  }
+
+  Future<bool> finishAuth(String accessToken) async {
+    try {
+      // final res = await http.get(Uri.parse("${FilcAPI.premiumScopesApi}?access_token=${Uri.encodeComponent(accessToken)}"));
+      // final scopes = ((jsonDecode(res.body) as Map)["scopes"] as List).cast<String>();
+      // log("[INFO] Premium auth finish: ${scopes.join(',')}");
+      await _settings.update(premiumAccessToken: accessToken);
+      final result = await refreshAuth();
+      if (Platform.isAndroid) updateWidget();
+      return result;
+    } catch (err, sta) {
+      log("[ERROR] Premium auth failed: $err\n$sta");
+    }
+
+    await _settings.update(premiumAccessToken: "", premiumScopes: []);
+    if (Platform.isAndroid) updateWidget();
+    return false;
+  }
+
+  Future<bool?> updateWidget() async {
+    try {
+      return HomeWidget.updateWidget(name: 'widget_timetable.WidgetTimetable');
+    } on PlatformException catch (exception) {
+      if (kDebugMode) {
+        print('Error Updating Widget After Auth. $exception');
+      }
+    }
+    return false;
+  }
+
+  Future<bool> refreshAuth({bool removePremium = false}) async {
+    if (!removePremium) {
+      if (_settings.premiumAccessToken == "") {
+        await _settings.update(premiumScopes: [], premiumLogin: "");
+        return false;
+      }
+
+      // Skip premium check when disconnected
+      try {
+        final status = await InternetAddress.lookup('github.com');
+        if (status.isEmpty) return false;
+      } on SocketException catch (_) {
+        return false;
+      }
+
+      for (int tries = 0; tries < 3; tries++) {
+        try {
+          final res = await http.post(Uri.parse(FilcAPI.premiumApi), body: {
+            "access_token": _settings.premiumAccessToken,
+          });
+
+          if (res.body == "") throw "empty body";
+
+          final premium = PremiumResult.fromJson(jsonDecode(res.body) as Map);
+          // Activation succeeded
+          log("[INFO] Premium activated: ${premium.scopes.join(',')}");
+          await _settings.update(
+            premiumAccessToken: premium.accessToken,
+            premiumScopes: premium.scopes,
+            premiumLogin: premium.login,
+          );
+          return true;
+        } catch (err, sta) {
+          log("[ERROR] Premium activation failed: $err\n$sta");
+        }
+
+        await Future.delayed(const Duration(seconds: 1));
+      }
+    }
+
+    // Activation failed
+    await _settings.update(premiumAccessToken: "", premiumScopes: [], premiumLogin: "");
+    return false;
+  }
+}
diff --git a/filcnaplo_premium/lib/models/premium_result.dart b/filcnaplo_premium/lib/models/premium_result.dart
new file mode 100644
index 0000000..d86aa16
--- /dev/null
+++ b/filcnaplo_premium/lib/models/premium_result.dart
@@ -0,0 +1,19 @@
+class PremiumResult {
+  final String accessToken;
+  final List<String> scopes;
+  final String login;
+
+  PremiumResult({
+    required this.accessToken,
+    required this.scopes,
+    required this.login,
+  });
+
+  factory PremiumResult.fromJson(Map json) {
+    return PremiumResult(
+      accessToken: json["access_token"] ?? "",
+      scopes: (json["scopes"] ?? []).cast<String>(),
+      login: json["login"],
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/models/premium_scopes.dart b/filcnaplo_premium/lib/models/premium_scopes.dart
new file mode 100644
index 0000000..2642a9d
--- /dev/null
+++ b/filcnaplo_premium/lib/models/premium_scopes.dart
@@ -0,0 +1,32 @@
+class PremiumScopes {
+  /// VIP
+  static const all = "filc.premium.*";
+
+  /// Kupak
+
+  /// Custom nickname
+  static const nickname = "filc.premium.NICKNAME";
+
+  /// Advanced grade statistics
+  static const gradeStats = "filc.premium.GRADE_STATS";
+
+  /// Advanced theme customization
+  static const customColors = "filc.premium.CUSTOM_COLORS";
+
+  /// Icon pack customization for subjects
+  static const customIcons = "filc.premium.CUSTOM_ICONS";
+
+  /// Modify subject names
+  static const renameSubjects = "filc.premium.RENAME_SUBJECTS";
+
+  /// Tinta
+
+  /// Timetable homescreen widget
+  static const timetableWidget = "filc.premium.TIMETALBE_WIDGET";
+
+  /// Goal Planner
+  static const goalPlanner = "filc.premium.GOAL_PLANNER";
+
+  /// Fullscreen weekly timetable view
+  static const fsTimetable = "filc.premium.FS_TIMETABLE";
+}
diff --git a/filcnaplo_premium/lib/providers/premium_provider.dart b/filcnaplo_premium/lib/providers/premium_provider.dart
new file mode 100644
index 0000000..980afd4
--- /dev/null
+++ b/filcnaplo_premium/lib/providers/premium_provider.dart
@@ -0,0 +1,28 @@
+import 'package:filcnaplo/models/settings.dart';
+import 'package:filcnaplo_premium/api/auth.dart';
+import 'package:filcnaplo_premium/models/premium_scopes.dart';
+import 'package:flutter/widgets.dart';
+
+class PremiumProvider extends ChangeNotifier {
+  final SettingsProvider _settings;
+  List<String> get scopes => _settings.premiumScopes;
+  bool hasScope(String scope) => scopes.contains(scope) || scopes.contains(PremiumScopes.all);
+  String get accessToken => _settings.premiumAccessToken;
+  String get login => _settings.premiumLogin;
+  bool get hasPremium => _settings.premiumAccessToken != "" && _settings.premiumScopes.isNotEmpty;
+
+  late final PremiumAuth _auth;
+  PremiumAuth get auth => _auth;
+
+  PremiumProvider({required SettingsProvider settings}) : _settings = settings {
+    _auth = PremiumAuth(settings: _settings);
+    _settings.addListener(() {
+      notifyListeners();
+    });
+  }
+
+  Future<void> activate({bool removePremium = false}) async {
+    await _auth.refreshAuth(removePremium: removePremium);
+    notifyListeners();
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/block_picker.dart b/filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/block_picker.dart
new file mode 100644
index 0000000..35cfc9a
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/block_picker.dart
@@ -0,0 +1,137 @@
+// FROM: https://pub.dev/packages/flutter_colorpicker
+// FROM: https://pub.dev/packages/flutter_colorpicker
+// FROM: https://pub.dev/packages/flutter_colorpicker
+// FROM: https://pub.dev/packages/flutter_colorpicker
+
+/// Blocky Color Picker
+
+library block_colorpicker;
+
+import 'package:flutter/material.dart';
+import 'package:filcnaplo/theme/colors/accent.dart';
+import 'utils.dart';
+
+/// Child widget for layout builder.
+typedef PickerItem = Widget Function(Color color);
+
+/// Customize the layout.
+typedef PickerLayoutBuilder = Widget Function(BuildContext context, List<Color> colors, PickerItem child);
+
+/// Customize the item shape.
+typedef PickerItemBuilder = Widget Function(Color color, bool isCurrentColor, void Function() changeColor);
+
+// Provide a list of colors for block color picker.
+// const List<Color> _defaultColors = [
+//   Colors.red,
+//   Colors.pink,
+//   Colors.purple,
+//   Colors.deepPurple,
+//   Colors.indigo,
+//   Colors.blue,
+//   Colors.lightBlue,
+//   Colors.cyan,
+//   Colors.teal,
+//   Colors.green,
+//   Colors.lightGreen,
+//   Colors.lime,
+//   Colors.yellow,
+//   Colors.amber,
+//   Colors.orange,
+//   Colors.deepOrange,
+//   Colors.brown,
+//   Colors.grey,
+//   Colors.blueGrey,
+//   Colors.black,
+// ];
+
+// Provide a layout for [BlockPicker].
+Widget _defaultLayoutBuilder(BuildContext context, List<Color> colors, PickerItem child) {
+  Orientation orientation = MediaQuery.of(context).orientation;
+
+  return SizedBox(
+    width: 300,
+    height: orientation == Orientation.portrait ? 360 : 200,
+    child: GridView.count(
+      crossAxisCount: orientation == Orientation.portrait ? 4 : 6,
+      crossAxisSpacing: 5,
+      mainAxisSpacing: 5,
+      children: [for (Color color in colors) child(color)],
+    ),
+  );
+}
+
+// Provide a shape for [BlockPicker].
+Widget _defaultItemBuilder(Color color, bool isCurrentColor, void Function() changeColor) {
+  return Container(
+    margin: const EdgeInsets.all(7),
+    decoration: BoxDecoration(
+      shape: BoxShape.circle,
+      color: color,
+      boxShadow: [BoxShadow(color: color.withOpacity(0.8), offset: const Offset(1, 2), blurRadius: 5)],
+    ),
+    child: Material(
+      color: Colors.transparent,
+      child: InkWell(
+        onTap: changeColor,
+        borderRadius: BorderRadius.circular(50),
+        child: AnimatedOpacity(
+          duration: const Duration(milliseconds: 210),
+          opacity: isCurrentColor ? 1 : 0,
+          child: Icon(Icons.done, color: useWhiteForeground(color) ? Colors.white : Colors.black),
+        ),
+      ),
+    ),
+  );
+}
+
+// The blocky color picker you can alter the layout and shape.
+class BlockPicker extends StatefulWidget {
+  BlockPicker({
+    Key? key,
+    required this.pickerColor,
+    required this.onColorChanged,
+    this.useInShowDialog = true,
+    this.layoutBuilder = _defaultLayoutBuilder,
+    this.itemBuilder = _defaultItemBuilder,
+  }) : super(key: key);
+
+  final Color? pickerColor;
+  final ValueChanged<Color> onColorChanged;
+  final List<Color> availableColors = accentColorMap.values.toList();
+  final bool useInShowDialog;
+  final PickerLayoutBuilder layoutBuilder;
+  final PickerItemBuilder itemBuilder;
+
+  @override
+  State<StatefulWidget> createState() => _BlockPickerState();
+}
+
+class _BlockPickerState extends State<BlockPicker> {
+  Color? _currentColor;
+
+  @override
+  void initState() {
+    _currentColor = widget.pickerColor;
+    super.initState();
+  }
+
+  void changeColor(Color color) {
+    setState(() => _currentColor = color);
+    widget.onColorChanged(color);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return widget.layoutBuilder(
+      context,
+      widget.availableColors,
+      (Color color) => widget.itemBuilder(
+        color,
+        (_currentColor != null && (widget.useInShowDialog ? true : widget.pickerColor != null))
+            ? (_currentColor?.value == color.value) && (widget.useInShowDialog ? true : widget.pickerColor?.value == color.value)
+            : false,
+        () => changeColor(color),
+      ),
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/colorpicker.dart b/filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/colorpicker.dart
new file mode 100644
index 0000000..e17fbf5
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/colorpicker.dart
@@ -0,0 +1,348 @@
+// FROM: https://pub.dev/packages/flutter_colorpicker
+// FROM: https://pub.dev/packages/flutter_colorpicker
+// FROM: https://pub.dev/packages/flutter_colorpicker
+// FROM: https://pub.dev/packages/flutter_colorpicker
+
+/// HSV(HSB)/HSL Color Picker example
+///
+/// You can create your own layout by importing `picker.dart`.
+
+library hsv_picker;
+
+import 'package:filcnaplo_premium/ui/mobile/flutter_colorpicker/block_picker.dart';
+import 'package:filcnaplo_premium/ui/mobile/flutter_colorpicker/palette.dart';
+import 'package:filcnaplo_premium/ui/mobile/flutter_colorpicker/utils.dart';
+import 'package:filcnaplo_premium/ui/mobile/settings/theme.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:filcnaplo/theme/colors/colors.dart';
+import 'package:filcnaplo_mobile_ui/common/widgets/custom_switch.dart';
+
+class FilcColorPicker extends StatefulWidget {
+  const FilcColorPicker({
+    Key? key,
+    required this.colorMode,
+    required this.pickerColor,
+    required this.onColorChanged,
+    required this.onColorChangeEnd,
+    this.pickerHsvColor,
+    this.onHsvColorChanged,
+    this.paletteType = PaletteType.hsvWithHue,
+    this.enableAlpha = true,
+    @Deprecated('Use empty list in [labelTypes] to disable label.') this.showLabel = true,
+    this.labelTypes = const [ColorLabelType.rgb, ColorLabelType.hsv, ColorLabelType.hsl],
+    @Deprecated('Use Theme.of(context).textTheme.bodyText1 & 2 to alter text style.') this.labelTextStyle,
+    this.displayThumbColor = false,
+    this.portraitOnly = false,
+    this.colorPickerWidth = 300.0,
+    this.pickerAreaHeightPercent = 1.0,
+    this.pickerAreaBorderRadius = const BorderRadius.all(Radius.zero),
+    this.hexInputBar = false,
+    this.hexInputController,
+    this.colorHistory,
+    this.onHistoryChanged,
+  }) : super(key: key);
+
+  final CustomColorMode colorMode;
+  final Color pickerColor;
+  final ValueChanged<Color> onColorChanged;
+  final void Function(Color color, {bool? adaptive}) onColorChangeEnd;
+  final HSVColor? pickerHsvColor;
+  final ValueChanged<HSVColor>? onHsvColorChanged;
+  final PaletteType paletteType;
+  final bool enableAlpha;
+  final bool showLabel;
+  final List<ColorLabelType> labelTypes;
+  final TextStyle? labelTextStyle;
+  final bool displayThumbColor;
+  final bool portraitOnly;
+  final double colorPickerWidth;
+  final double pickerAreaHeightPercent;
+  final BorderRadius pickerAreaBorderRadius;
+  final bool hexInputBar;
+  final TextEditingController? hexInputController;
+  final List<Color>? colorHistory;
+  final ValueChanged<List<Color>>? onHistoryChanged;
+
+  @override
+  _FilcColorPickerState createState() => _FilcColorPickerState();
+}
+
+class _FilcColorPickerState extends State<FilcColorPicker> {
+  HSVColor currentHsvColor = const HSVColor.fromAHSV(0.0, 0.0, 0.0, 0.0);
+  List<Color> colorHistory = [];
+  bool isAdvancedView = false;
+
+  @override
+  void initState() {
+    currentHsvColor = (widget.pickerHsvColor != null) ? widget.pickerHsvColor as HSVColor : HSVColor.fromColor(widget.pickerColor);
+    // If there's no initial text in `hexInputController`,
+    if (widget.hexInputController?.text.isEmpty == true) {
+      // set it to the current's color HEX value.
+      widget.hexInputController?.text = colorToHex(
+        currentHsvColor.toColor(),
+        enableAlpha: widget.enableAlpha,
+      );
+    }
+    // Listen to the text input, If there is an `hexInputController` provided.
+    widget.hexInputController?.addListener(colorPickerTextInputListener);
+    if (widget.colorHistory != null && widget.onHistoryChanged != null) {
+      colorHistory = widget.colorHistory ?? [];
+    }
+    super.initState();
+  }
+
+  @override
+  void didUpdateWidget(FilcColorPicker oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    currentHsvColor = (widget.pickerHsvColor != null) ? widget.pickerHsvColor as HSVColor : HSVColor.fromColor(widget.pickerColor);
+  }
+
+  void colorPickerTextInputListener() {
+    // It can't be null really, since it's only listening if the controller
+    // is provided, but it may help to calm the Dart analyzer in the future.
+    if (widget.hexInputController == null) return;
+    // If a user is inserting/typing any text — try to get the color value from it,
+    // and interpret its transparency, dependent on the widget's settings.
+    final Color? color = colorFromHex(widget.hexInputController!.text, enableAlpha: widget.enableAlpha);
+    // If it's the valid color:
+    if (color != null) {
+      // set it as the current color and
+      setState(() => currentHsvColor = HSVColor.fromColor(color));
+      // notify with a callback.
+      widget.onColorChanged(color);
+      if (widget.onHsvColorChanged != null) widget.onHsvColorChanged!(currentHsvColor);
+    }
+  }
+
+  @override
+  void dispose() {
+    widget.hexInputController?.removeListener(colorPickerTextInputListener);
+    super.dispose();
+  }
+
+  Widget colorPickerSlider(TrackType trackType) {
+    return ColorPickerSlider(
+      trackType,
+      currentHsvColor,
+      (HSVColor color) {
+        // Update text in `hexInputController` if provided.
+        widget.hexInputController?.text = colorToHex(color.toColor(), enableAlpha: widget.enableAlpha);
+        setState(() => currentHsvColor = color);
+        widget.onColorChanged(currentHsvColor.toColor());
+        if (widget.onHsvColorChanged != null) widget.onHsvColorChanged!(currentHsvColor);
+      },
+      () => widget.onColorChangeEnd(currentHsvColor.toColor()),
+      (p) {
+        ScaffoldMessenger.of(context).clearSnackBars();
+        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+            content: Text("Move the ${p == 0 ? 'Saturation (second)' : 'Value (third)'} slider first.",
+                textAlign: TextAlign.center, style: TextStyle(color: AppColors.of(context).text, fontWeight: FontWeight.w600)),
+            backgroundColor: AppColors.of(context).background));
+      },
+      displayThumbColor: widget.displayThumbColor,
+    );
+  }
+
+  void onColorChanging(HSVColor color) {
+    // Update text in `hexInputController` if provided.
+    widget.hexInputController?.text = colorToHex(color.toColor(), enableAlpha: widget.enableAlpha);
+    setState(() => currentHsvColor = color);
+    widget.onColorChanged(currentHsvColor.toColor());
+    if (widget.onHsvColorChanged != null) widget.onHsvColorChanged!(currentHsvColor);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (MediaQuery.of(context).orientation == Orientation.portrait || widget.portraitOnly) {
+      return Column(
+        children: [
+          if (widget.colorMode != CustomColorMode.theme)
+            Padding(
+              padding: const EdgeInsets.only(top: 8.0),
+              child: Column(
+                children: [
+                  Padding(
+                    padding: const EdgeInsets.only(left: 12.0, right: 12.0),
+                    child: SizedBox(
+                      height: 45.0,
+                      width: double.infinity,
+                      child: colorPickerSlider(TrackType.hue),
+                    ),
+                  ),
+                  Padding(
+                    padding: const EdgeInsets.only(left: 12.0, right: 12.0),
+                    child: SizedBox(
+                      height: 45.0,
+                      width: double.infinity,
+                      child: colorPickerSlider(TrackType.saturation),
+                    ),
+                  ),
+                  if (isAdvancedView)
+                    Padding(
+                      padding: const EdgeInsets.only(left: 12.0, right: 12.0),
+                      child: SizedBox(
+                        height: 45.0,
+                        width: double.infinity,
+                        child: colorPickerSlider(TrackType.value),
+                      ),
+                    ),
+                ],
+              ),
+            ),
+          if (isAdvancedView && widget.colorMode != CustomColorMode.theme)
+            Padding(
+              padding: const EdgeInsets.only(bottom: 6.0),
+              child: ColorPickerInput(
+                currentHsvColor.toColor(),
+                (Color color) {
+                  setState(() => currentHsvColor = HSVColor.fromColor(color));
+                  widget.onColorChanged(currentHsvColor.toColor());
+                  if (widget.onHsvColorChanged != null) widget.onHsvColorChanged!(currentHsvColor);
+                },
+                enableAlpha: false,
+                embeddedText: false,
+              ),
+            ),
+          SizedBox(
+            height: 70 * (widget.colorMode == CustomColorMode.theme ? 2 : 1),
+            child: BlockPicker(
+              pickerColor: Colors.red,
+              layoutBuilder: (context, colors, child) {
+                return GridView.count(
+                  shrinkWrap: true,
+                  crossAxisCount: widget.colorMode == CustomColorMode.theme ? 2 : 1,
+                  scrollDirection: Axis.horizontal,
+                  crossAxisSpacing: 15,
+                  physics: const BouncingScrollPhysics(),
+                  mainAxisSpacing: 15,
+                  padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
+                  children: List.generate(colors.toSet().length + (widget.colorMode == CustomColorMode.theme ? 1 : 0), (index) {
+                    if (widget.colorMode == CustomColorMode.theme) {
+                      if (index == 0) {
+                        return GestureDetector(
+                          onTap: () => widget.onColorChangeEnd(Colors.transparent, adaptive: true),
+                          child: ColorIndicator(HSVColor.fromColor(const Color.fromARGB(255, 255, 238, 177)),
+                              icon: CupertinoIcons.wand_stars, currentHsvColor: currentHsvColor, width: 30, height: 30, adaptive: true),
+                        );
+                      }
+                      index--;
+                    }
+                    return GestureDetector(
+                      onTap: () => widget.onColorChangeEnd(colors[index]),
+                      child: ColorIndicator(HSVColor.fromColor(colors[index]), currentHsvColor: currentHsvColor, width: 30, height: 30),
+                    );
+                  }),
+                );
+              },
+              onColorChanged: (c) => {},
+            ),
+          ),
+          if (widget.colorMode != CustomColorMode.theme)
+            Material(
+              color: Colors.transparent,
+              child: InkWell(
+                customBorder: RoundedRectangleBorder(
+                  borderRadius: BorderRadius.circular(20),
+                ),
+                onTap: () => setState(() {
+                  isAdvancedView = !isAdvancedView;
+                }),
+                child: Padding(
+                  padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0),
+                  child: Row(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    mainAxisSize: MainAxisSize.min,
+                    children: [
+                      CustomSwitch(
+                        onChanged: (v) => setState(() => isAdvancedView = v),
+                        value: isAdvancedView,
+                      ),
+                      const SizedBox(width: 12.0),
+                      Text(
+                        "Advanced",
+                        style: TextStyle(
+                          fontWeight: FontWeight.w600,
+                          fontSize: 16.0,
+                          color: AppColors.of(context).text.withOpacity(isAdvancedView ? 1.0 : .5),
+                        ),
+                      ),
+                    ],
+                  ),
+                ),
+              ),
+            ),
+        ],
+      );
+    } else {
+      return Row(
+        children: [
+          //SizedBox(width: widget.colorPickerWidth, height: widget.colorPickerWidth * widget.pickerAreaHeightPercent, child: colorPicker()),
+          Column(
+            children: [
+              Row(
+                children: <Widget>[
+                  const SizedBox(width: 20.0),
+                  GestureDetector(
+                    onTap: () => setState(() {
+                      if (widget.onHistoryChanged != null && !colorHistory.contains(currentHsvColor.toColor())) {
+                        colorHistory.add(currentHsvColor.toColor());
+                        widget.onHistoryChanged!(colorHistory);
+                      }
+                    }),
+                    child: ColorIndicator(currentHsvColor),
+                  ),
+                  Column(
+                    children: <Widget>[
+                      //SizedBox(height: 40.0, width: 260.0, child: sliderByPaletteType()),
+                      if (widget.enableAlpha) SizedBox(height: 40.0, width: 260.0, child: colorPickerSlider(TrackType.alpha)),
+                    ],
+                  ),
+                  const SizedBox(width: 10.0),
+                ],
+              ),
+              if (colorHistory.isNotEmpty)
+                SizedBox(
+                  width: widget.colorPickerWidth,
+                  height: 50,
+                  child: ListView(scrollDirection: Axis.horizontal, children: <Widget>[
+                    for (Color color in colorHistory)
+                      Padding(
+                        key: Key(color.hashCode.toString()),
+                        padding: const EdgeInsets.fromLTRB(15, 18, 0, 0),
+                        child: Center(
+                          child: GestureDetector(
+                            onTap: () => onColorChanging(HSVColor.fromColor(color)),
+                            onLongPress: () {
+                              if (colorHistory.remove(color)) {
+                                widget.onHistoryChanged!(colorHistory);
+                                setState(() {});
+                              }
+                            },
+                            child: ColorIndicator(HSVColor.fromColor(color), width: 30, height: 30),
+                          ),
+                        ),
+                      ),
+                    const SizedBox(width: 15),
+                  ]),
+                ),
+              const SizedBox(height: 20.0),
+              if (widget.hexInputBar)
+                ColorPickerInput(
+                  currentHsvColor.toColor(),
+                  (Color color) {
+                    setState(() => currentHsvColor = HSVColor.fromColor(color));
+                    widget.onColorChanged(currentHsvColor.toColor());
+                    if (widget.onHsvColorChanged != null) widget.onHsvColorChanged!(currentHsvColor);
+                  },
+                  enableAlpha: widget.enableAlpha,
+                  embeddedText: false,
+                ),
+              const SizedBox(height: 5),
+            ],
+          ),
+        ],
+      );
+    }
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/colors.dart b/filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/colors.dart
new file mode 100644
index 0000000..289cf20
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/colors.dart
@@ -0,0 +1,174 @@
+// FROM: https://pub.dev/packages/flutter_colorpicker
+// FROM: https://pub.dev/packages/flutter_colorpicker
+// FROM: https://pub.dev/packages/flutter_colorpicker
+// FROM: https://pub.dev/packages/flutter_colorpicker
+
+import 'dart:ui';
+
+/// X11 Colors
+///
+/// https://en.wikipedia.org/wiki/X11_color_names
+
+const Map<String, Color> x11Colors = {
+  'aliceblue': Color(0xfff0f8ff),
+  'antiquewhite': Color(0xfffaebd7),
+  'aqua': Color(0xff00ffff),
+  'aquamarine': Color(0xff7fffd4),
+  'azure': Color(0xfff0ffff),
+  'beige': Color(0xfff5f5dc),
+  'bisque': Color(0xffffe4c4),
+  'black': Color(0xff000000),
+  'blanchedalmond': Color(0xffffebcd),
+  'blue': Color(0xff0000ff),
+  'blueviolet': Color(0xff8a2be2),
+  'brown': Color(0xffa52a2a),
+  'burlywood': Color(0xffdeb887),
+  'cadetblue': Color(0xff5f9ea0),
+  'chartreuse': Color(0xff7fff00),
+  'chocolate': Color(0xffd2691e),
+  'coral': Color(0xffff7f50),
+  'cornflower': Color(0xff6495ed),
+  'cornflowerblue': Color(0xff6495ed),
+  'cornsilk': Color(0xfffff8dc),
+  'crimson': Color(0xffdc143c),
+  'cyan': Color(0xff00ffff),
+  'darkblue': Color(0xff00008b),
+  'darkcyan': Color(0xff008b8b),
+  'darkgoldenrod': Color(0xffb8860b),
+  'darkgray': Color(0xffa9a9a9),
+  'darkgreen': Color(0xff006400),
+  'darkgrey': Color(0xffa9a9a9),
+  'darkkhaki': Color(0xffbdb76b),
+  'darkmagenta': Color(0xff8b008b),
+  'darkolivegreen': Color(0xff556b2f),
+  'darkorange': Color(0xffff8c00),
+  'darkorchid': Color(0xff9932cc),
+  'darkred': Color(0xff8b0000),
+  'darksalmon': Color(0xffe9967a),
+  'darkseagreen': Color(0xff8fbc8f),
+  'darkslateblue': Color(0xff483d8b),
+  'darkslategray': Color(0xff2f4f4f),
+  'darkslategrey': Color(0xff2f4f4f),
+  'darkturquoise': Color(0xff00ced1),
+  'darkviolet': Color(0xff9400d3),
+  'deeppink': Color(0xffff1493),
+  'deepskyblue': Color(0xff00bfff),
+  'dimgray': Color(0xff696969),
+  'dimgrey': Color(0xff696969),
+  'dodgerblue': Color(0xff1e90ff),
+  'firebrick': Color(0xffb22222),
+  'floralwhite': Color(0xfffffaf0),
+  'forestgreen': Color(0xff228b22),
+  'fuchsia': Color(0xffff00ff),
+  'gainsboro': Color(0xffdcdcdc),
+  'ghostwhite': Color(0xfff8f8ff),
+  'gold': Color(0xffffd700),
+  'goldenrod': Color(0xffdaa520),
+  'gray': Color(0xff808080),
+  'green': Color(0xff008000),
+  'greenyellow': Color(0xffadff2f),
+  'grey': Color(0xff808080),
+  'honeydew': Color(0xfff0fff0),
+  'hotpink': Color(0xffff69b4),
+  'indianred': Color(0xffcd5c5c),
+  'indigo': Color(0xff4b0082),
+  'ivory': Color(0xfffffff0),
+  'khaki': Color(0xfff0e68c),
+  'laserlemon': Color(0xffffff54),
+  'lavender': Color(0xffe6e6fa),
+  'lavenderblush': Color(0xfffff0f5),
+  'lawngreen': Color(0xff7cfc00),
+  'lemonchiffon': Color(0xfffffacd),
+  'lightblue': Color(0xffadd8e6),
+  'lightcoral': Color(0xfff08080),
+  'lightcyan': Color(0xffe0ffff),
+  'lightgoldenrod': Color(0xfffafad2),
+  'lightgoldenrodyellow': Color(0xfffafad2),
+  'lightgray': Color(0xffd3d3d3),
+  'lightgreen': Color(0xff90ee90),
+  'lightgrey': Color(0xffd3d3d3),
+  'lightpink': Color(0xffffb6c1),
+  'lightsalmon': Color(0xffffa07a),
+  'lightseagreen': Color(0xff20b2aa),
+  'lightskyblue': Color(0xff87cefa),
+  'lightslategray': Color(0xff778899),
+  'lightslategrey': Color(0xff778899),
+  'lightsteelblue': Color(0xffb0c4de),
+  'lightyellow': Color(0xffffffe0),
+  'lime': Color(0xff00ff00),
+  'limegreen': Color(0xff32cd32),
+  'linen': Color(0xfffaf0e6),
+  'magenta': Color(0xffff00ff),
+  'maroon': Color(0xff800000),
+  'maroon2': Color(0xff7f0000),
+  'maroon3': Color(0xffb03060),
+  'mediumaquamarine': Color(0xff66cdaa),
+  'mediumblue': Color(0xff0000cd),
+  'mediumorchid': Color(0xffba55d3),
+  'mediumpurple': Color(0xff9370db),
+  'mediumseagreen': Color(0xff3cb371),
+  'mediumslateblue': Color(0xff7b68ee),
+  'mediumspringgreen': Color(0xff00fa9a),
+  'mediumturquoise': Color(0xff48d1cc),
+  'mediumvioletred': Color(0xffc71585),
+  'midnightblue': Color(0xff191970),
+  'mintcream': Color(0xfff5fffa),
+  'mistyrose': Color(0xffffe4e1),
+  'moccasin': Color(0xffffe4b5),
+  'navajowhite': Color(0xffffdead),
+  'navy': Color(0xff000080),
+  'oldlace': Color(0xfffdf5e6),
+  'olive': Color(0xff808000),
+  'olivedrab': Color(0xff6b8e23),
+  'orange': Color(0xffffa500),
+  'orangered': Color(0xffff4500),
+  'orchid': Color(0xffda70d6),
+  'palegoldenrod': Color(0xffeee8aa),
+  'palegreen': Color(0xff98fb98),
+  'paleturquoise': Color(0xffafeeee),
+  'palevioletred': Color(0xffdb7093),
+  'papayawhip': Color(0xffffefd5),
+  'peachpuff': Color(0xffffdab9),
+  'peru': Color(0xffcd853f),
+  'pink': Color(0xffffc0cb),
+  'plum': Color(0xffdda0dd),
+  'powderblue': Color(0xffb0e0e6),
+  'purple': Color(0xff800080),
+  'purple2': Color(0xff7f007f),
+  'purple3': Color(0xffa020f0),
+  'rebeccapurple': Color(0xff663399),
+  'red': Color(0xffff0000),
+  'rosybrown': Color(0xffbc8f8f),
+  'royalblue': Color(0xff4169e1),
+  'saddlebrown': Color(0xff8b4513),
+  'salmon': Color(0xfffa8072),
+  'sandybrown': Color(0xfff4a460),
+  'seagreen': Color(0xff2e8b57),
+  'seashell': Color(0xfffff5ee),
+  'sienna': Color(0xffa0522d),
+  'silver': Color(0xffc0c0c0),
+  'skyblue': Color(0xff87ceeb),
+  'slateblue': Color(0xff6a5acd),
+  'slategray': Color(0xff708090),
+  'slategrey': Color(0xff708090),
+  'snow': Color(0xfffffafa),
+  'springgreen': Color(0xff00ff7f),
+  'steelblue': Color(0xff4682b4),
+  'tan': Color(0xffd2b48c),
+  'teal': Color(0xff008080),
+  'thistle': Color(0xffd8bfd8),
+  'tomato': Color(0xffff6347),
+  'turquoise': Color(0xff40e0d0),
+  'violet': Color(0xffee82ee),
+  'wheat': Color(0xfff5deb3),
+  'white': Color(0xffffffff),
+  'whitesmoke': Color(0xfff5f5f5),
+  'yellow': Color(0xffffff00),
+  'yellowgreen': Color(0xff9acd32),
+};
+
+Color? colorFromName(String val) => x11Colors[val.trim().replaceAll(' ', '').toLowerCase()];
+
+extension ColorExtension on String {
+  Color? toColor() => colorFromName(this);
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/palette.dart b/filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/palette.dart
new file mode 100644
index 0000000..fe4b066
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/palette.dart
@@ -0,0 +1,785 @@
+// FROM: https://pub.dev/packages/flutter_colorpicker
+// FROM: https://pub.dev/packages/flutter_colorpicker
+// FROM: https://pub.dev/packages/flutter_colorpicker
+// FROM: https://pub.dev/packages/flutter_colorpicker
+
+/// The components of HSV Color Picker
+///
+/// Try to create a Color Picker with other layout on your own :)
+
+import 'package:filcnaplo/models/settings.dart';
+import 'package:filcnaplo/theme/colors/accent.dart';
+import 'package:filcnaplo/theme/colors/colors.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:provider/provider.dart';
+import 'utils.dart';
+
+/// Palette types for color picker area widget.
+enum PaletteType {
+  hsv,
+  hsvWithHue,
+  hsvWithValue,
+  hsvWithSaturation,
+  hsl,
+  hslWithHue,
+  hslWithLightness,
+  hslWithSaturation,
+  rgbWithBlue,
+  rgbWithGreen,
+  rgbWithRed,
+  hueWheel,
+}
+
+/// Track types for slider picker.
+enum TrackType {
+  hue,
+  saturation,
+  saturationForHSL,
+  value,
+  lightness,
+  red,
+  green,
+  blue,
+  alpha,
+}
+
+enum FilcTrackType {
+  hue,
+  saturation,
+  value,
+}
+
+/// Color information label type.
+enum ColorLabelType { hex, rgb, hsv, hsl }
+
+/// Types for slider picker widget.
+enum ColorModel { rgb, hsv, hsl }
+// enum ColorSpace { rgb, hsv, hsl, hsp, okhsv, okhsl, xyz, yuv, lab, lch, cmyk }
+
+/// Painter for SV mixture.
+class HSVWithHueColorPainter extends CustomPainter {
+  const HSVWithHueColorPainter(this.hsvColor, {this.pointerColor});
+
+  final HSVColor hsvColor;
+  final Color? pointerColor;
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final Rect rect = Offset.zero & size;
+    const Gradient gradientV = LinearGradient(
+      begin: Alignment.topCenter,
+      end: Alignment.bottomCenter,
+      colors: [Colors.white, Colors.black],
+    );
+    final Gradient gradientH = LinearGradient(
+      colors: [
+        Colors.white,
+        HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, 1.0).toColor(),
+      ],
+    );
+    canvas.drawRect(rect, Paint()..shader = gradientV.createShader(rect));
+    canvas.drawRect(
+      rect,
+      Paint()
+        ..blendMode = BlendMode.multiply
+        ..shader = gradientH.createShader(rect),
+    );
+
+    canvas.drawCircle(
+      Offset(size.width * hsvColor.saturation, size.height * (1 - hsvColor.value)),
+      size.height * 0.04,
+      Paint()
+        ..color = pointerColor ?? (useWhiteForeground(hsvColor.toColor()) ? Colors.white : Colors.black)
+        ..strokeWidth = 1.5
+        ..style = PaintingStyle.stroke,
+    );
+  }
+
+  @override
+  bool shouldRepaint(CustomPainter oldDelegate) => false;
+}
+
+class _SliderLayout extends MultiChildLayoutDelegate {
+  static const String track = 'track';
+  static const String thumb = 'thumb';
+  static const String gestureContainer = 'gesturecontainer';
+
+  @override
+  void performLayout(Size size) {
+    layoutChild(
+      track,
+      BoxConstraints.tightFor(
+        width: size.width + 3,
+        height: size.height / 1.5,
+      ),
+    );
+    positionChild(track, const Offset(-2.0, 0));
+    layoutChild(
+      thumb,
+      const BoxConstraints.tightFor(width: 5.5, height: 10.5),
+    );
+    positionChild(thumb, Offset(0.0, (size.height / 1.5) / 2 - 4.5));
+    layoutChild(
+      gestureContainer,
+      BoxConstraints.tightFor(width: size.width, height: size.height),
+    );
+    positionChild(gestureContainer, Offset.zero);
+  }
+
+  @override
+  bool shouldRelayout(_SliderLayout oldDelegate) => false;
+}
+
+/// Painter for all kinds of track types.
+class TrackPainter extends CustomPainter {
+  const TrackPainter(this.trackType, this.hsvColor);
+
+  final TrackType trackType;
+  final HSVColor hsvColor;
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final Rect rect = Offset.zero & size;
+    if (trackType == TrackType.alpha) {
+      final Size chessSize = Size(size.height / 2, size.height / 2);
+      Paint chessPaintB = Paint()..color = const Color(0xffcccccc);
+      Paint chessPaintW = Paint()..color = Colors.white;
+      List.generate((size.height / chessSize.height).round(), (int y) {
+        List.generate((size.width / chessSize.width).round(), (int x) {
+          canvas.drawRect(
+            Offset(chessSize.width * x, chessSize.width * y) & chessSize,
+            (x + y) % 2 != 0 ? chessPaintW : chessPaintB,
+          );
+        });
+      });
+    }
+
+    switch (trackType) {
+      case TrackType.hue:
+        final List<Color> colors = [
+          const HSVColor.fromAHSV(1.0, 0.0, 1.0, 1.0).toColor(),
+          const HSVColor.fromAHSV(1.0, 60.0, 1.0, 1.0).toColor(),
+          const HSVColor.fromAHSV(1.0, 120.0, 1.0, 1.0).toColor(),
+          const HSVColor.fromAHSV(1.0, 180.0, 1.0, 1.0).toColor(),
+          const HSVColor.fromAHSV(1.0, 240.0, 1.0, 1.0).toColor(),
+          const HSVColor.fromAHSV(1.0, 300.0, 1.0, 1.0).toColor(),
+          const HSVColor.fromAHSV(1.0, 360.0, 1.0, 1.0).toColor(),
+        ];
+        Gradient gradient = LinearGradient(colors: colors);
+        canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
+        break;
+      case TrackType.saturation:
+        final List<Color> colors = [
+          HSVColor.fromAHSV(1.0, hsvColor.hue, 0.0, 1.0).toColor(),
+          HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, 1.0).toColor(),
+        ];
+        Gradient gradient = LinearGradient(colors: colors);
+        canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
+        break;
+      case TrackType.saturationForHSL:
+        final List<Color> colors = [
+          HSLColor.fromAHSL(1.0, hsvColor.hue, 0.0, 0.5).toColor(),
+          HSLColor.fromAHSL(1.0, hsvColor.hue, 1.0, 0.5).toColor(),
+        ];
+        Gradient gradient = LinearGradient(colors: colors);
+        canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
+        break;
+      case TrackType.value:
+        final List<Color> colors = [
+          HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, 0.0).toColor(),
+          HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, 1.0).toColor(),
+        ];
+        Gradient gradient = LinearGradient(colors: colors);
+        canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
+        break;
+      case TrackType.lightness:
+        final List<Color> colors = [
+          HSLColor.fromAHSL(1.0, hsvColor.hue, 1.0, 0.0).toColor(),
+          HSLColor.fromAHSL(1.0, hsvColor.hue, 1.0, 0.5).toColor(),
+          HSLColor.fromAHSL(1.0, hsvColor.hue, 1.0, 1.0).toColor(),
+        ];
+        Gradient gradient = LinearGradient(colors: colors);
+        canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
+        break;
+      case TrackType.red:
+        final List<Color> colors = [
+          hsvColor.toColor().withRed(0).withOpacity(1.0),
+          hsvColor.toColor().withRed(255).withOpacity(1.0),
+        ];
+        Gradient gradient = LinearGradient(colors: colors);
+        canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
+        break;
+      case TrackType.green:
+        final List<Color> colors = [
+          hsvColor.toColor().withGreen(0).withOpacity(1.0),
+          hsvColor.toColor().withGreen(255).withOpacity(1.0),
+        ];
+        Gradient gradient = LinearGradient(colors: colors);
+        canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
+        break;
+      case TrackType.blue:
+        final List<Color> colors = [
+          hsvColor.toColor().withBlue(0).withOpacity(1.0),
+          hsvColor.toColor().withBlue(255).withOpacity(1.0),
+        ];
+        Gradient gradient = LinearGradient(colors: colors);
+        canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
+        break;
+      case TrackType.alpha:
+        final List<Color> colors = [
+          hsvColor.toColor().withOpacity(0.0),
+          hsvColor.toColor().withOpacity(1.0),
+        ];
+        Gradient gradient = LinearGradient(colors: colors);
+        canvas.drawRect(rect, Paint()..shader = gradient.createShader(rect));
+        break;
+    }
+  }
+
+  @override
+  bool shouldRepaint(CustomPainter oldDelegate) => false;
+}
+
+/// Painter for thumb of slider.
+class ThumbPainter extends CustomPainter {
+  const ThumbPainter({this.thumbColor, this.fullThumbColor = false});
+
+  final Color? thumbColor;
+  final bool fullThumbColor;
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    canvas.drawShadow(
+      Path()
+        ..addOval(
+          Rect.fromCircle(center: const Offset(0.5, 2.0), radius: size.width * 1.8),
+        ),
+      Colors.black,
+      3.0,
+      true,
+    );
+    canvas.drawCircle(
+        Offset(0.0, size.height * 0.4),
+        size.height,
+        Paint()
+          ..color = Colors.white
+          ..style = PaintingStyle.fill);
+    if (thumbColor != null) {
+      canvas.drawCircle(
+          Offset(0.0, size.height * 0.4),
+          size.height * (fullThumbColor ? 1.0 : 0.65),
+          Paint()
+            ..color = thumbColor!
+            ..style = PaintingStyle.fill);
+    }
+  }
+
+  @override
+  bool shouldRepaint(CustomPainter oldDelegate) => false;
+}
+
+/// Painter for chess type alpha background in color indicator widget.
+class IndicatorPainter extends CustomPainter {
+  const IndicatorPainter(this.color);
+
+  final Color color;
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final Size chessSize = Size(size.width / 10, size.height / 10);
+    final Paint chessPaintB = Paint()..color = const Color(0xFFCCCCCC);
+    final Paint chessPaintW = Paint()..color = Colors.white;
+    List.generate((size.height / chessSize.height).round(), (int y) {
+      List.generate((size.width / chessSize.width).round(), (int x) {
+        canvas.drawRect(
+          Offset(chessSize.width * x, chessSize.height * y) & chessSize,
+          (x + y) % 2 != 0 ? chessPaintW : chessPaintB,
+        );
+      });
+    });
+
+    canvas.drawCircle(
+        Offset(size.width / 2, size.height / 2),
+        size.height / 2,
+        Paint()
+          ..color = color
+          ..style = PaintingStyle.fill);
+  }
+
+  @override
+  bool shouldRepaint(CustomPainter oldDelegate) => false;
+}
+
+/// Provide hex input wiget for 3/6/8 digits.
+class ColorPickerInput extends StatefulWidget {
+  const ColorPickerInput(
+    this.color,
+    this.onColorChanged, {
+    Key? key,
+    this.enableAlpha = true,
+    this.embeddedText = false,
+    this.disable = false,
+  }) : super(key: key);
+
+  final Color color;
+  final ValueChanged<Color> onColorChanged;
+  final bool enableAlpha;
+  final bool embeddedText;
+  final bool disable;
+
+  @override
+  _ColorPickerInputState createState() => _ColorPickerInputState();
+}
+
+class _ColorPickerInputState extends State<ColorPickerInput> {
+  TextEditingController textEditingController = TextEditingController();
+  int inputColor = 0;
+
+  @override
+  void dispose() {
+    textEditingController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (inputColor != widget.color.value) {
+      textEditingController.text = '#' +
+          widget.color.red.toRadixString(16).toUpperCase().padLeft(2, '0') +
+          widget.color.green.toRadixString(16).toUpperCase().padLeft(2, '0') +
+          widget.color.blue.toRadixString(16).toUpperCase().padLeft(2, '0') +
+          (widget.enableAlpha ? widget.color.alpha.toRadixString(16).toUpperCase().padLeft(2, '0') : '');
+    }
+    return Padding(
+      padding: const EdgeInsets.only(top: 6.0, left: 12.0, right: 12.0),
+      child: SizedBox(
+        width: double.infinity,
+        child: TextField(
+          enabled: !widget.disable,
+          controller: textEditingController,
+          style: TextStyle(
+            fontSize: 18,
+            color: Theme.of(context).colorScheme.onBackground,
+          ),
+          inputFormatters: [
+            UpperCaseTextFormatter(),
+            FilteringTextInputFormatter.allow(RegExp(kValidHexPattern)),
+          ],
+          decoration: InputDecoration(
+            isDense: true,
+            filled: true,
+            border: OutlineInputBorder(
+              borderRadius: BorderRadius.circular(12.0),
+              borderSide: const BorderSide(color: Colors.transparent, width: 0.0),
+            ),
+            enabledBorder: OutlineInputBorder(
+              borderRadius: BorderRadius.circular(12.0),
+              borderSide: const BorderSide(color: Colors.transparent, width: 0.0),
+            ),
+            focusedBorder: OutlineInputBorder(
+              borderRadius: BorderRadius.circular(12.0),
+              borderSide: const BorderSide(color: Colors.transparent, width: 0.0),
+            ),
+            contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0),
+            fillColor: AppColors.of(context).text.withOpacity(.1),
+          ),
+          onChanged: (String value) {
+            String input = value;
+            if (value.length == 9) {
+              input = value.split('').getRange(7, 9).join() + value.split('').getRange(1, 7).join();
+            }
+            final Color? color = colorFromHex(input);
+            if (color != null) {
+              widget.onColorChanged(color);
+              inputColor = color.value;
+            }
+          },
+        ),
+      ),
+    );
+  }
+}
+
+/*class ValueColorPickerSlider extends StatefulWidget {
+  ValueColorPickerSlider(this.trackType, this.initialHsvColor, this.onProgressChanged, this.onColorChangeEnd, {Key? key}) : super(key: key);
+
+  final TrackType trackType;
+  final HSVColor initialHsvColor;
+  final void Function(double progress) onProgressChanged;
+  final void Function() onColorChangeEnd;
+
+  @override
+  State<ValueColorPickerSlider> createState() => _ValueColorPickerSliderState();
+}
+
+class _ValueColorPickerSliderState extends State<ValueColorPickerSlider> {
+  HSVColor hsvColor = HSVColor.fromColor(Colors.red);
+
+  @override
+  void initState() {
+    super.initState();
+    hsvColor = widget.initialHsvColor;
+  }
+
+  void slideEvent(RenderBox getBox, BoxConstraints box, Offset globalPosition) {
+    double localDx = getBox.globalToLocal(globalPosition).dx - 15.0;
+    double progress = localDx.clamp(0.0, box.maxWidth - 30.0) / (box.maxWidth - 30.0);
+    setState(() {
+      switch (widget.trackType) {
+        case TrackType.hue:
+          hsvColor = hsvColor.withHue(progress * 359);
+          break;
+        case TrackType.saturation:
+          hsvColor = hsvColor.withSaturation(progress);
+          break;
+        case TrackType.value:
+          hsvColor = hsvColor.withValue(progress);
+          break;
+        default:
+          break;
+      }
+    });
+    widget.onProgressChanged(progress);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return LayoutBuilder(builder: (BuildContext context, BoxConstraints box) {
+      double thumbOffset = 15.0;
+      Color thumbColor = Colors.white;
+      switch (widget.trackType) {
+        case TrackType.hue:
+          thumbOffset += (box.maxWidth - 30.0) * hsvColor.hue / 360;
+          break;
+        case TrackType.saturation:
+          thumbOffset += (box.maxWidth - 30.0) * hsvColor.saturation;
+          break;
+        case TrackType.value:
+          thumbOffset += (box.maxWidth - 30.0) * hsvColor.value;
+          break;
+        default:
+          break;
+      }
+
+      return CustomMultiChildLayout(
+        delegate: _SliderLayout(),
+        children: <Widget>[
+          LayoutId(
+            id: _SliderLayout.track,
+            child: ClipRRect(
+              borderRadius: const BorderRadius.all(Radius.circular(50.0)),
+              child: CustomPaint(
+                  painter: TrackPainter(
+                TrackType.values.firstWhere((element) => element == widget.trackType),
+                hsvColor,
+              )),
+            ),
+          ),
+          LayoutId(
+            id: _SliderLayout.thumb,
+            child: Transform.translate(
+              offset: Offset(thumbOffset, 0.0),
+              child: CustomPaint(
+                painter: ThumbPainter(
+                  thumbColor: thumbColor,
+                  fullThumbColor: false,
+                ),
+              ),
+            ),
+          ),
+          LayoutId(
+            id: _SliderLayout.gestureContainer,
+            child: LayoutBuilder(
+              builder: (BuildContext context, BoxConstraints box) {
+                RenderBox? getBox = context.findRenderObject() as RenderBox?;
+                return GestureDetector(
+                  onPanDown: (DragDownDetails details) => getBox != null ? slideEvent(getBox, box, details.globalPosition) : null,
+                  onPanEnd: (details) => widget.onColorChangeEnd(),
+                  onPanUpdate: (DragUpdateDetails details) => getBox != null ? slideEvent(getBox, box, details.globalPosition) : null,
+                );
+              },
+            ),
+          ),
+        ],
+      );
+    });
+  }
+}*/
+
+/// 9 track types for slider picker widget.
+class ColorPickerSlider extends StatelessWidget {
+  const ColorPickerSlider(
+    this.trackType,
+    this.hsvColor,
+    this.onColorChanged,
+    this.onColorChangeEnd,
+    this.onProblem, {
+    Key? key,
+    this.displayThumbColor = false,
+    this.fullThumbColor = false,
+  }) : super(key: key);
+
+  final TrackType trackType;
+  final HSVColor hsvColor;
+  final ValueChanged<HSVColor> onColorChanged;
+  final void Function() onColorChangeEnd;
+  final void Function(int v) onProblem;
+  final bool displayThumbColor;
+  final bool fullThumbColor;
+
+  void slideEvent(RenderBox getBox, BoxConstraints box, Offset globalPosition) {
+    double localDx = getBox.globalToLocal(globalPosition).dx - 15.0;
+    double progress = localDx.clamp(0.0, box.maxWidth - 30.0) / (box.maxWidth - 30.0);
+    switch (trackType) {
+      case TrackType.hue:
+        // 360 is the same as zero
+        // if set to 360, sliding to end goes to zero
+        final newColor = hsvColor.withHue(progress * 359);
+        if (newColor.saturation == 0) {
+          onProblem(0);
+          return;
+        }
+        onColorChanged(newColor);
+        break;
+      case TrackType.saturation:
+        final newColor = hsvColor.withSaturation(progress);
+        if (newColor.value == 0) {
+          onProblem(1);
+          return;
+        }
+        onColorChanged(newColor);
+        break;
+      case TrackType.value:
+        onColorChanged(hsvColor.withValue(progress));
+        break;
+      default:
+        break;
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return LayoutBuilder(builder: (BuildContext context, BoxConstraints box) {
+      double thumbOffset = 15.0;
+      Color thumbColor;
+      switch (trackType) {
+        case TrackType.hue:
+          thumbOffset += (box.maxWidth - 30.0) * hsvColor.hue / 360;
+          thumbColor = HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, 1.0).toColor();
+          break;
+        case TrackType.saturation:
+          thumbOffset += (box.maxWidth - 30.0) * hsvColor.saturation;
+          thumbColor = HSVColor.fromAHSV(1.0, hsvColor.hue, hsvColor.saturation, 1.0).toColor();
+          break;
+        case TrackType.saturationForHSL:
+          thumbOffset += (box.maxWidth - 30.0) * hsvToHsl(hsvColor).saturation;
+          thumbColor = HSLColor.fromAHSL(1.0, hsvColor.hue, hsvToHsl(hsvColor).saturation, 0.5).toColor();
+          break;
+        case TrackType.value:
+          thumbOffset += (box.maxWidth - 30.0) * hsvColor.value;
+          thumbColor = HSVColor.fromAHSV(1.0, hsvColor.hue, 1.0, hsvColor.value).toColor();
+          break;
+        case TrackType.lightness:
+          thumbOffset += (box.maxWidth - 30.0) * hsvToHsl(hsvColor).lightness;
+          thumbColor = HSLColor.fromAHSL(1.0, hsvColor.hue, 1.0, hsvToHsl(hsvColor).lightness).toColor();
+          break;
+        case TrackType.red:
+          thumbOffset += (box.maxWidth - 30.0) * hsvColor.toColor().red / 0xff;
+          thumbColor = hsvColor.toColor().withOpacity(1.0);
+          break;
+        case TrackType.green:
+          thumbOffset += (box.maxWidth - 30.0) * hsvColor.toColor().green / 0xff;
+          thumbColor = hsvColor.toColor().withOpacity(1.0);
+          break;
+        case TrackType.blue:
+          thumbOffset += (box.maxWidth - 30.0) * hsvColor.toColor().blue / 0xff;
+          thumbColor = hsvColor.toColor().withOpacity(1.0);
+          break;
+        case TrackType.alpha:
+          thumbOffset += (box.maxWidth - 30.0) * hsvColor.toColor().opacity;
+          thumbColor = hsvColor.toColor().withOpacity(hsvColor.alpha);
+          break;
+      }
+
+      return CustomMultiChildLayout(
+        delegate: _SliderLayout(),
+        children: <Widget>[
+          LayoutId(
+            id: _SliderLayout.track,
+            child: ClipRRect(
+              borderRadius: const BorderRadius.all(Radius.circular(50.0)),
+              child: CustomPaint(
+                  painter: TrackPainter(
+                trackType,
+                hsvColor,
+              )),
+            ),
+          ),
+          LayoutId(
+            id: _SliderLayout.thumb,
+            child: Transform.translate(
+              offset: Offset(thumbOffset, 0.0),
+              child: CustomPaint(
+                painter: ThumbPainter(
+                  thumbColor: displayThumbColor ? thumbColor : null,
+                  fullThumbColor: fullThumbColor,
+                ),
+              ),
+            ),
+          ),
+          LayoutId(
+            id: _SliderLayout.gestureContainer,
+            child: LayoutBuilder(
+              builder: (BuildContext context, BoxConstraints box) {
+                RenderBox? getBox = context.findRenderObject() as RenderBox?;
+                return GestureDetector(
+                  onPanDown: (DragDownDetails details) => getBox != null ? slideEvent(getBox, box, details.globalPosition) : null,
+                  onPanEnd: (details) {
+                    if ((trackType == TrackType.hue && hsvColor.saturation == 0) || (trackType == TrackType.saturation && hsvColor.value == 0)) {
+                      return;
+                    }
+                    onColorChangeEnd();
+                  },
+                  onPanUpdate: (DragUpdateDetails details) => getBox != null ? slideEvent(getBox, box, details.globalPosition) : null,
+                );
+              },
+            ),
+          ),
+        ],
+      );
+    });
+  }
+}
+
+/// Simple round color indicator.
+class ColorIndicator extends StatelessWidget {
+  const ColorIndicator(
+    this.hsvColor, {
+    Key? key,
+    this.currentHsvColor,
+    this.icon,
+    this.width = 50.0,
+    this.height = 50.0,
+    this.adaptive = false,
+  }) : super(key: key);
+
+  final HSVColor hsvColor;
+  final HSVColor? currentHsvColor;
+  final double width;
+  final double height;
+  final IconData? icon;
+  final bool adaptive;
+
+  @override
+  Widget build(BuildContext context) {
+    Color color = hsvColor.toColor();
+
+    return Container(
+      width: width,
+      height: height,
+      decoration: BoxDecoration(
+        shape: BoxShape.circle,
+        color: color,
+        boxShadow: [
+          BoxShadow(
+              color: useWhiteForeground(color) ? Colors.white.withOpacity(.5) : Colors.black.withOpacity(.5),
+              offset: const Offset(0, 0),
+              blurRadius: 5)
+        ],
+      ),
+      child: Material(
+        color: Colors.transparent,
+        child: AnimatedOpacity(
+          duration: const Duration(milliseconds: 210),
+          opacity: (icon != null || currentHsvColor == hsvColor) &&
+                  (adaptive || Provider.of<SettingsProvider>(context, listen: false).accentColor != AccentColor.adaptive)
+              ? 1
+              : 0,
+          child: Icon(icon ?? Icons.done, color: useWhiteForeground(color) ? Colors.white : Colors.black),
+        ),
+      ),
+    );
+  }
+}
+
+/// Provide Rectangle & Circle 2 categories, 10 variations of palette widget.
+class ColorPickerArea extends StatelessWidget {
+  const ColorPickerArea(
+    this.hsvColor,
+    this.onColorChanged,
+    this.onChangeEnd,
+    this.paletteType, {
+    Key? key,
+  }) : super(key: key);
+
+  final HSVColor hsvColor;
+  final ValueChanged<HSVColor> onColorChanged;
+  final void Function() onChangeEnd;
+  final PaletteType paletteType;
+
+  /*void _handleColorRectChange(double horizontal, double vertical) {
+    onColorChanged(hsvColor.withSaturation(horizontal).withValue(vertical));
+  }*/
+
+  void _handleGesture(Offset position, BuildContext context, double height, double width) {
+    RenderBox? getBox = context.findRenderObject() as RenderBox?;
+    if (getBox == null) return;
+
+    Offset localOffset = getBox.globalToLocal(position);
+    double horizontal = localOffset.dx.clamp(0.0, width);
+    double vertical = localOffset.dy.clamp(0.0, height);
+
+    //_handleColorRectChange(horizontal / width, 1 - vertical / height);
+
+    onColorChanged(hsvColor.withSaturation(horizontal).withValue(vertical));
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return LayoutBuilder(
+      builder: (BuildContext context, BoxConstraints constraints) {
+        double width = constraints.maxWidth;
+        double height = constraints.maxHeight;
+
+        return RawGestureDetector(
+          gestures: {
+            _AlwaysWinPanGestureRecognizer: GestureRecognizerFactoryWithHandlers<_AlwaysWinPanGestureRecognizer>(
+              () => _AlwaysWinPanGestureRecognizer(),
+              (_AlwaysWinPanGestureRecognizer instance) {
+                instance
+                  ..onDown = ((details) => _handleGesture(details.globalPosition, context, height, width))
+                  ..onEnd = ((d) => onChangeEnd())
+                  ..onUpdate = ((details) => _handleGesture(details.globalPosition, context, height, width));
+              },
+            ),
+          },
+          child: Builder(
+            builder: (BuildContext _) {
+              return CustomPaint(painter: HSVWithHueColorPainter(hsvColor));
+            },
+          ),
+        );
+      },
+    );
+  }
+}
+
+class _AlwaysWinPanGestureRecognizer extends PanGestureRecognizer {
+  @override
+  void addAllowedPointer(event) {
+    super.addAllowedPointer(event);
+    resolve(GestureDisposition.accepted);
+  }
+
+  @override
+  String get debugDescription => 'alwaysWin';
+}
+
+/// Uppercase text formater
+class UpperCaseTextFormatter extends TextInputFormatter {
+  @override
+  TextEditingValue formatEditUpdate(oldValue, TextEditingValue newValue) =>
+      TextEditingValue(text: newValue.text.toUpperCase(), selection: newValue.selection);
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/utils.dart b/filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/utils.dart
new file mode 100644
index 0000000..515ae1f
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/utils.dart
@@ -0,0 +1,220 @@
+// FROM: https://pub.dev/packages/flutter_colorpicker
+// FROM: https://pub.dev/packages/flutter_colorpicker
+// FROM: https://pub.dev/packages/flutter_colorpicker
+// FROM: https://pub.dev/packages/flutter_colorpicker
+
+/// Common function lib
+
+import 'dart:math';
+import 'package:flutter/painting.dart';
+import 'colors.dart';
+
+/// Check if is good condition to use white foreground color by passing
+/// the background color, and optional bias.
+///
+/// Reference:
+///
+/// Old: https://www.w3.org/TR/WCAG20-TECHS/G18.html
+///
+/// New: https://github.com/mchome/flutter_statusbarcolor/issues/40
+bool useWhiteForeground(Color backgroundColor, {double bias = 0.0}) {
+  // Old:
+  // return 1.05 / (color.computeLuminance() + 0.05) > 4.5;
+
+  // New:
+  int v = sqrt(pow(backgroundColor.red, 2) * 0.299 +
+          pow(backgroundColor.green, 2) * 0.587 +
+          pow(backgroundColor.blue, 2) * 0.114)
+      .round();
+  return v < 130 + bias ? true : false;
+}
+
+/// Convert HSV to HSL
+///
+/// Reference: https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL
+HSLColor hsvToHsl(HSVColor color) {
+  double s = 0.0;
+  double l = 0.0;
+  l = (2 - color.saturation) * color.value / 2;
+  if (l != 0) {
+    if (l == 1) {
+      s = 0.0;
+    } else if (l < 0.5) {
+      s = color.saturation * color.value / (l * 2);
+    } else {
+      s = color.saturation * color.value / (2 - l * 2);
+    }
+  }
+  return HSLColor.fromAHSL(
+    color.alpha,
+    color.hue,
+    s.clamp(0.0, 1.0),
+    l.clamp(0.0, 1.0),
+  );
+}
+
+/// Convert HSL to HSV
+///
+/// Reference: https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_HSV
+HSVColor hslToHsv(HSLColor color) {
+  double s = 0.0;
+  double v = 0.0;
+
+  v = color.lightness + color.saturation * (color.lightness < 0.5 ? color.lightness : 1 - color.lightness);
+  if (v != 0) s = 2 - 2 * color.lightness / v;
+
+  return HSVColor.fromAHSV(
+    color.alpha,
+    color.hue,
+    s.clamp(0.0, 1.0),
+    v.clamp(0.0, 1.0),
+  );
+}
+
+/// [RegExp] pattern for validation HEX color [String] inputs, allows only:
+///
+/// * exactly 1 to 8 digits in HEX format,
+/// * only Latin A-F characters, case insensitive,
+/// * and integer numbers 0,1,2,3,4,5,6,7,8,9,
+/// * with optional hash (`#`) symbol at the beginning (not calculated in length).
+///
+/// ```dart
+/// final RegExp hexInputValidator = RegExp(kValidHexPattern);
+/// if (hexInputValidator.hasMatch(hex)) print('$hex might be a valid HEX color');
+/// ```
+/// Reference: https://en.wikipedia.org/wiki/Web_colors#Hex_triplet
+const String kValidHexPattern = r'^#?[0-9a-fA-F]{1,8}';
+
+/// [RegExp] pattern for validation complete HEX color [String], allows only:
+///
+/// * exactly 6 or 8 digits in HEX format,
+/// * only Latin A-F characters, case insensitive,
+/// * and integer numbers 0,1,2,3,4,5,6,7,8,9,
+/// * with optional hash (`#`) symbol at the beginning (not calculated in length).
+///
+/// ```dart
+/// final RegExp hexCompleteValidator = RegExp(kCompleteValidHexPattern);
+/// if (hexCompleteValidator.hasMatch(hex)) print('$hex is valid HEX color');
+/// ```
+/// Reference: https://en.wikipedia.org/wiki/Web_colors#Hex_triplet
+const String kCompleteValidHexPattern = r'^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$';
+
+/// Try to convert text input or any [String] to valid [Color].
+/// The [String] must be provided in one of those formats:
+///
+/// * RGB
+/// * #RGB
+/// * RRGGBB
+/// * #RRGGBB
+/// * AARRGGBB
+/// * #AARRGGBB
+///
+/// Where: A stands for Alpha, R for Red, G for Green, and B for blue color.
+/// It will only accept 3/6/8 long HEXs with an optional hash (`#`) at the beginning.
+/// Allowed characters are Latin A-F case insensitive and numbers 0-9.
+/// Optional [enableAlpha] can be provided (it's `true` by default). If it's set
+/// to `false` transparency information (alpha channel) will be removed.
+/// ```dart
+/// /// // Valid 3 digit HEXs:
+/// colorFromHex('abc') == Color(0xffaabbcc)
+/// colorFromHex('ABc') == Color(0xffaabbcc)
+/// colorFromHex('ABC') == Color(0xffaabbcc)
+/// colorFromHex('#Abc') == Color(0xffaabbcc)
+/// colorFromHex('#abc') == Color(0xffaabbcc)
+/// colorFromHex('#ABC') == Color(0xffaabbcc)
+/// // Valid 6 digit HEXs:
+/// colorFromHex('aabbcc') == Color(0xffaabbcc)
+/// colorFromHex('AABbcc') == Color(0xffaabbcc)
+/// colorFromHex('AABBCC') == Color(0xffaabbcc)
+/// colorFromHex('#AABbcc') == Color(0xffaabbcc)
+/// colorFromHex('#aabbcc') == Color(0xffaabbcc)
+/// colorFromHex('#AABBCC') == Color(0xffaabbcc)
+/// // Valid 8 digit HEXs:
+/// colorFromHex('ffaabbcc') == Color(0xffaabbcc)
+/// colorFromHex('ffAABbcc') == Color(0xffaabbcc)
+/// colorFromHex('ffAABBCC') == Color(0xffaabbcc)
+/// colorFromHex('ffaabbcc', enableAlpha: true) == Color(0xffaabbcc)
+/// colorFromHex('FFAAbbcc', enableAlpha: true) == Color(0xffaabbcc)
+/// colorFromHex('ffAABBCC', enableAlpha: true) == Color(0xffaabbcc)
+/// colorFromHex('FFaabbcc', enableAlpha: true) == Color(0xffaabbcc)
+/// colorFromHex('#ffaabbcc') == Color(0xffaabbcc)
+/// colorFromHex('#ffAABbcc') == Color(0xffaabbcc)
+/// colorFromHex('#FFAABBCC') == Color(0xffaabbcc)
+/// colorFromHex('#ffaabbcc', enableAlpha: true) == Color(0xffaabbcc)
+/// colorFromHex('#FFAAbbcc', enableAlpha: true) == Color(0xffaabbcc)
+/// colorFromHex('#ffAABBCC', enableAlpha: true) == Color(0xffaabbcc)
+/// colorFromHex('#FFaabbcc', enableAlpha: true) == Color(0xffaabbcc)
+/// // Invalid HEXs:
+/// colorFromHex('bc') == null // length 2
+/// colorFromHex('aabbc') == null // length 5
+/// colorFromHex('#ffaabbccd') == null // length 9 (+#)
+/// colorFromHex('aabbcx') == null // x character
+/// colorFromHex('#aabbвв') == null // в non-latin character
+/// colorFromHex('') == null // empty
+/// ```
+/// Reference: https://en.wikipedia.org/wiki/Web_colors#Hex_triplet
+Color? colorFromHex(String inputString, {bool enableAlpha = true}) {
+  // Registers validator for exactly 6 or 8 digits long HEX (with optional #).
+  final RegExp hexValidator = RegExp(kCompleteValidHexPattern);
+  // Validating input, if it does not match — it's not proper HEX.
+  if (!hexValidator.hasMatch(inputString)) return null;
+  // Remove optional hash if exists and convert HEX to UPPER CASE.
+  String hexToParse = inputString.replaceFirst('#', '').toUpperCase();
+  // It may allow HEXs with transparency information even if alpha is disabled,
+  if (!enableAlpha && hexToParse.length == 8) {
+    // but it will replace this info with 100% non-transparent value (FF).
+    hexToParse = 'FF${hexToParse.substring(2)}';
+  }
+  // HEX may be provided in 3-digits format, let's just duplicate each letter.
+  if (hexToParse.length == 3) {
+    hexToParse = hexToParse.split('').expand((i) => [i * 2]).join();
+  }
+  // We will need 8 digits to parse the color, let's add missing digits.
+  if (hexToParse.length == 6) hexToParse = 'FF$hexToParse';
+  // HEX must be valid now, but as a precaution, it will just "try" to parse it.
+  final intColorValue = int.tryParse(hexToParse, radix: 16);
+  // If for some reason HEX is not valid — abort the operation, return nothing.
+  if (intColorValue == null) return null;
+  // Register output color for the last step.
+  final color = Color(intColorValue);
+  // Decide to return color with transparency information or not.
+  return enableAlpha ? color : color.withAlpha(255);
+}
+
+/// Converts `dart:ui` [Color] to the 6/8 digits HEX [String].
+///
+/// Prefixes a hash (`#`) sign if [includeHashSign] is set to `true`.
+/// The result will be provided as UPPER CASE, it can be changed via [toUpperCase]
+/// flag set to `false` (default is `true`). Hex can be returned without alpha
+/// channel information (transparency), with the [enableAlpha] flag set to `false`.
+String colorToHex(
+  Color color, {
+  bool includeHashSign = false,
+  bool enableAlpha = true,
+  bool toUpperCase = true,
+}) {
+  final String hex = (includeHashSign ? '#' : '') +
+      (enableAlpha ? _padRadix(color.alpha) : '') +
+      _padRadix(color.red) +
+      _padRadix(color.green) +
+      _padRadix(color.blue);
+  return toUpperCase ? hex.toUpperCase() : hex;
+}
+
+// Shorthand for padLeft of RadixString, DRY.
+String _padRadix(int value) => value.toRadixString(16).padLeft(2, '0');
+
+// Extension for String
+extension ColorExtension1 on String {
+  Color? toColor() {
+    Color? color = colorFromName(this);
+    if (color != null) return color;
+    return colorFromHex(this);
+  }
+}
+
+// Extension from Color
+extension ColorExtension2 on Color {
+  String toHexString({bool includeHashSign = false, bool enableAlpha = true, bool toUpperCase = true}) =>
+      colorToHex(this, includeHashSign: false, enableAlpha: true, toUpperCase: true);
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_input.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_input.dart
new file mode 100644
index 0000000..1b26475
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_input.dart
@@ -0,0 +1,156 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+class GoalInput extends StatelessWidget {
+  const GoalInput({Key? key, required this.currentAverage, required this.value, required this.onChanged}) : super(key: key);
+
+  final double currentAverage;
+  final double value;
+  final void Function(double value) onChanged;
+
+  void offsetToValue(Offset offset, Size size) {
+    double v = ((offset.dx / size.width * 4 + 1) * 10).round() / 10;
+    v = v.clamp(1.5, 5);
+    v = v.clamp(((currentAverage * 10).round() / 10), 5);
+    setValue(v);
+  }
+
+  void setValue(double v) {
+    if (v != value) {
+      HapticFeedback.lightImpact();
+    }
+    onChanged(v);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    List<int> presets = [2, 3, 4, 5];
+    presets = presets.where((e) => gradeToAvg(e) > currentAverage).toList();
+
+    return Column(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        LayoutBuilder(builder: (context, size) {
+          return GestureDetector(
+            onTapDown: (details) {
+              offsetToValue(details.localPosition, size.biggest);
+            },
+            onHorizontalDragUpdate: (details) {
+              offsetToValue(details.localPosition, size.biggest);
+            },
+            child: SizedBox(
+              height: 32.0,
+              width: double.infinity,
+              child: Padding(
+                padding: const EdgeInsets.only(right: 20.0),
+                child: CustomPaint(
+                  painter: GoalSliderPainter(value: (value - 1) / 4),
+                ),
+              ),
+            ),
+          );
+        }),
+        const SizedBox(height: 12.0),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: presets.map((e) {
+            final pv = (value * 10).round() / 10;
+            final selected = gradeToAvg(e) == pv;
+            return Padding(
+              padding: const EdgeInsets.symmetric(horizontal: 12.0),
+              child: Container(
+                decoration: BoxDecoration(
+                  borderRadius: BorderRadius.circular(99.0),
+                  color: gradeColor(e).withOpacity(selected ? 1.0 : 0.2),
+                  border: Border.all(color: gradeColor(e), width: 4),
+                ),
+                child: Material(
+                  type: MaterialType.transparency,
+                  child: InkWell(
+                    borderRadius: BorderRadius.circular(99.0),
+                    onTap: () => setValue(gradeToAvg(e)),
+                    child: Padding(
+                      padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 24.0),
+                      child: Text(
+                        e.toString(),
+                        style: TextStyle(
+                          color: selected ? Colors.white : gradeColor(e),
+                          fontWeight: FontWeight.bold,
+                          fontSize: 24.0,
+                        ),
+                      ),
+                    ),
+                  ),
+                ),
+              ),
+            );
+          }).toList(),
+        )
+      ],
+    );
+  }
+}
+
+class GoalSliderPainter extends CustomPainter {
+  final double value;
+
+  GoalSliderPainter({required this.value});
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final radius = size.height / 2;
+    const cpadding = 4;
+    final rect = Rect.fromLTWH(0, 0, size.width + radius, size.height);
+    final vrect = Rect.fromLTWH(0, 0, size.width * value + radius, size.height);
+    canvas.drawRRect(
+      RRect.fromRectAndRadius(
+        rect,
+        const Radius.circular(99.0),
+      ),
+      Paint()..color = Colors.black.withOpacity(.1),
+    );
+    canvas.drawRRect(
+      RRect.fromRectAndRadius(
+        vrect,
+        const Radius.circular(99.0),
+      ),
+      Paint()
+        ..shader = const LinearGradient(colors: [
+          Color(0xffFF3B30),
+          Color(0xffFF9F0A),
+          Color(0xffFFD60A),
+          Color(0xff34C759),
+          Color(0xff247665),
+        ]).createShader(rect),
+    );
+    canvas.drawOval(
+      Rect.fromCircle(center: Offset(size.width * value, size.height / 2), radius: radius - cpadding),
+      Paint()..color = Colors.white,
+    );
+    for (int i = 1; i < 4; i++) {
+      canvas.drawOval(
+        Rect.fromCircle(center: Offset(size.width / 4 * i, size.height / 2), radius: 4),
+        Paint()..color = Colors.white.withOpacity(.5),
+      );
+    }
+  }
+
+  @override
+  bool shouldRepaint(GoalSliderPainter oldDelegate) {
+    return oldDelegate.value != value;
+  }
+}
+
+double gradeToAvg(int grade) {
+  return grade - 0.5;
+}
+
+Color gradeColor(int grade) {
+  return [
+    const Color(0xffFF3B30),
+    const Color(0xffFF9F0A),
+    const Color(0xffFFD60A),
+    const Color(0xff34C759),
+    const Color(0xff247665),
+  ].elementAt(grade.clamp(1, 5) - 1);
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner.dart
new file mode 100644
index 0000000..cbacb55
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner.dart
@@ -0,0 +1,172 @@
+/* 
+ * Maintainer: DarK
+ * Translated from C version
+ * Minimal Working Fixed @ 2022.12.25
+ * ##Please do NOT modify if you don't know whats going on##
+ * 
+ * Issue: #59
+ * 
+ * Future changes / ideas:
+ *  - `best` should be configurable
+ */
+import 'dart:math';
+import 'package:filcnaplo_kreta_api/models/category.dart';
+import 'package:filcnaplo_kreta_api/models/grade.dart';
+import 'package:filcnaplo_kreta_api/models/subject.dart';
+import 'package:flutter/foundation.dart' show listEquals;
+
+/// Generate list of grades that achieve the wanted goal.
+/// After generating possible options, it (when doing so would NOT result in empty list) filters with two criteria:
+///  - Plan should not contain more than 15 grades
+///  - Plan should not contain only one type of grade
+///
+/// **Usage**:
+///
+/// ```dart
+/// List<int> GoalPlanner(double goal, List<Grade> grades).solve().plan
+/// ```
+class GoalPlanner {
+  final double goal;
+  final List<Grade> grades;
+  List<Plan> plans = [];
+  GoalPlanner(this.goal, this.grades);
+
+  bool _allowed(int grade) => grade > goal;
+
+  void _generate(Generator g) {
+    // Exit condition 1: Generator has working plan.
+    if (g.currentAvg.avg >= goal) {
+      plans.add(Plan(g.plan));
+      return;
+    }
+    // Exit condition 2: Generator plan will never work.
+    if (!_allowed(g.gradeToAdd)) {
+      return;
+    }
+
+    for (int i = g.max; i >= 0; i--) {
+      int newGradeToAdd = g.gradeToAdd - 1;
+      List<int> newPlan = GoalPlannerHelper._addToList<int>(g.plan, g.gradeToAdd, i);
+
+      Avg newAvg = GoalPlannerHelper._addToAvg(g.currentAvg, g.gradeToAdd, i);
+      int newN = GoalPlannerHelper.howManyNeeded(
+          newGradeToAdd,
+          grades +
+              newPlan
+                  .map((e) => Grade(
+                        id: '',
+                        date: DateTime(0),
+                        value: GradeValue(e, '', '', 100),
+                        teacher: '',
+                        description: '',
+                        form: '',
+                        groupId: '',
+                        type: GradeType.midYear,
+                        subject: Subject.fromJson({}),
+                        mode: Category.fromJson({}),
+                        seenDate: DateTime(0),
+                        writeDate: DateTime(0),
+                      ))
+                  .toList(),
+          goal);
+
+      _generate(Generator(newGradeToAdd, newN, newAvg, newPlan));
+    }
+  }
+
+  List<Plan> solve() {
+    _generate(
+      Generator(
+        5,
+        GoalPlannerHelper.howManyNeeded(
+          5,
+          grades,
+          goal,
+        ),
+        Avg(GoalPlannerHelper.averageEvals(grades), GoalPlannerHelper.weightSum(grades)),
+        [],
+      ),
+    );
+
+    // Calculate Statistics
+    for (var e in plans) {
+      e.sum = e.plan.fold(0, (int a, b) => a + b);
+      e.avg = e.sum / e.plan.length;
+      e.sigma = sqrt(e.plan.map((i) => pow(i - e.avg, 2)).fold(0, (num a, b) => a + b) / e.plan.length);
+    }
+
+    // filter without aggression
+    if (plans.where((e) => e.plan.length < 30).isNotEmpty) {
+      plans.removeWhere((e) => !(e.plan.length < 30));
+    }
+    if (plans.where((e) => e.sigma > 1).isNotEmpty) {
+      plans.removeWhere((e) => !(e.sigma > 1));
+    }
+
+    return plans;
+  }
+}
+
+class Avg {
+  final double avg;
+  final double n;
+
+  Avg(this.avg, this.n);
+}
+
+class Generator {
+  final int gradeToAdd;
+  final int max;
+  final Avg currentAvg;
+  final List<int> plan;
+
+  Generator(this.gradeToAdd, this.max, this.currentAvg, this.plan);
+}
+
+class Plan {
+  final List<int> plan;
+  int sum = 0;
+  double avg = 0;
+  int med = 0; // currently
+  int mod = 0; // unused
+  double sigma = 0;
+
+  Plan(this.plan);
+
+  @override
+  bool operator ==(other) => other is Plan && listEquals(plan, other.plan);
+
+  @override
+  int get hashCode => Object.hashAll(plan);
+}
+
+class GoalPlannerHelper {
+  static Avg _addToAvg(Avg base, int grade, int n) => Avg((base.avg * base.n + grade * n) / (base.n + n), base.n + n);
+
+  static List<T> _addToList<T>(List<T> l, T e, int n) {
+    if (n == 0) return l;
+    List<T> tmp = l;
+    for (int i = 0; i < n; i++) {
+      tmp = tmp + [e];
+    }
+    return tmp;
+  }
+
+  static int howManyNeeded(int grade, List<Grade> base, double goal) {
+    double avg = averageEvals(base);
+    double wsum = weightSum(base);
+    if (avg >= goal) return 0;
+    if (grade * 1.0 == goal) return -1;
+    int candidate = (wsum * (avg - goal) / (goal - grade)).floor();
+    return (candidate * grade + avg * wsum) / (candidate + wsum) < goal ? candidate + 1 : candidate;
+  }
+
+  static double averageEvals(List<Grade> grades, {bool finalAvg = false}) {
+    double average =
+        grades.map((e) => e.value.value * e.value.weight / 100.0).fold(0.0, (double a, double b) => a + b) / weightSum(grades, finalAvg: finalAvg);
+    return average.isNaN ? 0.0 : average;
+  }
+
+  static double weightSum(List<Grade> grades, {bool finalAvg = false}) =>
+      grades.map((e) => finalAvg ? 1 : e.value.weight / 100).fold(0, (a, b) => a + b);
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/goal_planner/grade_display.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/grade_display.dart
new file mode 100644
index 0000000..631973f
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/grade_display.dart
@@ -0,0 +1,30 @@
+import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_input.dart';
+import 'package:flutter/material.dart';
+
+class GradeDisplay extends StatelessWidget {
+  const GradeDisplay({Key? key, required this.grade}) : super(key: key);
+
+  final int grade;
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      width: 36,
+      height: 36,
+      decoration: BoxDecoration(
+        shape: BoxShape.circle,
+        color: gradeColor(grade).withOpacity(.3),
+      ),
+      child: Center(
+        child: Text(
+          grade.toInt().toString(),
+          style: TextStyle(
+            fontWeight: FontWeight.bold,
+            fontSize: 22.0,
+            color: gradeColor(grade),
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/goal_planner/route_option.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/route_option.dart
new file mode 100644
index 0000000..cfcc55a
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/route_option.dart
@@ -0,0 +1,126 @@
+import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner.dart';
+import 'package:filcnaplo_premium/ui/mobile/goal_planner/grade_display.dart';
+import 'package:flutter/material.dart';
+
+enum RouteMark { recommended, fastest }
+
+class RouteOption extends StatelessWidget {
+  const RouteOption({Key? key, required this.plan, this.mark, this.selected = false, required this.onSelected}) : super(key: key);
+
+  final Plan plan;
+  final RouteMark? mark;
+  final bool selected;
+  final void Function() onSelected;
+
+  Widget markLabel() {
+    const style = TextStyle(fontWeight: FontWeight.bold);
+
+    switch (mark!) {
+      case RouteMark.recommended:
+        return const Text("Recommended", style: style);
+      case RouteMark.fastest:
+        return const Text("Fastest", style: style);
+    }
+  }
+
+  Color markColor() {
+    switch (mark) {
+      case RouteMark.recommended:
+        return const Color(0xff6a63d4);
+      case RouteMark.fastest:
+        return const Color(0xffe9d524);
+      default:
+        return Colors.teal;
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    List<Widget> gradeWidgets = [];
+
+    for (int i = 5; i > 1; i--) {
+      final count = plan.plan.where((e) => e == i).length;
+
+      if (count > 4) {
+        gradeWidgets.add(Row(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            Text(
+              "${count}x",
+              style: TextStyle(
+                fontSize: 22.0,
+                fontWeight: FontWeight.w500,
+                color: Colors.black.withOpacity(.7),
+              ),
+            ),
+            const SizedBox(width: 4.0),
+            GradeDisplay(grade: i),
+          ],
+        ));
+      } else {
+        gradeWidgets.addAll(List.generate(count, (_) => GradeDisplay(grade: i)));
+      }
+
+      if (count > 0) {
+        gradeWidgets.add(SizedBox(
+          height: 36.0,
+          width: 32.0,
+          child: Center(child: Icon(Icons.add, color: Colors.black.withOpacity(.5))),
+        ));
+      }
+    }
+
+    gradeWidgets.removeLast();
+
+    return Padding(
+      padding: const EdgeInsets.only(bottom: 12.0),
+      child: SizedBox(
+        width: double.infinity,
+        child: Card(
+          surfaceTintColor: selected ? markColor().withOpacity(.2) : Colors.white,
+          margin: EdgeInsets.zero,
+          elevation: 5,
+          shadowColor: Colors.transparent,
+          shape: RoundedRectangleBorder(
+            borderRadius: BorderRadius.circular(16.0),
+            side: selected ? BorderSide(color: markColor(), width: 4.0) : BorderSide.none,
+          ),
+          child: InkWell(
+            borderRadius: BorderRadius.circular(16.0),
+            onTap: onSelected,
+            child: Padding(
+              padding: const EdgeInsets.only(top: 16.0, bottom: 16.0, left: 20.0, right: 12.0),
+              child: Column(
+                mainAxisSize: MainAxisSize.min,
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  if (mark != null) ...[
+                    Chip(
+                      label: markLabel(),
+                      visualDensity: VisualDensity.compact,
+                      backgroundColor: selected ? markColor() : Colors.transparent,
+                      labelPadding: const EdgeInsets.symmetric(horizontal: 8.0),
+                      labelStyle: TextStyle(color: selected ? Colors.white : null),
+                      shape: StadiumBorder(
+                        side: BorderSide(
+                          color: markColor(),
+                          width: 3.0,
+                        ),
+                      ),
+                    ),
+                    const SizedBox(height: 6.0),
+                  ],
+                  Wrap(
+                    spacing: 4.0,
+                    runSpacing: 8.0,
+                    children: gradeWidgets,
+                  ),
+                ],
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/goal_planner/test.dart b/filcnaplo_premium/lib/ui/mobile/goal_planner/test.dart
new file mode 100644
index 0000000..78126bb
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/goal_planner/test.dart
@@ -0,0 +1,209 @@
+import 'package:filcnaplo_kreta_api/models/grade.dart';
+import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_input.dart';
+import 'package:filcnaplo_premium/ui/mobile/goal_planner/goal_planner.dart';
+import 'package:filcnaplo_premium/ui/mobile/goal_planner/route_option.dart';
+import 'package:flutter/material.dart';
+
+enum PlanResult {
+  available, // There are possible solutions
+  unreachable, // The solutions are too hard don't even try
+  unsolvable, // There are no solutions
+  reached, // Goal already reached
+}
+
+class GoalPlannerTest extends StatefulWidget {
+  const GoalPlannerTest({Key? key}) : super(key: key);
+
+  @override
+  State<GoalPlannerTest> createState() => _GoalPlannerTestState();
+}
+
+class _GoalPlannerTestState extends State<GoalPlannerTest> {
+  double goalValue = 4.0;
+  List<Grade> grades = [];
+
+  Plan? recommended;
+  Plan? fastest;
+  Plan? selectedRoute;
+  List<Plan> otherPlans = [];
+
+  PlanResult getResult() {
+    final currentAvg = GoalPlannerHelper.averageEvals(grades);
+
+    recommended = null;
+    fastest = null;
+    otherPlans = [];
+
+    if (currentAvg >= goalValue) return PlanResult.reached;
+
+    final planner = GoalPlanner(goalValue, grades);
+    final plans = planner.solve();
+
+    plans.sort((a, b) => (a.avg - (2 * goalValue + 5) / 3).abs().compareTo(b.avg - (2 * goalValue + 5) / 3));
+
+    try {
+      final singleSolution = plans.every((e) => e.sigma == 0);
+      recommended = plans.where((e) => singleSolution ? true : e.sigma > 0).first;
+      plans.removeWhere((e) => e == recommended);
+    } catch (_) {}
+
+    plans.sort((a, b) => a.plan.length.compareTo(b.plan.length));
+
+    try {
+      fastest = plans.removeAt(0);
+    } catch (_) {}
+
+    if ((recommended?.plan.length ?? 0) - (fastest?.plan.length ?? 0) >= 3) {
+      recommended = fastest;
+    }
+
+    if (recommended == null) {
+      recommended = null;
+      fastest = null;
+      otherPlans = [];
+      selectedRoute = null;
+      return PlanResult.unsolvable;
+    }
+
+    if (recommended!.plan.length > 10) {
+      recommended = null;
+      fastest = null;
+      otherPlans = [];
+      selectedRoute = null;
+      return PlanResult.unreachable;
+    }
+
+    otherPlans = List.from(plans);
+
+    return PlanResult.available;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final currentAvg = GoalPlannerHelper.averageEvals(grades);
+
+    final result = getResult();
+
+    return Scaffold(
+      body: SafeArea(
+        child: ListView(
+          padding: const EdgeInsets.all(20.0),
+          children: [
+            const Align(
+              alignment: Alignment.topLeft,
+              child: BackButton(),
+            ),
+            const SizedBox(height: 12.0),
+            const Text(
+              "Set a goal",
+              style: TextStyle(
+                fontWeight: FontWeight.bold,
+                fontSize: 20.0,
+              ),
+            ),
+            const SizedBox(height: 4.0),
+            Text(
+              goalValue.toString(),
+              style: TextStyle(
+                fontWeight: FontWeight.w900,
+                fontSize: 42.0,
+                color: gradeColor(goalValue.round()),
+              ),
+            ),
+            const SizedBox(height: 24.0),
+            const Text(
+              "Pick a route",
+              style: TextStyle(
+                fontWeight: FontWeight.bold,
+                fontSize: 20.0,
+              ),
+            ),
+            const SizedBox(height: 12.0),
+            if (recommended != null)
+              RouteOption(
+                plan: recommended!,
+                mark: RouteMark.recommended,
+                selected: selectedRoute == recommended!,
+                onSelected: () => setState(() {
+                  selectedRoute = recommended;
+                }),
+              ),
+            if (fastest != null && fastest != recommended)
+              RouteOption(
+                plan: fastest!,
+                mark: RouteMark.fastest,
+                selected: selectedRoute == fastest!,
+                onSelected: () => setState(() {
+                  selectedRoute = fastest;
+                }),
+              ),
+            ...otherPlans.map((e) => RouteOption(
+                  plan: e,
+                  selected: selectedRoute == e,
+                  onSelected: () => setState(() {
+                    selectedRoute = e;
+                  }),
+                )),
+            if (result != PlanResult.available) Text(result.name),
+          ],
+        ),
+      ),
+      bottomSheet: MediaQuery.removePadding(
+        context: context,
+        removeBottom: false,
+        removeTop: true,
+        child: Container(
+          color: Theme.of(context).scaffoldBackgroundColor,
+          child: Container(
+            padding: const EdgeInsets.only(top: 24.0),
+            decoration: BoxDecoration(
+                color: const Color.fromARGB(255, 215, 255, 242),
+                borderRadius: const BorderRadius.vertical(top: Radius.circular(24.0)),
+                boxShadow: [
+                  BoxShadow(
+                    color: Colors.black.withOpacity(.1),
+                    blurRadius: 8.0,
+                  )
+                ]),
+            child: SafeArea(
+              child: Padding(
+                padding: const EdgeInsets.symmetric(horizontal: 20.0),
+                child: Column(
+                  mainAxisSize: MainAxisSize.min,
+                  children: [
+                    GoalInput(
+                      value: goalValue,
+                      currentAverage: currentAvg,
+                      onChanged: (v) => setState(() {
+                        selectedRoute = null;
+                        goalValue = v;
+                      }),
+                    ),
+                    const SizedBox(height: 24.0),
+                    SizedBox(
+                      width: double.infinity,
+                      child: RawMaterialButton(
+                        onPressed: () {},
+                        fillColor: const Color(0xff01342D),
+                        shape: const StadiumBorder(),
+                        padding: const EdgeInsets.symmetric(vertical: 8.0),
+                        child: const Text(
+                          "Track it!",
+                          style: TextStyle(
+                            color: Colors.white,
+                            fontSize: 20.0,
+                            fontWeight: FontWeight.w600,
+                          ),
+                        ),
+                      ),
+                    )
+                  ],
+                ),
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/grades/average_selector.dart b/filcnaplo_premium/lib/ui/mobile/grades/average_selector.dart
new file mode 100644
index 0000000..8013e5f
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/grades/average_selector.dart
@@ -0,0 +1,92 @@
+import 'package:filcnaplo/theme/colors/colors.dart';
+import 'package:filcnaplo_premium/models/premium_scopes.dart';
+import 'package:filcnaplo_premium/providers/premium_provider.dart';
+import 'package:filcnaplo_premium/ui/mobile/premium/upsell.dart';
+import 'package:flutter/material.dart';
+import 'package:dropdown_button2/dropdown_button2.dart';
+import 'package:filcnaplo_mobile_ui/pages/grades/grades_page.i18n.dart';
+import 'package:flutter_feather_icons/flutter_feather_icons.dart';
+import 'package:provider/provider.dart';
+
+final Map<int, String> avgDropItems = {
+  0: "annual_average".i18n,
+  90: "3_months_average".i18n,
+  30: "30_days_average".i18n,
+  14: "14_days_average".i18n,
+  7: "7_days_average".i18n,
+};
+
+class PremiumAverageSelector extends StatelessWidget {
+  const PremiumAverageSelector({Key? key, this.onChanged, required this.value}) : super(key: key);
+
+  final Function(int?)? onChanged;
+  final int value;
+
+  @override
+  Widget build(BuildContext context) {
+    return DropdownButton2<int>(
+      items: avgDropItems.keys
+          .map((item) => DropdownMenuItem<int>(
+                value: item,
+                child: Text(
+                  avgDropItems[item] ?? "",
+                  style: TextStyle(
+                    fontSize: 14,
+                    fontWeight: FontWeight.bold,
+                    color: AppColors.of(context).text,
+                  ),
+                  overflow: TextOverflow.ellipsis,
+                ),
+              ))
+          .toList(),
+      onChanged: (int? value) {
+        if (Provider.of<PremiumProvider>(context, listen: false).hasScope(PremiumScopes.gradeStats)) {
+          if (onChanged != null) onChanged!(value);
+        } else {
+          PremiumLockedFeatureUpsell.show(context: context, feature: PremiumFeature.gradestats);
+        }
+      },
+      value: value,
+      iconSize: 14,
+      iconEnabledColor: AppColors.of(context).text,
+      iconDisabledColor: AppColors.of(context).text,
+      underline: const SizedBox(),
+      itemHeight: 40,
+      itemPadding: const EdgeInsets.only(left: 14, right: 14),
+      dropdownWidth: 200,
+      dropdownPadding: null,
+      buttonDecoration: BoxDecoration(
+        borderRadius: BorderRadius.circular(8),
+      ),
+      dropdownDecoration: BoxDecoration(
+        borderRadius: BorderRadius.circular(14),
+      ),
+      dropdownElevation: 8,
+      scrollbarRadius: const Radius.circular(40),
+      scrollbarThickness: 6,
+      scrollbarAlwaysShow: true,
+      offset: const Offset(-10, -10),
+      buttonSplashColor: Colors.transparent,
+      customButton: SizedBox(
+        height: 30,
+        child: Row(
+          children: [
+            Text(avgDropItems[value] ?? "",
+                style: Theme.of(context)
+                    .textTheme
+                    .titleSmall!
+                    .copyWith(fontWeight: FontWeight.w600, color: AppColors.of(context).text.withOpacity(0.65))),
+            const SizedBox(
+              width: 4,
+            ),
+            Icon(
+              FeatherIcons.chevronDown,
+              size: 16,
+              color: AppColors.of(context).text,
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/premium/activation_view/activation_dashboard.dart b/filcnaplo_premium/lib/ui/mobile/premium/activation_view/activation_dashboard.dart
new file mode 100644
index 0000000..0a113a2
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/premium/activation_view/activation_dashboard.dart
@@ -0,0 +1,182 @@
+import 'package:filcnaplo/theme/colors/colors.dart';
+import 'package:filcnaplo_premium/providers/premium_provider.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_feather_icons/flutter_feather_icons.dart';
+import 'package:flutter_svg/svg.dart';
+import 'package:flutter/services.dart';
+import 'package:provider/provider.dart';
+
+class ActivationDashboard extends StatefulWidget {
+  const ActivationDashboard({super.key});
+
+  @override
+  State<ActivationDashboard> createState() => _ActivationDashboardState();
+}
+
+class _ActivationDashboardState extends State<ActivationDashboard> {
+  bool manualActivationLoading = false;
+
+  Future<void> onManualActivation() async {
+    final data = await Clipboard.getData("text/plain");
+    if (data == null || data.text == null || data.text == "") {
+      return;
+    }
+    setState(() {
+      manualActivationLoading = true;
+    });
+    final result = await context.read<PremiumProvider>().auth.finishAuth(data.text!);
+    setState(() {
+      manualActivationLoading = false;
+    });
+
+    if (!result && mounted) {
+      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
+        content: Text(
+          "Sikertelen aktiválás. Kérlek próbáld újra később!",
+          style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
+        ),
+        backgroundColor: Colors.red,
+      ));
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 24.0),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          const Spacer(),
+          Center(
+            child: SvgPicture.asset(
+              "assets/images/github.svg",
+              height: 64.0,
+            ),
+          ),
+          const SizedBox(height: 32.0),
+          const Text(
+            "Jelentkezz be a GitHub felületén és adj hozzáférést a Filcnek, hogy aktiváld a Premiumot.",
+            textAlign: TextAlign.center,
+            style: TextStyle(fontWeight: FontWeight.w700, fontSize: 18.0),
+          ),
+          const SizedBox(height: 12.0),
+          Card(
+            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
+            child: Padding(
+              padding: const EdgeInsets.all(20.0),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Row(
+                    children: const [
+                      Icon(FeatherIcons.alertTriangle, size: 20.0, color: Colors.orange),
+                      SizedBox(width: 12.0),
+                      Text(
+                        "Figyelem!",
+                        style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold),
+                      ),
+                    ],
+                  ),
+                  const SizedBox(height: 6.0),
+                  const Text(
+                    "Csak akkor érzékeli a Filc a támogatói státuszod, ha nem állítod privátra!",
+                    style: TextStyle(fontSize: 16.0),
+                  ),
+                ],
+              ),
+            ),
+          ),
+          const SizedBox(height: 12.0),
+          Card(
+            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
+            child: Padding(
+              padding: const EdgeInsets.all(20.0),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Row(
+                    children: const [
+                      Icon(FeatherIcons.alertTriangle, size: 20.0, color: Colors.orange),
+                      SizedBox(width: 12.0),
+                      Text(
+                        "Figyelem!",
+                        style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold),
+                      ),
+                    ],
+                  ),
+                  const SizedBox(height: 6.0),
+                  const Text(
+                    "Ha friss támogató vagy, 5-10 percbe telhet az aktiválás. Kérlek gyere vissza később, és próbáld újra!",
+                    style: TextStyle(fontSize: 16.0),
+                  ),
+                ],
+              ),
+            ),
+          ),
+          const SizedBox(height: 12.0),
+          Card(
+            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14.0)),
+            child: Padding(
+              padding: const EdgeInsets.all(20.0),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  const Text(
+                    "Ha bejelentkezés után nem lép vissza az alkalmazásba automatikusan, aktiváld a támogatásod manuálisan",
+                    style: TextStyle(fontSize: 15.0, fontWeight: FontWeight.w500),
+                  ),
+                  const SizedBox(height: 6.0),
+                  Center(
+                    child: TextButton.icon(
+                      onPressed: onManualActivation,
+                      style: ButtonStyle(
+                        foregroundColor: MaterialStatePropertyAll(Theme.of(context).colorScheme.secondary),
+                        overlayColor: MaterialStatePropertyAll(Theme.of(context).colorScheme.secondary.withOpacity(.1)),
+                      ),
+                      icon: manualActivationLoading
+                          ? const SizedBox(
+                              child: CircularProgressIndicator(),
+                              height: 16.0,
+                              width: 16.0,
+                            )
+                          : const Icon(FeatherIcons.key, size: 20.0),
+                      label: const Padding(
+                        padding: EdgeInsets.only(left: 8.0),
+                        child: Text(
+                          "Aktiválás tokennel",
+                          style: TextStyle(fontSize: 16.0),
+                        ),
+                      ),
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ),
+          const Spacer(),
+          Padding(
+            padding: const EdgeInsets.only(bottom: 24.0),
+            child: Center(
+              child: TextButton.icon(
+                onPressed: () {
+                  Navigator.of(context).pop();
+                },
+                style: ButtonStyle(
+                  foregroundColor: MaterialStatePropertyAll(AppColors.of(context).text),
+                  overlayColor: MaterialStatePropertyAll(AppColors.of(context).text.withOpacity(.1)),
+                ),
+                icon: const Icon(FeatherIcons.arrowLeft, size: 20.0),
+                label: const Text(
+                  "Vissza",
+                  style: TextStyle(fontSize: 16.0),
+                ),
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/premium/activation_view/activation_view.dart b/filcnaplo_premium/lib/ui/mobile/premium/activation_view/activation_view.dart
new file mode 100644
index 0000000..118e907
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/premium/activation_view/activation_view.dart
@@ -0,0 +1,67 @@
+import 'package:animations/animations.dart';
+import 'package:filcnaplo_premium/providers/premium_provider.dart';
+import 'package:filcnaplo_premium/ui/mobile/premium/activation_view/activation_dashboard.dart';
+import 'package:flutter/material.dart';
+import 'package:lottie/lottie.dart';
+import 'package:provider/provider.dart';
+
+class PremiumActivationView extends StatefulWidget {
+  const PremiumActivationView({super.key});
+
+  @override
+  State<PremiumActivationView> createState() => _PremiumActivationViewState();
+}
+
+class _PremiumActivationViewState extends State<PremiumActivationView> with SingleTickerProviderStateMixin {
+  late AnimationController animation;
+  bool activated = false;
+
+  @override
+  void initState() {
+    super.initState();
+    context.read<PremiumProvider>().auth.initAuth();
+
+    animation = AnimationController(vsync: this, duration: const Duration(seconds: 2));
+  }
+
+  @override
+  void dispose() {
+    animation.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final premium = context.watch<PremiumProvider>();
+
+    if (premium.hasPremium && !activated) {
+      activated = true;
+      animation.forward();
+      WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+        Future.delayed(const Duration(seconds: 2)).then((value) {
+          if (mounted) Navigator.of(context).pop();
+        });
+      });
+    }
+
+    return Scaffold(
+      body: PageTransitionSwitcher(
+        transitionBuilder: (child, primaryAnimation, secondaryAnimation) => SharedAxisTransition(
+          animation: primaryAnimation,
+          secondaryAnimation: secondaryAnimation,
+          transitionType: SharedAxisTransitionType.horizontal,
+          fillColor: Colors.transparent,
+          child: child,
+        ),
+        child: premium.hasPremium
+            ? Center(
+                child: SizedBox(
+                  width: 400,
+                  child: Lottie.network("https://assets2.lottiefiles.com/packages/lf20_wkebwzpz.json", controller: animation),
+                ),
+              )
+            : const SafeArea(child: ActivationDashboard()),
+      ),
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/premium/premium_inline.dart b/filcnaplo_premium/lib/ui/mobile/premium/premium_inline.dart
new file mode 100644
index 0000000..35c29fb
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/premium/premium_inline.dart
@@ -0,0 +1,66 @@
+import 'package:filcnaplo_premium/ui/mobile/premium/upsell.dart';
+import 'package:flutter/material.dart';
+
+enum PremiumInlineFeature { nickname, theme, widget, goal, stats }
+
+const Map<PremiumInlineFeature, String> _featureAssets = {
+  PremiumInlineFeature.nickname: "assets/images/premium_nickname_inline_showcase.png",
+  PremiumInlineFeature.theme: "assets/images/premium_theme_inline_showcase.png",
+  PremiumInlineFeature.widget: "assets/images/premium_widget_inline_showcase.png",
+  PremiumInlineFeature.goal: "assets/images/premium_goal_inline_showcase.png",
+  PremiumInlineFeature.stats: "assets/images/premium_stats_inline_showcase.png",
+};
+
+const Map<PremiumInlineFeature, PremiumFeature> _featuresInline = {
+  PremiumInlineFeature.nickname: PremiumFeature.profile,
+  PremiumInlineFeature.theme: PremiumFeature.customcolors,
+  PremiumInlineFeature.widget: PremiumFeature.widget,
+  PremiumInlineFeature.goal: PremiumFeature.goalplanner,
+  PremiumInlineFeature.stats: PremiumFeature.gradestats,
+};
+
+class PremiumInline extends StatelessWidget {
+  const PremiumInline({super.key, required this.features});
+
+  final List<PremiumInlineFeature> features;
+
+  String _getAsset() {
+    for (int i = 0; i < features.length; i++) {
+      if (DateTime.now().day % features.length == i) {
+        return _featureAssets[features[i]]!;
+      }
+    }
+
+    return _featureAssets[features[0]]!;
+  }
+
+  PremiumFeature _getFeature() {
+    for (int i = 0; i < features.length; i++) {
+      if (DateTime.now().day % features.length == i) {
+        return _featuresInline[features[i]]!;
+      }
+    }
+
+    return _featuresInline[features[0]]!;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      children: [
+        Image.asset(_getAsset()),
+        Positioned.fill(
+          child: Material(
+            type: MaterialType.transparency,
+            child: InkWell(
+              borderRadius: BorderRadius.circular(16.0),
+              onTap: () {
+                PremiumLockedFeatureUpsell.show(context: context, feature: _getFeature());
+              },
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/premium/upsell.dart b/filcnaplo_premium/lib/ui/mobile/premium/upsell.dart
new file mode 100644
index 0000000..ac82265
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/premium/upsell.dart
@@ -0,0 +1,164 @@
+import 'package:filcnaplo/icons/filc_icons.dart';
+import 'package:filcnaplo_mobile_ui/premium/premium_screen.dart';
+import 'package:flutter/material.dart';
+
+enum PremiumFeature {
+  gradestats,
+  customcolors,
+  profile,
+  iconpack,
+  subjectrename,
+  weeklytimetable,
+  goalplanner,
+  widget,
+}
+
+enum PremiumFeatureLevel { kupak, tinta }
+
+const Map<PremiumFeature, PremiumFeatureLevel> _featureLevels = {
+  PremiumFeature.gradestats: PremiumFeatureLevel.kupak,
+  PremiumFeature.customcolors: PremiumFeatureLevel.kupak,
+  PremiumFeature.profile: PremiumFeatureLevel.kupak,
+  PremiumFeature.iconpack: PremiumFeatureLevel.kupak,
+  PremiumFeature.subjectrename: PremiumFeatureLevel.kupak,
+  PremiumFeature.weeklytimetable: PremiumFeatureLevel.tinta,
+  PremiumFeature.goalplanner: PremiumFeatureLevel.tinta,
+  PremiumFeature.widget: PremiumFeatureLevel.tinta,
+};
+
+const Map<PremiumFeature, String> _featureAssets = {
+  PremiumFeature.gradestats: "assets/images/premium_stats_showcase.png",
+  PremiumFeature.customcolors: "assets/images/premium_theme_showcase.png",
+  PremiumFeature.profile: "assets/images/premium_nickname_showcase.png",
+  PremiumFeature.weeklytimetable: "assets/images/premium_timetable_showcase.png",
+  PremiumFeature.goalplanner: "assets/images/premium_goal_showcase.png",
+  PremiumFeature.widget: "assets/images/premium_widget_showcase.png",
+};
+
+const Map<PremiumFeature, String> _featureTitles = {
+  PremiumFeature.gradestats: "Találtál egy prémium funkciót.",
+  PremiumFeature.customcolors: "Több személyre szabás kell?",
+  PremiumFeature.profile: "Nem tetszik a neved?",
+  PremiumFeature.iconpack: "Jobban tetszettek a régi ikonok?",
+  PremiumFeature.subjectrename: "Sokáig tart elolvasni, hogy \"Földrajz természettudomány\"?",
+  PremiumFeature.weeklytimetable: "Szeretnéd egyszerre az egész hetet látni?",
+  PremiumFeature.goalplanner: "Kövesd a céljaidat, sok-sok statisztikával.",
+  PremiumFeature.widget: "Órák a kezdőképernyőd kényelméből.",
+};
+
+const Map<PremiumFeature, String> _featureDescriptions = {
+  PremiumFeature.gradestats: "Támogass Kupak szinten, hogy több statisztikát láthass. ",
+  PremiumFeature.customcolors: "Támogass Kupak szinten, és szabd személyre az elemek, a háttér, és a panelek színeit.",
+  PremiumFeature.profile: "Kupak szinten változtathatod a nevedet, sőt, akár a profilképedet is.",
+  PremiumFeature.iconpack: "Támogass Kupak szinten, hogy ikon témát választhass.",
+  PremiumFeature.subjectrename: "Támogass Kupak szinten, hogy átnevezhesd Föcire.",
+  PremiumFeature.weeklytimetable: "Támogass Tinta szinten a heti órarend funkcióért.",
+  PremiumFeature.goalplanner: "A célkövetéshez támogass Tinta szinten.",
+  PremiumFeature.widget: "Támogass Tinta szinten, és helyezz egy widgetet a kezdőképernyődre.",
+};
+
+class PremiumLockedFeatureUpsell extends StatelessWidget {
+  const PremiumLockedFeatureUpsell({super.key, required this.feature});
+
+  static void show({required BuildContext context, required PremiumFeature feature}) =>
+      showDialog(context: context, builder: (context) => PremiumLockedFeatureUpsell(feature: feature));
+
+  final PremiumFeature feature;
+
+  IconData _getIcon() => _featureLevels[feature] == PremiumFeatureLevel.kupak ? FilcIcons.kupak : FilcIcons.tinta;
+  Color _getColor(BuildContext context) => _featureLevels[feature] == PremiumFeatureLevel.kupak
+      ? const Color(0xffC8A708)
+      : Theme.of(context).brightness == Brightness.light
+          ? const Color(0xff691A9B)
+          : const Color(0xffA66FC8);
+  String? _getAsset() => _featureAssets[feature];
+  String _getTitle() => _featureTitles[feature]!;
+  String _getDescription() => _featureDescriptions[feature]!;
+
+  @override
+  Widget build(BuildContext context) {
+    final Color color = _getColor(context);
+
+    return Dialog(
+      child: Padding(
+        padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 16.0),
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            // Title Bar
+            Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                Padding(
+                  padding: const EdgeInsets.only(left: 8.0),
+                  child: Icon(_getIcon()),
+                ),
+                IconButton(
+                  onPressed: () => Navigator.of(context).pop(),
+                  icon: const Icon(Icons.close),
+                ),
+              ],
+            ),
+
+            // Image showcase
+            if (_getAsset() != null)
+              Padding(
+                padding: const EdgeInsets.only(top: 8.0),
+                child: Image.asset(_getAsset()!),
+              ),
+
+            // Dialog title
+            Padding(
+              padding: const EdgeInsets.only(top: 12.0),
+              child: Text(
+                _getTitle(),
+                style: const TextStyle(
+                  fontWeight: FontWeight.bold,
+                  fontSize: 20.0,
+                ),
+              ),
+            ),
+
+            // Dialog description
+            Padding(
+              padding: const EdgeInsets.only(top: 8.0),
+              child: Text(
+                _getDescription(),
+                style: const TextStyle(
+                  fontSize: 16.0,
+                ),
+              ),
+            ),
+
+            // CTA button
+            Padding(
+              padding: const EdgeInsets.only(top: 8.0),
+              child: SizedBox(
+                width: double.infinity,
+                child: TextButton(
+                  style: ButtonStyle(
+                      backgroundColor: MaterialStatePropertyAll(color.withOpacity(.25)),
+                      foregroundColor: MaterialStatePropertyAll(color),
+                      overlayColor: MaterialStatePropertyAll(color.withOpacity(.1))),
+                  onPressed: () {
+                    Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(builder: (context) {
+                      return const PremiumScreen();
+                    }));
+                  },
+                  child: const Text(
+                    "Vigyél oda!",
+                    style: TextStyle(
+                      fontWeight: FontWeight.bold,
+                      fontSize: 18.0,
+                    ),
+                  ),
+                ),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/settings/icon_pack.dart b/filcnaplo_premium/lib/ui/mobile/settings/icon_pack.dart
new file mode 100644
index 0000000..8cd16bb
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/settings/icon_pack.dart
@@ -0,0 +1,34 @@
+import 'package:filcnaplo/models/settings.dart';
+import 'package:filcnaplo_mobile_ui/common/panel/panel_button.dart';
+import 'package:filcnaplo_mobile_ui/screens/settings/settings_helper.dart';
+import 'package:filcnaplo_premium/models/premium_scopes.dart';
+import 'package:filcnaplo_premium/providers/premium_provider.dart';
+import 'package:filcnaplo_premium/ui/mobile/premium/upsell.dart';
+import 'package:flutter/material.dart';
+import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.i18n.dart';
+import 'package:flutter_feather_icons/flutter_feather_icons.dart';
+import 'package:provider/provider.dart';
+import 'package:filcnaplo/utils/format.dart';
+
+class PremiumIconPackSelector extends StatelessWidget {
+  const PremiumIconPackSelector({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final settings = Provider.of<SettingsProvider>(context);
+
+    return PanelButton(
+      onPressed: () {
+        if (!Provider.of<PremiumProvider>(context, listen: false).hasScope(PremiumScopes.customIcons)) {
+          PremiumLockedFeatureUpsell.show(context: context, feature: PremiumFeature.iconpack);
+          return;
+        }
+
+        SettingsHelper.iconPack(context);
+      },
+      title: Text("icon_pack".i18n),
+      leading: const Icon(FeatherIcons.grid),
+      trailing: Text(settings.iconPack.name.capital()),
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/settings/modify_subject_names.dart b/filcnaplo_premium/lib/ui/mobile/settings/modify_subject_names.dart
new file mode 100644
index 0000000..dbad6fc
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/settings/modify_subject_names.dart
@@ -0,0 +1,383 @@
+import 'package:dropdown_button2/dropdown_button2.dart';
+import 'package:filcnaplo/api/providers/database_provider.dart';
+import 'package:filcnaplo/api/providers/user_provider.dart';
+import 'package:filcnaplo/helpers/subject.dart';
+import 'package:filcnaplo/models/settings.dart';
+import 'package:filcnaplo/theme/colors/colors.dart';
+import 'package:filcnaplo/utils/format.dart';
+import 'package:filcnaplo_kreta_api/models/subject.dart';
+import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
+import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
+import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
+import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
+import 'package:filcnaplo_mobile_ui/common/panel/panel_button.dart';
+import 'package:filcnaplo_premium/models/premium_scopes.dart';
+import 'package:filcnaplo_premium/providers/premium_provider.dart';
+import 'package:filcnaplo_premium/ui/mobile/premium/upsell.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_feather_icons/flutter_feather_icons.dart';
+import 'package:provider/provider.dart';
+
+import 'modify_subject_names.i18n.dart';
+
+class MenuRenamedSubjects extends StatelessWidget {
+  const MenuRenamedSubjects({Key? key, required this.settings}) : super(key: key);
+
+  final SettingsProvider settings;
+
+  @override
+  Widget build(BuildContext context) {
+    return PanelButton(
+      padding: const EdgeInsets.only(left: 14.0),
+      onPressed: () {
+        if (!Provider.of<PremiumProvider>(context, listen: false).hasScope(PremiumScopes.renameSubjects)) {
+          PremiumLockedFeatureUpsell.show(context: context, feature: PremiumFeature.subjectrename);
+          return;
+        }
+
+        Navigator.of(context, rootNavigator: true).push(
+          CupertinoPageRoute(builder: (context) => const ModifySubjectNames()),
+        );
+      },
+      title: Text(
+        "rename_subjects".i18n,
+        style: TextStyle(color: AppColors.of(context).text.withOpacity(settings.renamedSubjectsEnabled ? 1.0 : .5)),
+      ),
+      leading: settings.renamedSubjectsEnabled
+          ? const Icon(FeatherIcons.penTool)
+          : Icon(FeatherIcons.penTool, color: AppColors.of(context).text.withOpacity(.25)),
+      trailingDivider: true,
+      trailing: Switch(
+        onChanged: (v) async {
+          if (!Provider.of<PremiumProvider>(context, listen: false).hasScope(PremiumScopes.renameSubjects)) {
+            PremiumLockedFeatureUpsell.show(context: context, feature: PremiumFeature.subjectrename);
+            return;
+          }
+
+          settings.update(renamedSubjectsEnabled: v);
+          await Provider.of<GradeProvider>(context, listen: false).convertBySettings();
+          await Provider.of<TimetableProvider>(context, listen: false).convertBySettings();
+          await Provider.of<AbsenceProvider>(context, listen: false).convertBySettings();
+        },
+        value: settings.renamedSubjectsEnabled,
+        activeColor: Theme.of(context).colorScheme.secondary,
+      ),
+    );
+  }
+}
+
+class ModifySubjectNames extends StatefulWidget {
+  const ModifySubjectNames({Key? key}) : super(key: key);
+
+  @override
+  State<ModifySubjectNames> createState() => _ModifySubjectNamesState();
+}
+
+class _ModifySubjectNamesState extends State<ModifySubjectNames> {
+  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
+  final _subjectName = TextEditingController();
+  String? selectedSubjectId;
+
+  late List<Subject> subjects;
+  late UserProvider user;
+  late DatabaseProvider dbProvider;
+
+  @override
+  void initState() {
+    super.initState();
+    subjects = Provider.of<GradeProvider>(context, listen: false).grades.map((e) => e.subject).toSet().toList()
+      ..sort((a, b) => a.name.compareTo(b.name));
+    user = Provider.of<UserProvider>(context, listen: false);
+    dbProvider = Provider.of<DatabaseProvider>(context, listen: false);
+  }
+
+  Future<Map<String, String>> fetchRenamedSubjects() async {
+    return await dbProvider.userQuery.renamedSubjects(userId: user.id!);
+  }
+
+  void showRenameDialog() {
+    showDialog(
+      context: context,
+      builder: (context) => StatefulBuilder(builder: (context, setS) {
+        return AlertDialog(
+          shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(14.0))),
+          title: Text("rename_subject".i18n),
+          content: Column(
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              DropdownButton2(
+                items: subjects
+                    .map((item) => DropdownMenuItem<String>(
+                          value: item.id,
+                          child: Text(
+                            item.name,
+                            style: TextStyle(
+                              fontSize: 14,
+                              fontWeight: FontWeight.bold,
+                              color: AppColors.of(context).text,
+                            ),
+                            overflow: TextOverflow.ellipsis,
+                          ),
+                        ))
+                    .toList(),
+                onChanged: (String? v) async {
+                  final renamedSubs = await fetchRenamedSubjects();
+
+                  setS(() {
+                    selectedSubjectId = v;
+
+                    if (renamedSubs.containsKey(selectedSubjectId)) {
+                      _subjectName.text = renamedSubs[selectedSubjectId]!;
+                    } else {
+                      _subjectName.text = "";
+                    }
+                  });
+                },
+                iconSize: 14,
+                iconEnabledColor: AppColors.of(context).text,
+                iconDisabledColor: AppColors.of(context).text,
+                underline: const SizedBox(),
+                itemHeight: 40,
+                itemPadding: const EdgeInsets.only(left: 14, right: 14),
+                buttonWidth: 50,
+                dropdownWidth: 300,
+                dropdownPadding: null,
+                buttonDecoration: BoxDecoration(
+                  borderRadius: BorderRadius.circular(8),
+                ),
+                dropdownDecoration: BoxDecoration(
+                  borderRadius: BorderRadius.circular(14),
+                ),
+                dropdownElevation: 8,
+                scrollbarRadius: const Radius.circular(40),
+                scrollbarThickness: 6,
+                scrollbarAlwaysShow: true,
+                offset: const Offset(-10, -10),
+                buttonSplashColor: Colors.transparent,
+                customButton: Container(
+                  width: double.infinity,
+                  decoration: BoxDecoration(
+                    border: Border.all(color: Colors.grey, width: 2),
+                    borderRadius: BorderRadius.circular(12.0),
+                  ),
+                  padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0),
+                  child: Text(
+                    selectedSubjectId == null ? "select_subject".i18n : subjects.firstWhere((element) => element.id == selectedSubjectId).name,
+                    style: Theme.of(context)
+                        .textTheme
+                        .titleSmall!
+                        .copyWith(fontWeight: FontWeight.w700, color: AppColors.of(context).text.withOpacity(0.75)),
+                    overflow: TextOverflow.ellipsis,
+                    maxLines: 2,
+                    textAlign: TextAlign.center,
+                  ),
+                ),
+              ),
+              const Padding(
+                padding: EdgeInsets.symmetric(vertical: 8.0),
+                child: Icon(FeatherIcons.arrowDown, size: 32),
+              ),
+              TextField(
+                controller: _subjectName,
+                decoration: InputDecoration(
+                  border: OutlineInputBorder(
+                    borderSide: const BorderSide(color: Colors.grey, width: 1.5),
+                    borderRadius: BorderRadius.circular(12.0),
+                  ),
+                  focusedBorder: OutlineInputBorder(
+                    borderSide: const BorderSide(color: Colors.grey, width: 1.5),
+                    borderRadius: BorderRadius.circular(12.0),
+                  ),
+                  contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
+                  hintText: "modified_name".i18n,
+                  suffixIcon: IconButton(
+                    icon: const Icon(
+                      FeatherIcons.x,
+                      color: Colors.grey,
+                    ),
+                    onPressed: () {
+                      setState(() {
+                        _subjectName.text = "";
+                      });
+                    },
+                  ),
+                ),
+              ),
+            ],
+          ),
+          actions: [
+            TextButton(
+              child: Text(
+                "cancel".i18n,
+                style: const TextStyle(fontWeight: FontWeight.w500),
+              ),
+              onPressed: () {
+                Navigator.of(context).maybePop();
+              },
+            ),
+            TextButton(
+              child: Text(
+                "done".i18n,
+                style: const TextStyle(fontWeight: FontWeight.w500),
+              ),
+              onPressed: () async {
+                if (selectedSubjectId != null) {
+                  final renamedSubs = await fetchRenamedSubjects();
+
+                  renamedSubs[selectedSubjectId!] = _subjectName.text;
+                  await dbProvider.userStore.storeRenamedSubjects(renamedSubs, userId: user.id!);
+                  await Provider.of<GradeProvider>(context, listen: false).convertBySettings();
+                  await Provider.of<TimetableProvider>(context, listen: false).convertBySettings();
+                  await Provider.of<AbsenceProvider>(context, listen: false).convertBySettings();
+                }
+                Navigator.of(context).pop(true);
+                setState(() {});
+              },
+            ),
+          ],
+        );
+      }),
+    ).then((val) {
+      _subjectName.text = "";
+      selectedSubjectId = null;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+        key: _scaffoldKey,
+        appBar: AppBar(
+          surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
+          leading: BackButton(color: AppColors.of(context).text),
+          title: Text(
+            "modify_subjects".i18n,
+            style: TextStyle(color: AppColors.of(context).text),
+          ),
+        ),
+        body: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
+          child: SingleChildScrollView(
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                InkWell(
+                  onTap: showRenameDialog,
+                  borderRadius: BorderRadius.circular(12.0),
+                  child: Container(
+                    width: double.infinity,
+                    decoration: BoxDecoration(
+                      border: Border.all(color: Colors.grey, width: 2),
+                      borderRadius: BorderRadius.circular(12.0),
+                    ),
+                    padding: const EdgeInsets.symmetric(vertical: 18.0, horizontal: 12.0),
+                    child: Center(
+                      child: Text(
+                        "rename_new_subject".i18n,
+                        style: TextStyle(
+                          fontWeight: FontWeight.w600,
+                          fontSize: 18,
+                          color: AppColors.of(context).text.withOpacity(.85),
+                        ),
+                      ),
+                    ),
+                  ),
+                ),
+                const SizedBox(
+                  height: 30,
+                ),
+                FutureBuilder<Map<String, String>>(
+                  future: fetchRenamedSubjects(),
+                  builder: (context, snapshot) {
+                    if (!snapshot.hasData || snapshot.data!.isEmpty) return Container();
+
+                    return Panel(
+                      title: Text("renamed_subjects".i18n),
+                      child: Column(
+                        children: snapshot.data!.keys.map(
+                          (key) {
+                            Subject? subject = subjects.firstWhere((element) => key == element.id);
+                            String renameTo = snapshot.data![key]!;
+                            return RenamedSubjectItem(
+                              subject: subject,
+                              renamedTo: renameTo,
+                              modifyCallback: () {
+                                setState(() {
+                                  selectedSubjectId = subject.id;
+                                  _subjectName.text = renameTo;
+                                });
+                                showRenameDialog();
+                              },
+                              removeCallback: () {
+                                setState(() {
+                                  Map<String, String> subs = Map.from(snapshot.data!);
+                                  subs.remove(key);
+                                  dbProvider.userStore.storeRenamedSubjects(subs, userId: user.id!);
+                                });
+                              },
+                            );
+                          },
+                        ).toList(),
+                      ),
+                    );
+                  },
+                ),
+              ],
+            ),
+          ),
+        ));
+  }
+}
+
+class RenamedSubjectItem extends StatelessWidget {
+  const RenamedSubjectItem({
+    Key? key,
+    required this.subject,
+    required this.renamedTo,
+    required this.modifyCallback,
+    required this.removeCallback,
+  }) : super(key: key);
+
+  final Subject subject;
+  final String renamedTo;
+  final void Function() modifyCallback;
+  final void Function() removeCallback;
+
+  @override
+  Widget build(BuildContext context) {
+    return ListTile(
+      minLeadingWidth: 32.0,
+      dense: true,
+      contentPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0),
+      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
+      visualDensity: VisualDensity.compact,
+      onTap: () {},
+      leading: Icon(SubjectIcon.resolveVariant(subject: subject, context: context), color: AppColors.of(context).text.withOpacity(.75)),
+      title: InkWell(
+        onTap: modifyCallback,
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Text(
+              subject.name.capital(),
+              style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14, color: AppColors.of(context).text.withOpacity(.75)),
+              maxLines: 1,
+              overflow: TextOverflow.ellipsis,
+            ),
+            Text(
+              renamedTo,
+              style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 16),
+              maxLines: 2,
+              overflow: TextOverflow.ellipsis,
+            ),
+          ],
+        ),
+      ),
+      trailing: InkWell(
+        onTap: removeCallback,
+        child: Icon(FeatherIcons.trash, color: AppColors.of(context).red.withOpacity(.75)),
+      ),
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/settings/modify_subject_names.i18n.dart b/filcnaplo_premium/lib/ui/mobile/settings/modify_subject_names.i18n.dart
new file mode 100644
index 0000000..7bfff73
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/settings/modify_subject_names.i18n.dart
@@ -0,0 +1,45 @@
+import 'package:i18n_extension/i18n_extension.dart';
+
+extension SettingsLocalization on String {
+  static final _t = Translations.byLocale("hu_hu") +
+      {
+        "en_en": {
+          "renamed_subjects": "Renamed Subjects",
+          "rename_subjects": "Rename Subjects",
+          "rename_subject": "Rename Subject",
+          "select_subject": "Select Subject",
+          "modified_name": "Modified Name",
+          "modify_subjects": "Modify Subjects",
+          "cancel": "Cancel",
+          "done": "Done",
+          "rename_new_subject": "Rename New Subject",
+        },
+        "hu_hu": {
+          "renamed_subjects": "Átnevezett Tantárgyaid",
+          "rename_subjects": "Tantárgyak átnevezése",
+          "rename_subject": "Tantárgy átnevezése",
+          "select_subject": "Válassz tantárgyat",
+          "modified_name": "Módosított név",
+          "modify_subjects": "Tantárgyak átnevezése",
+          "cancel": "Mégse",
+          "done": "Kész",
+          "rename_new_subject": "Új Tantárgy átnevezése",
+        },
+        "de_de": {
+          "renamed_subjects": "Umbenannte Fächer",
+          "rename_subjects": "Fächer umbenennen",
+          "rename_subject": "Fach umbenennen",
+          "select_subject": "Fach auswählen",
+          "modified_name": "Geänderter Name",
+          "modify_subjects": "Fächer ändern",
+          "cancel": "Abbrechen",
+          "done": "Erledigt",
+          "rename_new_subject": "Neues Fach umbenennen",
+        },
+      };
+
+  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);
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/settings/nickname.dart b/filcnaplo_premium/lib/ui/mobile/settings/nickname.dart
new file mode 100644
index 0000000..6c057dc
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/settings/nickname.dart
@@ -0,0 +1,93 @@
+import 'package:filcnaplo/api/providers/database_provider.dart';
+import 'package:filcnaplo/api/providers/user_provider.dart';
+import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu_item.dart';
+import 'package:filcnaplo_premium/models/premium_scopes.dart';
+import 'package:filcnaplo_premium/providers/premium_provider.dart';
+import 'package:filcnaplo_premium/ui/mobile/premium/upsell.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_feather_icons/flutter_feather_icons.dart';
+import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.i18n.dart';
+import 'package:provider/provider.dart';
+
+class UserMenuNickname extends StatelessWidget {
+  const UserMenuNickname({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BottomSheetMenuItem(
+      onPressed: () {
+        if (!Provider.of<PremiumProvider>(context, listen: false).hasScope(PremiumScopes.nickname)) {
+          PremiumLockedFeatureUpsell.show(context: context, feature: PremiumFeature.profile);
+          return;
+        }
+        showDialog(context: context, builder: (context) => const UserNicknameEditor());
+      },
+      icon: const Icon(FeatherIcons.edit2),
+      title: Text("edit_nickname".i18n),
+    );
+  }
+}
+
+class UserNicknameEditor extends StatefulWidget {
+  const UserNicknameEditor({Key? key}) : super(key: key);
+
+  @override
+  State<UserNicknameEditor> createState() => _UserNicknameEditorState();
+}
+
+class _UserNicknameEditorState extends State<UserNicknameEditor> {
+  final _userName = TextEditingController();
+  late final UserProvider user;
+
+  @override
+  void initState() {
+    super.initState();
+    user = Provider.of<UserProvider>(context, listen: false);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      title: Text("change_username".i18n),
+      content: TextField(
+        controller: _userName,
+        autofocus: true,
+        decoration: InputDecoration(
+          border: const OutlineInputBorder(),
+          label: Text(user.name!),
+          suffixIcon: IconButton(
+            icon: const Icon(FeatherIcons.x),
+            onPressed: () {
+              setState(() {
+                _userName.text = "";
+              });
+            },
+          ),
+        ),
+      ),
+      actions: [
+        TextButton(
+          child: Text(
+            "cancel".i18n,
+            style: const TextStyle(fontWeight: FontWeight.w500),
+          ),
+          onPressed: () {
+            Navigator.of(context).maybePop();
+          },
+        ),
+        TextButton(
+          child: Text(
+            "done".i18n,
+            style: const TextStyle(fontWeight: FontWeight.w500),
+          ),
+          onPressed: () {
+            user.user!.nickname = _userName.text.trim();
+            Provider.of<DatabaseProvider>(context, listen: false).store.storeUser(user.user!);
+            Provider.of<UserProvider>(context, listen: false).refresh();
+            Navigator.of(context).pop(true);
+          },
+        ),
+      ],
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/settings/profile_pic.dart b/filcnaplo_premium/lib/ui/mobile/settings/profile_pic.dart
new file mode 100644
index 0000000..e857df8
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/settings/profile_pic.dart
@@ -0,0 +1,208 @@
+import 'dart:convert';
+import 'dart:developer';
+import 'dart:io';
+
+import 'package:filcnaplo/api/providers/database_provider.dart';
+import 'package:filcnaplo/api/providers/user_provider.dart';
+import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu_item.dart';
+import 'package:filcnaplo_premium/models/premium_scopes.dart';
+import 'package:filcnaplo_premium/providers/premium_provider.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_feather_icons/flutter_feather_icons.dart';
+import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.i18n.dart';
+import 'package:provider/provider.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:image_crop/image_crop.dart';
+
+class UserMenuProfilePic extends StatelessWidget {
+  const UserMenuProfilePic({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    if (!Provider.of<PremiumProvider>(context).hasScope(PremiumScopes.nickname)) {
+      return const SizedBox();
+    }
+
+    return BottomSheetMenuItem(
+      onPressed: () {
+        showDialog(context: context, builder: (context) => const UserProfilePicEditor());
+      },
+      icon: const Icon(FeatherIcons.camera),
+      title: Text("edit_profile_picture".i18n),
+    );
+  }
+}
+
+class UserProfilePicEditor extends StatefulWidget {
+  const UserProfilePicEditor({Key? key}) : super(key: key);
+
+  @override
+  State<UserProfilePicEditor> createState() => _UserProfilePicEditorState();
+}
+
+class _UserProfilePicEditorState extends State<UserProfilePicEditor> {
+  late final UserProvider user;
+
+  final cropKey = GlobalKey<CropState>();
+  File? _file;
+  File? _sample;
+  File? _lastCropped;
+
+  File? image;
+  Future pickImage() async {
+    try {
+      final image = await ImagePicker().pickImage(source: ImageSource.gallery);
+      if (image == null) return;
+      File imageFile = File(image.path);
+
+      final sample = await ImageCrop.sampleImage(
+        file: imageFile,
+        preferredSize: context.size!.longestSide.ceil(),
+      );
+
+      _sample?.delete();
+      _file?.delete();
+
+      setState(() {
+        _sample = sample;
+        _file = imageFile;
+      });
+    } on PlatformException catch (e) {
+      log('Failed to pick image: $e');
+    }
+  }
+
+  Widget cropImageWidget() {
+    return SizedBox(
+        height: 300,
+        child: Crop.file(
+          _sample!,
+          key: cropKey,
+          aspectRatio: 1.0,
+        ));
+  }
+
+  Widget openImageWidget() {
+    return InkWell(
+      customBorder: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(14.0),
+      ),
+      onTap: () => pickImage(),
+      child: Container(
+        decoration: BoxDecoration(border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(14.0)),
+        width: double.infinity,
+        padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 8.0),
+        child: Column(
+          children: [
+            Text(
+              "click_here".i18n,
+              style: const TextStyle(fontSize: 22.0, fontWeight: FontWeight.w600),
+            ),
+            Text(
+              "select_profile_picture".i18n,
+              style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500),
+            )
+          ],
+        ),
+      ),
+    );
+  }
+
+  Future<void> _cropImage() async {
+    final scale = cropKey.currentState!.scale;
+    final area = cropKey.currentState!.area;
+    if (area == null || _file == null) {
+      return;
+    }
+
+    final sample = await ImageCrop.sampleImage(
+      file: _file!,
+      preferredSize: (2000 / scale).round(),
+    );
+
+    final file = await ImageCrop.cropImage(
+      file: sample,
+      area: area,
+    );
+
+    sample.delete();
+
+    _lastCropped?.delete();
+    _lastCropped = file;
+
+    List<int> imageBytes = await _lastCropped!.readAsBytes();
+    String base64Image = base64Encode(imageBytes);
+    user.user!.picture = base64Image;
+    Provider.of<DatabaseProvider>(context, listen: false).store.storeUser(user.user!);
+    Provider.of<UserProvider>(context, listen: false).refresh();
+
+    debugPrint('$file');
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    user = Provider.of<UserProvider>(context, listen: false);
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _file?.delete();
+    _sample?.delete();
+    _lastCropped?.delete();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(14.0))),
+      contentPadding: const EdgeInsets.only(top: 10.0),
+      title: Text("edit_profile_picture".i18n),
+      content: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          Padding(
+            padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
+            child: _sample == null ? openImageWidget() : cropImageWidget(),
+          ),
+          if (user.user!.picture != "")
+            TextButton(
+              child: Text(
+                "remove_profile_picture".i18n,
+                style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.red),
+              ),
+              onPressed: () {
+                user.user!.picture = "";
+                Provider.of<DatabaseProvider>(context, listen: false).store.storeUser(user.user!);
+                Provider.of<UserProvider>(context, listen: false).refresh();
+                Navigator.of(context).pop(true);
+              },
+            ),
+        ],
+      ),
+      actions: [
+        TextButton(
+          child: Text(
+            "cancel".i18n,
+            style: const TextStyle(fontWeight: FontWeight.w500),
+          ),
+          onPressed: () {
+            Navigator.of(context).maybePop();
+          },
+        ),
+        TextButton(
+          child: Text(
+            "done".i18n,
+            style: const TextStyle(fontWeight: FontWeight.w500),
+          ),
+          onPressed: () async {
+            await _cropImage();
+            Navigator.of(context).pop(true);
+          },
+        ),
+      ],
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/settings/theme.dart b/filcnaplo_premium/lib/ui/mobile/settings/theme.dart
new file mode 100644
index 0000000..18a1c44
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/settings/theme.dart
@@ -0,0 +1,657 @@
+import 'package:filcnaplo/models/settings.dart';
+import 'package:filcnaplo/theme/colors/accent.dart';
+import 'package:filcnaplo/theme/colors/colors.dart';
+import 'package:filcnaplo/theme/observer.dart';
+import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
+import 'package:filcnaplo/ui/widgets/message/message_tile.dart';
+import 'package:filcnaplo_kreta_api/models/grade.dart';
+import 'package:filcnaplo_kreta_api/models/homework.dart';
+import 'package:filcnaplo_kreta_api/models/message.dart';
+import 'package:filcnaplo_mobile_ui/common/filter_bar.dart';
+import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
+import 'package:filcnaplo_mobile_ui/common/widgets/grade/new_grades.dart';
+import 'package:filcnaplo_mobile_ui/common/widgets/homework/homework_tile.dart';
+import 'package:filcnaplo_premium/models/premium_scopes.dart';
+import 'package:filcnaplo_premium/providers/premium_provider.dart';
+import 'package:filcnaplo_premium/ui/mobile/flutter_colorpicker/colorpicker.dart';
+import 'package:filcnaplo_premium/ui/mobile/premium/upsell.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:provider/provider.dart';
+import 'theme.i18n.dart';
+
+class PremiumCustomAccentColorSetting extends StatefulWidget {
+  const PremiumCustomAccentColorSetting({Key? key}) : super(key: key);
+
+  @override
+  State<PremiumCustomAccentColorSetting> createState() => _PremiumCustomAccentColorSettingState();
+}
+
+enum CustomColorMode { theme, accent, background, highlight }
+
+class _PremiumCustomAccentColorSettingState extends State<PremiumCustomAccentColorSetting> with TickerProviderStateMixin {
+  late final SettingsProvider settings;
+  bool colorSelection = false;
+  bool customColorMenu = false;
+  CustomColorMode colorMode = CustomColorMode.theme;
+  final customColorInput = TextEditingController();
+  final unknownColor = Colors.black;
+
+  late TabController _testTabController;
+  late TabController _colorsTabController;
+  late AnimationController _openAnimController;
+
+  late final Animation<double> backgroundAnimation = Tween<double>(begin: 0, end: 1).animate(
+    CurvedAnimation(
+      parent: _openAnimController,
+      curve: const Interval(0.2, 1.0, curve: Curves.easeInOut),
+    ),
+  );
+
+  late final Animation<double> fullPageAnimation = Tween<double>(begin: 0, end: 1).animate(
+    CurvedAnimation(
+      parent: _openAnimController,
+      curve: const Interval(0.0, 0.6, curve: Curves.easeInOut),
+    ),
+  );
+
+  late final Animation<double> backContainerAnimation = Tween<double>(begin: 100, end: 0).animate(
+    CurvedAnimation(
+      parent: _openAnimController,
+      curve: const Interval(0.0, 0.9, curve: Curves.easeInOut),
+    ),
+  );
+
+  late final Animation<double> backContentAnimation = Tween<double>(begin: 100, end: 0).animate(
+    CurvedAnimation(
+      parent: _openAnimController,
+      curve: const Interval(0.2, 1.0, curve: Curves.easeInOut),
+    ),
+  );
+
+  late final Animation<double> backContentScaleAnimation = Tween<double>(begin: 0.8, end: 0.9).animate(
+    CurvedAnimation(
+      parent: _openAnimController,
+      curve: const Interval(0.45, 1.0, curve: Curves.easeInOut),
+    ),
+  );
+
+  late final Animation<double> pickerContainerAnimation = Tween<double>(begin: 0, end: 1).animate(
+    CurvedAnimation(
+      parent: _openAnimController,
+      curve: const Interval(0.25, 0.8, curve: Curves.easeInOut),
+    ),
+  );
+
+  @override
+  void initState() {
+    super.initState();
+    _colorsTabController = TabController(length: 4, vsync: this);
+    _testTabController = TabController(length: 4, vsync: this);
+    settings = Provider.of<SettingsProvider>(context, listen: false);
+
+    _openAnimController = AnimationController(vsync: this, duration: const Duration(milliseconds: 750));
+    _openAnimController.forward();
+  }
+
+  @override
+  void dispose() {
+    _openAnimController.dispose();
+    super.dispose();
+  }
+
+  void setTheme(ThemeMode mode, bool store) async {
+    await settings.update(theme: mode, store: store);
+    Provider.of<ThemeModeObserver>(context, listen: false).changeTheme(mode, updateNavbarColor: false);
+  }
+
+  Color? getCustomColor() {
+    switch (colorMode) {
+      case CustomColorMode.theme:
+        return accentColorMap[settings.accentColor];
+      case CustomColorMode.background:
+        return settings.customBackgroundColor;
+      case CustomColorMode.highlight:
+        return settings.customHighlightColor;
+      case CustomColorMode.accent:
+        return settings.customAccentColor;
+    }
+  }
+
+  void updateCustomColor(Color v, bool store) {
+    if (colorMode != CustomColorMode.theme) settings.update(accentColor: AccentColor.custom, store: store);
+    switch (colorMode) {
+      case CustomColorMode.theme:
+        settings.update(
+            accentColor: accentColorMap.keys.firstWhere((element) => accentColorMap[element] == v, orElse: () => AccentColor.filc), store: store);
+        settings.update(customBackgroundColor: AppColors.of(context).background, store: store);
+        settings.update(customHighlightColor: AppColors.of(context).highlight, store: store);
+        settings.update(customAccentColor: v, store: store);
+        break;
+      case CustomColorMode.background:
+        settings.update(customBackgroundColor: v, store: store);
+        break;
+      case CustomColorMode.highlight:
+        settings.update(customHighlightColor: v, store: store);
+        break;
+      case CustomColorMode.accent:
+        settings.update(customAccentColor: v, store: store);
+        break;
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    bool hasAccess = Provider.of<PremiumProvider>(context).hasScope(PremiumScopes.customColors);
+    bool isBackgroundDifferent = Theme.of(context).colorScheme.background != AppColors.of(context).background;
+
+    ThemeMode currentTheme = Theme.of(context).brightness == Brightness.light ? ThemeMode.light : ThemeMode.dark;
+
+    return WillPopScope(
+      onWillPop: () async {
+        Provider.of<ThemeModeObserver>(context, listen: false).changeTheme(settings.theme, updateNavbarColor: true);
+        return true;
+      },
+      child: AnimatedBuilder(
+        animation: _openAnimController,
+        builder: (context, child) {
+          final backgroundGradientBottomColor = isBackgroundDifferent
+              ? Theme.of(context).colorScheme.background
+              : HSVColor.fromColor(Theme.of(context).colorScheme.background)
+                  .withValue(currentTheme == ThemeMode.dark ? 0.1 * _openAnimController.value : 1.0 - (0.1 * _openAnimController.value))
+                  .withAlpha(1.0)
+                  .toColor();
+
+          SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
+            systemNavigationBarColor: backgroundGradientBottomColor,
+          ));
+
+          return Container(
+            decoration: BoxDecoration(
+              gradient: LinearGradient(
+                begin: Alignment.topCenter,
+                end: Alignment.bottomCenter,
+                stops: const [0.0, 0.75],
+                colors: isBackgroundDifferent
+                    ? [
+                        Theme.of(context)
+                            .colorScheme
+                            .background
+                            .withOpacity(1 - ((currentTheme == ThemeMode.dark ? 0.65 : 0.45) * backgroundAnimation.value)),
+                        backgroundGradientBottomColor,
+                      ]
+                    : [backgroundGradientBottomColor, backgroundGradientBottomColor],
+              ),
+            ),
+            child: Opacity(
+              opacity: fullPageAnimation.value,
+              child: Scaffold(
+                backgroundColor: Colors.transparent,
+                appBar: AppBar(
+                  surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
+                  leading: BackButton(color: AppColors.of(context).text),
+                  title: Text(
+                    "Preview",
+                    style: TextStyle(color: AppColors.of(context).text),
+                  ),
+                  backgroundColor: Colors.transparent,
+                  elevation: 0,
+                ),
+                body: Stack(
+                  children: [
+                    Opacity(
+                      opacity: 1 - backContainerAnimation.value * (1 / 100),
+                      child: Transform.translate(
+                        offset: Offset(0, backContainerAnimation.value),
+                        child: Container(
+                          height: double.infinity,
+                          width: double.infinity,
+                          decoration: BoxDecoration(
+                            borderRadius: BorderRadius.circular(24),
+                            gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: const [
+                              0.35,
+                              0.75
+                            ], colors: [
+                              Theme.of(context).colorScheme.background,
+                              isBackgroundDifferent
+                                  ? HSVColor.fromColor(Theme.of(context).colorScheme.background)
+                                      .withSaturation(
+                                          (HSVColor.fromColor(Theme.of(context).colorScheme.background).saturation - 0.15).clamp(0.0, 1.0))
+                                      .toColor()
+                                  : backgroundGradientBottomColor,
+                            ]),
+                          ),
+                          margin: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
+                        ),
+                      ),
+                    ),
+                    Padding(
+                      padding: const EdgeInsets.only(top: 8.0),
+                      child: SizedBox(
+                        width: double.infinity,
+                        child: Padding(
+                          padding: const EdgeInsets.only(top: 24.0),
+                          child: Opacity(
+                            opacity: 1 - backContentAnimation.value * (1 / 100),
+                            child: SingleChildScrollView(
+                              physics: const BouncingScrollPhysics(),
+                              child: Transform.translate(
+                                offset: Offset(0, -24 + backContentAnimation.value),
+                                child: Transform.scale(
+                                  scale: backContentScaleAnimation.value,
+                                  child: Column(
+                                    children: [
+                                      Padding(
+                                        padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 6.0),
+                                        child: FilterBar(
+                                          items: const [
+                                            Tab(text: "All"),
+                                            Tab(text: "Grades"),
+                                            Tab(text: "Messages"),
+                                            Tab(text: "Absences"),
+                                          ],
+                                          controller: _testTabController,
+                                          padding: EdgeInsets.zero,
+                                          censored: true,
+                                        ),
+                                      ),
+                                      Padding(
+                                        padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 8.0),
+                                        child: NewGradesSurprise(
+                                          [
+                                            Grade.fromJson(
+                                              {
+                                                "Uid": "0,Ertekeles",
+                                                "RogzitesDatuma": "2022-01-01T23:00:00Z",
+                                                "KeszitesDatuma": "2022-01-01T23:00:00Z",
+                                                "LattamozasDatuma": null,
+                                                "Tantargy": {
+                                                  "Uid": "0",
+                                                  "Nev": "Filc szakirodalom",
+                                                  "Kategoria": {"Uid": "0,_", "Nev": "_", "Leiras": "Nem mondom meg"},
+                                                  "SortIndex": 2
+                                                },
+                                                "Tema": "Kupak csomag vásárlás vizsga",
+                                                "Tipus": {
+                                                  "Uid": "0,_",
+                                                  "Nev": "_",
+                                                  "Leiras": "Évközi jegy/értékelés",
+                                                },
+                                                "Mod": {
+                                                  "Uid": "0,_",
+                                                  "Nev": "_",
+                                                  "Leiras": "_ feladat",
+                                                },
+                                                "ErtekFajta": {
+                                                  "Uid": "1,Osztalyzat",
+                                                  "Nev": "Osztalyzat",
+                                                  "Leiras": "Elégtelen (1) és Jeles (5) között az öt alapértelmezett érték"
+                                                },
+                                                "ErtekeloTanarNeve": "Premium",
+                                                "Jelleg": "Ertekeles",
+                                                "SzamErtek": 5,
+                                                "SzovegesErtek": "Jeles(5)",
+                                                "SulySzazalekErteke": 100,
+                                                "SzovegesErtekelesRovidNev": null,
+                                                "OsztalyCsoport": {"Uid": "0"},
+                                                "SortIndex": 2
+                                              },
+                                            ),
+                                          ],
+                                          censored: true,
+                                        ),
+                                      ),
+                                      Padding(
+                                        padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 6.0),
+                                        child: Panel(
+                                          child: GradeTile(
+                                            Grade.fromJson(
+                                              {
+                                                "Uid": "0,Ertekeles",
+                                                "RogzitesDatuma": "2022-01-01T23:00:00Z",
+                                                "KeszitesDatuma": "2022-01-01T23:00:00Z",
+                                                "LattamozasDatuma": null,
+                                                "Tantargy": {
+                                                  "Uid": "0",
+                                                  "Nev": "Filc szakosztály",
+                                                  "Kategoria": {"Uid": "0,_", "Nev": "_", "Leiras": "Nem mondom meg"},
+                                                  "SortIndex": 2
+                                                },
+                                                "Tema": "Kupak csomag vásárlás vizsga",
+                                                "Tipus": {
+                                                  "Uid": "0,_",
+                                                  "Nev": "_",
+                                                  "Leiras": "Évközi jegy/értékelés",
+                                                },
+                                                "Mod": {
+                                                  "Uid": "0,_",
+                                                  "Nev": "_",
+                                                  "Leiras": "_ feladat",
+                                                },
+                                                "ErtekFajta": {
+                                                  "Uid": "1,Osztalyzat",
+                                                  "Nev": "Osztalyzat",
+                                                  "Leiras": "Elégtelen (1) és Jeles (5) között az öt alapértelmezett érték"
+                                                },
+                                                "ErtekeloTanarNeve": "Premium",
+                                                "Jelleg": "Ertekeles",
+                                                "SzamErtek": 5,
+                                                "SzovegesErtek": "Jeles(5)",
+                                                "SulySzazalekErteke": 100,
+                                                "SzovegesErtekelesRovidNev": null,
+                                                "OsztalyCsoport": {"Uid": "0"},
+                                                "SortIndex": 2
+                                              },
+                                            ),
+                                            padding: EdgeInsets.zero,
+                                            censored: true,
+                                          ),
+                                        ),
+                                      ),
+                                      Padding(
+                                        padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 6.0),
+                                        child: Panel(
+                                          child: HomeworkTile(
+                                            Homework.fromJson(
+                                              {
+                                                "Uid": "0",
+                                                "Tantargy": {
+                                                  "Uid": "0",
+                                                  "Nev": "Filc premium előnyei",
+                                                  "Kategoria": {
+                                                    "Uid": "0,_",
+                                                    "Nev": "_",
+                                                    "Leiras": "Filc premium előnyei",
+                                                  },
+                                                  "SortIndex": 0
+                                                },
+                                                "TantargyNeve": "Filc premium előnyei",
+                                                "RogzitoTanarNeve": "Kupak János",
+                                                "Szoveg": "45 perc filctollal való rajzolás",
+                                                "FeladasDatuma": "2022-01-01T23:00:00Z",
+                                                "HataridoDatuma": "2022-01-01T23:00:00Z",
+                                                "RogzitesIdopontja": "2022-01-01T23:00:00Z",
+                                                "IsTanarRogzitette": true,
+                                                "IsTanuloHaziFeladatEnabled": false,
+                                                "IsMegoldva": false,
+                                                "IsBeadhato": false,
+                                                "OsztalyCsoport": {"Uid": "0"},
+                                                "IsCsatolasEngedelyezes": false
+                                              },
+                                            ),
+                                            padding: EdgeInsets.zero,
+                                            censored: true,
+                                          ),
+                                        ),
+                                      ),
+                                      Padding(
+                                        padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 6.0),
+                                        child: Panel(
+                                          child: MessageTile(
+                                            Message.fromJson(
+                                              {
+                                                "azonosito": 0,
+                                                "isElolvasva": true,
+                                                "isToroltElem": false,
+                                                "tipus": {
+                                                  "azonosito": 1,
+                                                  "kod": "BEERKEZETT",
+                                                  "rovidNev": "Beérkezett üzenet",
+                                                  "nev": "Beérkezett üzenet",
+                                                  "leiras": "Beérkezett üzenet"
+                                                },
+                                                "uzenet": {
+                                                  "azonosito": 0,
+                                                  "kuldesDatum": "2022-01-01T23:00:00",
+                                                  "feladoNev": "Filc Napló",
+                                                  "feladoTitulus": "Nagyon magas szintű személy",
+                                                  "szoveg":
+                                                      "<p>Kedves Felhasználó!</p><p><br></p><p>A prémium vásárlásakor kapott filctollal 90%-al több esély van jó jegyek szerzésére.</p>",
+                                                  "targy": "Filctoll használati útmutató",
+                                                  "statusz": {
+                                                    "azonosito": 2,
+                                                    "kod": "KIKULDVE",
+                                                    "rovidNev": "Kiküldve",
+                                                    "nev": "Kiküldve",
+                                                    "leiras": "Kiküldve"
+                                                  },
+                                                  "cimzettLista": [
+                                                    {
+                                                      "azonosito": 0,
+                                                      "kretaAzonosito": 0,
+                                                      "nev": "Tinta Józsi",
+                                                      "tipus": {
+                                                        "azonosito": 0,
+                                                        "kod": "TANULO",
+                                                        "rovidNev": "Tanuló",
+                                                        "nev": "Tanuló",
+                                                        "leiras": "Tanuló"
+                                                      }
+                                                    },
+                                                  ],
+                                                  "csatolmanyok": [
+                                                    {"azonosito": 0, "fajlNev": "Filctoll.doc"}
+                                                  ]
+                                                }
+                                              },
+                                            ),
+                                            censored: true,
+                                          ),
+                                        ),
+                                      ),
+                                    ],
+                                  ),
+                                ),
+                              ),
+                            ),
+                          ),
+                        ),
+                      ),
+                    ),
+                    Align(
+                      alignment: Alignment.bottomCenter,
+                      child: Wrap(
+                        children: [
+                          Opacity(
+                            opacity: pickerContainerAnimation.value,
+                            child: SizedBox(
+                              width: double.infinity,
+                              child: Container(
+                                padding: const EdgeInsets.only(bottom: 12.0),
+                                decoration: BoxDecoration(
+                                  boxShadow: [
+                                    BoxShadow(
+                                      color: backgroundGradientBottomColor,
+                                      offset: const Offset(0, -4),
+                                      blurRadius: 16,
+                                      spreadRadius: 12,
+                                    ),
+                                  ],
+                                  gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: const [
+                                    0.0,
+                                    0.175
+                                  ], colors: [
+                                    backgroundGradientBottomColor,
+                                    backgroundGradientBottomColor,
+                                  ]),
+                                ),
+                                child: Column(
+                                  children: [
+                                    Padding(
+                                      padding: const EdgeInsets.symmetric(horizontal: 8.0),
+                                      child: FilterBar(
+                                        items: [
+                                          ColorTab(
+                                              color: accentColorMap[settings.accentColor] ?? unknownColor,
+                                              tab: Tab(text: "colorpicker_presets".i18n)),
+                                          ColorTab(
+                                              unlocked: hasAccess,
+                                              color: settings.customBackgroundColor ?? unknownColor,
+                                              tab: Tab(text: "colorpicker_background".i18n)),
+                                          ColorTab(
+                                              unlocked: hasAccess,
+                                              color: settings.customHighlightColor ?? unknownColor,
+                                              tab: Tab(text: "colorpicker_panels".i18n)),
+                                          ColorTab(
+                                              unlocked: hasAccess,
+                                              color: settings.customAccentColor ?? unknownColor,
+                                              tab: Tab(text: "colorpicker_accent".i18n)),
+                                        ],
+                                        onTap: (index) {
+                                          if (!hasAccess) {
+                                            index = 0;
+                                            _colorsTabController.animateTo(0, duration: Duration.zero);
+
+                                            PremiumLockedFeatureUpsell.show(context: context, feature: PremiumFeature.customcolors);
+                                          }
+
+                                          switch (index) {
+                                            case 0:
+                                              setState(() {
+                                                colorMode = CustomColorMode.theme;
+                                              });
+                                              break;
+                                            case 1:
+                                              setState(() {
+                                                colorMode = CustomColorMode.background;
+                                              });
+                                              break;
+                                            case 2:
+                                              setState(() {
+                                                colorMode = CustomColorMode.highlight;
+                                              });
+                                              break;
+                                            case 3:
+                                              setState(() {
+                                                colorMode = CustomColorMode.accent;
+                                              });
+                                              break;
+                                          }
+                                        },
+                                        controller: _colorsTabController,
+                                        padding: EdgeInsets.zero,
+                                      ),
+                                    ),
+                                    Padding(
+                                      padding: const EdgeInsets.symmetric(horizontal: 12.0),
+                                      child: SafeArea(
+                                        child: FilcColorPicker(
+                                          colorMode: colorMode,
+                                          pickerColor: colorMode == CustomColorMode.accent
+                                              ? settings.customAccentColor ?? unknownColor
+                                              : colorMode == CustomColorMode.background
+                                                  ? settings.customBackgroundColor ?? unknownColor
+                                                  : colorMode == CustomColorMode.theme
+                                                      ? (accentColorMap[settings.accentColor] ?? AppColors.of(context).text) // idk what else
+                                                      : settings.customHighlightColor ?? unknownColor,
+                                          onColorChanged: (c) {
+                                            setState(() {
+                                              updateCustomColor(c, false);
+                                            });
+                                            setTheme(settings.theme, false);
+                                          },
+                                          onColorChangeEnd: (c, {adaptive}) {
+                                            setState(() {
+                                              if (adaptive == true) {
+                                                settings.update(accentColor: AccentColor.adaptive);
+                                                settings.update(customBackgroundColor: AppColors.of(context).background, store: true);
+                                                settings.update(customHighlightColor: AppColors.of(context).highlight, store: true);
+                                              } else {
+                                                updateCustomColor(c, true);
+                                              }
+                                            });
+                                            setTheme(settings.theme, true);
+                                          },
+                                        ),
+                                      ),
+                                    ),
+                                  ],
+                                ),
+                              ),
+                            ),
+                          ),
+                        ],
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ),
+          );
+        },
+      ),
+    );
+  }
+}
+
+class ColorTab extends StatelessWidget {
+  const ColorTab({Key? key, required this.tab, required this.color, this.unlocked = true}) : super(key: key);
+
+  final Tab tab;
+  final Color color;
+  final bool unlocked;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      crossAxisAlignment: CrossAxisAlignment.center,
+      children: [
+        Transform.translate(
+          offset: const Offset(-3, 1),
+          child: unlocked
+              ? Container(
+                  width: 15,
+                  height: 15,
+                  decoration: BoxDecoration(
+                    shape: BoxShape.circle,
+                    color: color,
+                    border: Border.all(color: Colors.black, width: 2.0),
+                  ),
+                )
+              : const Padding(
+                  padding: EdgeInsets.symmetric(horizontal: 2),
+                  child: Icon(Icons.lock, color: Color.fromARGB(255, 82, 82, 82), size: 18),
+                ),
+        ),
+        tab
+      ],
+    );
+  }
+}
+
+class PremiumColorPickerItem extends StatelessWidget {
+  const PremiumColorPickerItem({Key? key, required this.label, this.onTap, required this.color}) : super(key: key);
+
+  final String label;
+  final void Function()? onTap;
+  final Color color;
+
+  @override
+  Widget build(BuildContext context) {
+    return Material(
+      type: MaterialType.transparency,
+      child: InkWell(
+        child: Padding(
+          padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              Expanded(
+                child: Text(
+                  label,
+                  style: TextStyle(color: AppColors.of(context).text, fontWeight: FontWeight.w500),
+                ),
+              ),
+              Container(
+                width: 30,
+                height: 30,
+                decoration: BoxDecoration(color: color, shape: BoxShape.circle, border: Border.all()),
+              ),
+            ],
+          ),
+        ),
+        onTap: onTap,
+      ),
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/settings/theme.i18n.dart b/filcnaplo_premium/lib/ui/mobile/settings/theme.i18n.dart
new file mode 100644
index 0000000..6f5bee7
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/settings/theme.i18n.dart
@@ -0,0 +1,33 @@
+import 'package:i18n_extension/i18n_extension.dart';
+
+extension SettingsLocalization on String {
+  static final _t = Translations.byLocale("hu_hu") +
+      {
+        "en_en": {
+          "colorpicker_presets": "Presets",
+          "colorpicker_background": "Background",
+          "colorpicker_panels": "Panels",
+          "colorpicker_accent": "Accent",
+          "need_sub": "You need Kupak subscription to use modify this.",
+        },
+        "hu_hu": {
+          "colorpicker_presets": "Téma",
+          "colorpicker_background": "Háttér",
+          "colorpicker_panels": "Panelek",
+          "colorpicker_accent": "Színtónus",
+          "need_sub": "A módosításhoz Kupak szintű támogatás szükséges.",
+        },
+        "de_de": {
+          "colorpicker_presets": "Farben",
+          "colorpicker_background": "Hintergrund",
+          "colorpicker_panels": "Tafeln",
+          "colorpicker_accent": "Akzent",
+          "need_sub": "Sie benötigen ein Kupak-Abonnement, um diese Funktion zu ändern.",
+        },
+      };
+
+  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);
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/timetable/fs_timetable.dart b/filcnaplo_premium/lib/ui/mobile/timetable/fs_timetable.dart
new file mode 100644
index 0000000..b0582d2
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/timetable/fs_timetable.dart
@@ -0,0 +1,179 @@
+import 'package:filcnaplo/helpers/subject.dart';
+import 'package:filcnaplo/theme/colors/colors.dart';
+import 'package:filcnaplo_kreta_api/controllers/timetable_controller.dart';
+import 'package:filcnaplo_mobile_ui/common/empty.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_feather_icons/flutter_feather_icons.dart';
+import 'package:filcnaplo/utils/format.dart';
+import 'dart:math' as math;
+import 'package:intl/intl.dart';
+import 'package:i18n_extension/i18n_widget.dart';
+
+class PremiumFSTimetable extends StatefulWidget {
+  const PremiumFSTimetable({Key? key, required this.controller}) : super(key: key);
+
+  final TimetableController controller;
+
+  @override
+  State<PremiumFSTimetable> createState() => _PremiumFSTimetableState();
+}
+
+class _PremiumFSTimetableState extends State<PremiumFSTimetable> {
+  @override
+  void initState() {
+    super.initState();
+
+    SystemChrome.setPreferredOrientations([
+      DeviceOrientation.landscapeLeft,
+      DeviceOrientation.landscapeRight,
+    ]);
+    SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
+    SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
+      statusBarColor: Colors.transparent,
+      systemNavigationBarColor: Colors.transparent,
+    ));
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (widget.controller.days == null || widget.controller.days!.isEmpty) {
+      return const Center(child: Empty());
+    }
+
+    final days = widget.controller.days!;
+    final everyLesson = days.expand((x) => x).toList();
+    everyLesson.sort((a, b) => a.start.compareTo(b.start));
+
+    final int maxLessonCount = days.fold(0, (a, b) => math.max(a, b.where((l) => l.subject.id != "" || l.isEmpty).length));
+
+    final int minIndex = int.tryParse(everyLesson.first.lessonIndex) ?? 0;
+    final int maxIndex = int.tryParse(everyLesson.last.lessonIndex) ?? maxLessonCount;
+
+    const prefixw = 40;
+    const padding = prefixw + 6 * 2;
+    final colw = (MediaQuery.of(context).size.width - padding) / days.length;
+
+    return Scaffold(
+      body: ListView.builder(
+        physics: const BouncingScrollPhysics(),
+        padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 24.0),
+        itemCount: maxIndex + 1,
+        itemBuilder: (context, index) {
+          List<Widget> columns = [];
+
+          for (int dayIndex = -1; dayIndex < days.length; dayIndex++) {
+            final dayOffset = dayIndex == -1 ? 0 : (int.tryParse(days[dayIndex].first.lessonIndex) ?? 0) - minIndex;
+            final lessonIndex = index - 1;
+
+            if (dayIndex == -1) {
+              if (lessonIndex >= 0) {
+                columns.add(SizedBox(
+                  width: prefixw.toDouble(),
+                  height: 40.0,
+                  child: Padding(
+                    padding: const EdgeInsets.symmetric(horizontal: 8.0),
+                    child: Text(
+                      "${minIndex + lessonIndex}.",
+                      style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.secondary),
+                    ),
+                  ),
+                ));
+              } else {
+                columns.add(SizedBox(width: prefixw.toDouble()));
+              }
+              continue;
+            }
+
+            final lessons = days[dayIndex].where((l) => l.subject.id != "" || l.isEmpty).toList();
+
+            if (lessons.isEmpty) continue;
+            if (lessonIndex >= lessons.length) continue;
+
+            if (dayIndex >= days.length || (lessonIndex + dayOffset) >= lessons.length) {
+              columns.add(SizedBox(width: colw));
+              continue;
+            }
+
+            if (lessonIndex == -1 && dayIndex >= 0) {
+              columns.add(SizedBox(
+                width: colw,
+                height: 40.0,
+                child: Text(
+                  DateFormat("EEEE", I18n.of(context).locale.languageCode).format(lessons.first.date).capital(),
+                  style: const TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold),
+                ),
+              ));
+              continue;
+            }
+
+            if (lessons[lessonIndex].isEmpty) {
+              columns.add(SizedBox(
+                width: colw,
+                child: Row(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    Icon(FeatherIcons.slash, size: 18.0, color: AppColors.of(context).text.withOpacity(.3)),
+                    const SizedBox(width: 8.0),
+                    Text(
+                      "Lyukas óra",
+                      style: TextStyle(color: AppColors.of(context).text.withOpacity(.3)),
+                    ),
+                  ],
+                ),
+              ));
+              continue;
+            }
+
+            if (dayOffset > 0 && lessonIndex < dayOffset) {
+              columns.add(SizedBox(width: colw));
+              continue;
+            }
+
+            columns.add(SizedBox(
+              width: colw,
+              child: Column(
+                mainAxisSize: MainAxisSize.min,
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Row(
+                    children: [
+                      Icon(
+                        SubjectIcon.resolveVariant(context: context, subject: lessons[lessonIndex - dayOffset].subject),
+                        size: 18.0,
+                        color: AppColors.of(context).text.withOpacity(.7),
+                      ),
+                      const SizedBox(width: 8.0),
+                      Expanded(
+                        child: Text(
+                          lessons[lessonIndex - dayOffset].subject.renamedTo ?? lessons[lessonIndex - dayOffset].subject.name.capital(),
+                          maxLines: 1,
+                          style: TextStyle(fontStyle: lessons[lessonIndex - dayOffset].subject.isRenamed ? FontStyle.italic : null),
+                          overflow: TextOverflow.clip,
+                          softWrap: false,
+                        ),
+                      ),
+                      const SizedBox(width: 15),
+                    ],
+                  ),
+                  Padding(
+                    padding: const EdgeInsets.only(left: 26.0),
+                    child: Text(
+                      lessons[lessonIndex - dayOffset].room,
+                      style: TextStyle(color: AppColors.of(context).text.withOpacity(.5), overflow: TextOverflow.ellipsis),
+                    ),
+                  ),
+                ],
+              ),
+            ));
+          }
+
+          return Row(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: columns,
+          );
+        },
+      ),
+    );
+  }
+}
diff --git a/filcnaplo_premium/lib/ui/mobile/timetable/fs_timetable_button.dart b/filcnaplo_premium/lib/ui/mobile/timetable/fs_timetable_button.dart
new file mode 100644
index 0000000..69f8b3f
--- /dev/null
+++ b/filcnaplo_premium/lib/ui/mobile/timetable/fs_timetable_button.dart
@@ -0,0 +1,45 @@
+import 'package:filcnaplo/theme/colors/colors.dart';
+import 'package:filcnaplo_kreta_api/controllers/timetable_controller.dart';
+import 'package:filcnaplo_mobile_ui/common/system_chrome.dart';
+import 'package:filcnaplo_premium/models/premium_scopes.dart';
+import 'package:filcnaplo_premium/providers/premium_provider.dart';
+import 'package:filcnaplo_premium/ui/mobile/premium/upsell.dart';
+import 'package:filcnaplo_premium/ui/mobile/timetable/fs_timetable.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_feather_icons/flutter_feather_icons.dart';
+import 'package:provider/provider.dart';
+
+class PremiumFSTimetableButton extends StatelessWidget {
+  const PremiumFSTimetableButton({Key? key, required this.controller}) : super(key: key);
+
+  final TimetableController controller;
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.all(8.0),
+      child: IconButton(
+        splashRadius: 24.0,
+        onPressed: () {
+          if (!Provider.of<PremiumProvider>(context, listen: false).hasScope(PremiumScopes.fsTimetable)) {
+            PremiumLockedFeatureUpsell.show(context: context, feature: PremiumFeature.weeklytimetable);
+            return;
+          }
+
+          Navigator.of(context, rootNavigator: true)
+              .push(PageRouteBuilder(
+            pageBuilder: (context, animation, secondaryAnimation) => PremiumFSTimetable(
+              controller: controller,
+            ),
+          ))
+              .then((_) {
+            SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
+            setSystemChrome(context);
+          });
+        },
+        icon: Icon(FeatherIcons.trello, color: AppColors.of(context).text),
+      ),
+    );
+  }
+}
diff --git a/filcnaplo_premium/pubspec.yaml b/filcnaplo_premium/pubspec.yaml
new file mode 100644
index 0000000..3ce02a3
--- /dev/null
+++ b/filcnaplo_premium/pubspec.yaml
@@ -0,0 +1,36 @@
+name: filcnaplo_premium
+publish_to: "none"
+
+environment:
+  sdk: ">=2.17.0 <3.0.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+  cupertino_icons: ^1.0.2
+
+  # Filcnaplo main dep
+  filcnaplo:
+    path: ../filcnaplo/
+  filcnaplo_kreta_api:
+    path: ../filcnaplo_kreta_api/
+  filcnaplo_mobile_ui:
+    path: "../filcnaplo_mobile_ui/"
+
+  provider: ^5.0.0
+  flutter_feather_icons: ^2.0.0+1
+  uni_links: ^0.5.1
+  url_launcher: ^6.1.6
+  dropdown_button2: ^1.8.9
+  home_widget: ^0.1.6
+  image_picker: ^0.8.6
+  image_crop: ^0.4.1
+  lottie: ^1.4.3
+  animations: ^2.0.1
+  flutter_svg: ^1.1.6
+
+dev_dependencies:
+  flutter_lints: ^1.0.0
+
+flutter:
+  uses-material-design: true