Add files via upload

This commit is contained in:
ReinerRego 2023-05-26 21:50:08 +02:00 committed by GitHub
parent 258a6ab8d3
commit baec76c29f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 5892 additions and 0 deletions

View File

@ -0,0 +1,3 @@
# Premium ✨
A collection of features only accessible for premium subscribers.

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
sdk.dir=/Users/unknown/Library/Android/sdk
flutter.sdk=/Users/unknown/flutter

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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