forked from firka/student-legacy
Add files via upload
This commit is contained in:
parent
258a6ab8d3
commit
baec76c29f
3
filcnaplo_premium/README.md
Normal file
3
filcnaplo_premium/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Premium ✨
|
||||||
|
|
||||||
|
A collection of features only accessible for premium subscribers.
|
28
filcnaplo_premium/analysis_options.yaml
Normal file
28
filcnaplo_premium/analysis_options.yaml
Normal 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
|
119
filcnaplo_premium/android/database/DBManager.java
Normal file
119
filcnaplo_premium/android/database/DBManager.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
36
filcnaplo_premium/android/database/SQLiteHelper.java
Normal file
36
filcnaplo_premium/android/database/SQLiteHelper.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
2
filcnaplo_premium/android/local.properties
Normal file
2
filcnaplo_premium/android/local.properties
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
sdk.dir=/Users/unknown/Library/Android/sdk
|
||||||
|
flutter.sdk=/Users/unknown/flutter
|
36
filcnaplo_premium/android/utils/Utils.java
Normal file
36
filcnaplo_premium/android/utils/Utils.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
65
filcnaplo_premium/android/utils/Week.java
Normal file
65
filcnaplo_premium/android/utils/Week.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
397
filcnaplo_premium/android/widget_timetable/WidgetTimetable.java
Normal file
397
filcnaplo_premium/android/widget_timetable/WidgetTimetable.java
Normal 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) {
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
120
filcnaplo_premium/lib/api/auth.dart
Normal file
120
filcnaplo_premium/lib/api/auth.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
19
filcnaplo_premium/lib/models/premium_result.dart
Normal file
19
filcnaplo_premium/lib/models/premium_result.dart
Normal 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"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
32
filcnaplo_premium/lib/models/premium_scopes.dart
Normal file
32
filcnaplo_premium/lib/models/premium_scopes.dart
Normal 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";
|
||||||
|
}
|
28
filcnaplo_premium/lib/providers/premium_provider.dart
Normal file
28
filcnaplo_premium/lib/providers/premium_provider.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
174
filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/colors.dart
Normal file
174
filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/colors.dart
Normal 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);
|
||||||
|
}
|
785
filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/palette.dart
Normal file
785
filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/palette.dart
Normal 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);
|
||||||
|
}
|
220
filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/utils.dart
Normal file
220
filcnaplo_premium/lib/ui/mobile/flutter_colorpicker/utils.dart
Normal 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);
|
||||||
|
}
|
156
filcnaplo_premium/lib/ui/mobile/goal_planner/goal_input.dart
Normal file
156
filcnaplo_premium/lib/ui/mobile/goal_planner/goal_input.dart
Normal 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);
|
||||||
|
}
|
172
filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner.dart
Normal file
172
filcnaplo_premium/lib/ui/mobile/goal_planner/goal_planner.dart
Normal 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);
|
||||||
|
}
|
@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
126
filcnaplo_premium/lib/ui/mobile/goal_planner/route_option.dart
Normal file
126
filcnaplo_premium/lib/ui/mobile/goal_planner/route_option.dart
Normal 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
209
filcnaplo_premium/lib/ui/mobile/goal_planner/test.dart
Normal file
209
filcnaplo_premium/lib/ui/mobile/goal_planner/test.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
92
filcnaplo_premium/lib/ui/mobile/grades/average_selector.dart
Normal file
92
filcnaplo_premium/lib/ui/mobile/grades/average_selector.dart
Normal 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
66
filcnaplo_premium/lib/ui/mobile/premium/premium_inline.dart
Normal file
66
filcnaplo_premium/lib/ui/mobile/premium/premium_inline.dart
Normal 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());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
164
filcnaplo_premium/lib/ui/mobile/premium/upsell.dart
Normal file
164
filcnaplo_premium/lib/ui/mobile/premium/upsell.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
34
filcnaplo_premium/lib/ui/mobile/settings/icon_pack.dart
Normal file
34
filcnaplo_premium/lib/ui/mobile/settings/icon_pack.dart
Normal 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()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
93
filcnaplo_premium/lib/ui/mobile/settings/nickname.dart
Normal file
93
filcnaplo_premium/lib/ui/mobile/settings/nickname.dart
Normal 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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
208
filcnaplo_premium/lib/ui/mobile/settings/profile_pic.dart
Normal file
208
filcnaplo_premium/lib/ui/mobile/settings/profile_pic.dart
Normal 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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
657
filcnaplo_premium/lib/ui/mobile/settings/theme.dart
Normal file
657
filcnaplo_premium/lib/ui/mobile/settings/theme.dart
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
33
filcnaplo_premium/lib/ui/mobile/settings/theme.i18n.dart
Normal file
33
filcnaplo_premium/lib/ui/mobile/settings/theme.i18n.dart
Normal 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);
|
||||||
|
}
|
179
filcnaplo_premium/lib/ui/mobile/timetable/fs_timetable.dart
Normal file
179
filcnaplo_premium/lib/ui/mobile/timetable/fs_timetable.dart
Normal 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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
36
filcnaplo_premium/pubspec.yaml
Normal file
36
filcnaplo_premium/pubspec.yaml
Normal 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
|
Loading…
x
Reference in New Issue
Block a user