moved non-premium features from premium folder

This commit is contained in:
Kima
2023-10-21 21:11:39 +02:00
parent 774f63aa89
commit 316039463f
25 changed files with 90 additions and 146 deletions

View File

@@ -0,0 +1,105 @@
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",
90: "3_months_average",
30: "30_days_average",
14: "14_days_average",
7: "7_days_average",
};
class AverageSelector extends StatefulWidget {
const AverageSelector({Key? key, this.onChanged, required this.value}) : super(key: key);
final Function(int?)? onChanged;
final int value;
@override
_AverageSelectorState createState() => _AverageSelectorState();
}
class _AverageSelectorState extends State<AverageSelector> {
@override
Widget build(BuildContext context) {
List<DropdownMenuItem<int>> dropdownItems = avgDropItems.keys.map((item) {
return DropdownMenuItem<int>(
value: item,
child: Text(
avgDropItems[item]!.i18n,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppColors.of(context).text,
),
overflow: TextOverflow.ellipsis,
),
);
}).toList();
return DropdownButton2<int>(
items: dropdownItems,
onChanged: (int? value) {
if (Provider.of<PremiumProvider>(context, listen: false).hasScope(PremiumScopes.gradeStats)) {
if (widget.onChanged != null) {
setState(() {
widget.onChanged!(value);
});
}
} else {
PremiumLockedFeatureUpsell.show(context: context, feature: PremiumFeature.gradestats);
}
},
value: widget.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[widget.value]!.i18n,
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(fontWeight: FontWeight.w600, color: AppColors.of(context).text.withOpacity(0.65)),
),
const SizedBox(
width: 4,
),
Icon(
FeatherIcons.chevronDown,
size: 16,
color: AppColors.of(context).text,
),
],
),
),
);
}
}

View File

@@ -26,7 +26,7 @@ import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:filcnaplo/helpers/average_helper.dart';
import 'package:filcnaplo_premium/ui/mobile/grades/average_selector.dart';
import 'average_selector.dart';
import 'package:filcnaplo_premium/ui/mobile/premium/premium_inline.dart';
import 'grades_page.i18n.dart';
@@ -240,7 +240,7 @@ class _GradesPageState extends State<GradesPage> {
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
PremiumAverageSelector(
AverageSelector(
value: avgDropValue,
onChanged: (value) {
setState(() {

View File

@@ -0,0 +1,218 @@
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/models/settings.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';
import 'package:provider/provider.dart';
class FSTimetable extends StatefulWidget {
const FSTimetable({Key? key, required this.controller})
: super(key: key);
final TimetableController controller;
@override
State<FSTimetable> createState() => _FSTimetableState();
}
class _FSTimetableState extends State<FSTimetable> {
late SettingsProvider settings;
@override
void initState() {
super.initState();
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
]);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
));
}
@override
Widget build(BuildContext context) {
settings = Provider.of<SettingsProvider>(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));
const prefixw = 40;
const padding = prefixw + 6 * 2;
final colw = (MediaQuery.of(context).size.width - padding) / days.length;
return Scaffold(
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
shadowColor: Colors.transparent,
),
body: ListView.builder(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 24.0),
itemCount: maxLessonCount + 1,
itemBuilder: (context, index) {
List<Widget> columns = [];
for (int dayIndex = -1; dayIndex < days.length; dayIndex++) {
if (dayIndex == -1) {
if (index >= 1) {
columns.add(SizedBox(
width: prefixw.toDouble(),
height: 40.0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
(index).toString()+".",
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;
final dayOffset = int.tryParse(lessons.first.lessonIndex) ?? 0;
if (index == 0 && 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;
}
final lessonIndex = index - dayOffset;
if (lessonIndex < 0 || lessonIndex >= lessons.length) {
columns.add(SizedBox(width: colw));
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;
}
columns.add(
SizedBox(
width: colw,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
SubjectIcon.resolveVariant(
context: context,
subject: lessons[lessonIndex].subject,
),
size: 18.0,
color: AppColors.of(context).text.withOpacity(.7),
),
const SizedBox(width: 8.0),
Expanded(
child: Text(
lessons[lessonIndex].subject.renamedTo ??
lessons[lessonIndex].subject.name.capital(),
maxLines: 1,
style: TextStyle(
fontStyle: lessons[lessonIndex]
.subject
.isRenamed && settings.renamedSubjectsItalics
? FontStyle.italic
: null,
),
overflow: TextOverflow.clip,
softWrap: false,
),
),
const SizedBox(width: 15),
],
),
Padding(
padding: const EdgeInsets.only(left: 26.0),
child: Text(
lessons[lessonIndex].room,
style: TextStyle(
color: AppColors.of(context).text.withOpacity(.5),
overflow: TextOverflow.ellipsis,
),
),
),
],
),
),
);
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: columns,
);
},
),
);
}
}

View File

@@ -12,19 +12,21 @@ import 'package:filcnaplo_mobile_ui/common/empty.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_button.dart';
import 'package:filcnaplo_mobile_ui/common/profile_image/profile_image.dart';
import 'package:filcnaplo_mobile_ui/common/system_chrome.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/lesson/lesson_view.dart';
import 'package:filcnaplo_kreta_api/controllers/timetable_controller.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/lesson/lesson_viewable.dart';
import 'package:filcnaplo_mobile_ui/pages/timetable/day_title.dart';
import 'package:filcnaplo_mobile_ui/pages/timetable/fs_timetable.dart';
import 'package:filcnaplo_mobile_ui/screens/navigation/navigation_route_handler.dart';
import 'package:filcnaplo_mobile_ui/screens/navigation/navigation_screen.dart';
import 'package:flutter/cupertino.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';
import 'package:intl/intl.dart';
import 'package:i18n_extension/i18n_widget.dart';
import 'package:filcnaplo_premium/ui/mobile/timetable/fs_timetable_button.dart';
import 'timetable_page.i18n.dart';
// todo: "fix" overflow (priority: -1)
@@ -199,7 +201,38 @@ class _TimetablePageState extends State<TimetablePage>
snap: false,
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
actions: [
PremiumFSTimetableButton(controller: _controller, tabcontroller: _tabController),
Padding(
padding: const EdgeInsets.all(8.0),
child: IconButton(
splashRadius: 24.0,
onPressed: () {
// If timetable empty, show empty
if (_tabController.length == 0) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text("empty_timetable".i18n),
duration: const Duration(seconds: 2),
));
return;
}
Navigator.of(context, rootNavigator: true)
.push(PageRouteBuilder(
pageBuilder:
(context, animation, secondaryAnimation) =>
FSTimetable(
controller: _controller,
),
))
.then((_) {
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp]);
setSystemChrome(context);
});
},
icon: Icon(FeatherIcons.trello,
color: AppColors.of(context).text),
),
),
// Profile Icon
Padding(

View File

@@ -0,0 +1,72 @@
import 'package:i18n_extension/i18n_extension.dart';
extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
// subject rename
"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",
"italics_toggle": "Italic Font",
// teacher rename
"renamed_teachers": "Renamed Teachers",
"rename_teachers": "Rename Teachers",
"rename_teacher": "Rename Teacher",
"select_teacher": "Select Teacher",
"modify_teachers": "Modify Teachers",
"rename_new_teacher": "Rename New Teacher",
},
"hu_hu": {
// subject rename
"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 módosítása",
"cancel": "Mégse",
"done": "Kész",
"rename_new_subject": "Új tantárgy átnevezése",
"italics_toggle": "Dőlt betűs megjelenítés",
// teacher rename
"renamed_teachers": "Átnevezett tanáraid",
"rename_teachers": "Tanárok átnevezése",
"rename_teacher": "Tanár átnevezése",
"select_teacher": "Válassz tanárt",
"modify_teachers": "Tanárok módosítása",
"rename_new_teacher": "Új tanár átnevezése",
},
"de_de": {
// subject rename
"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",
"italics_toggle": "Kursivschrift umschalten",
// teacher rename
"renamed_teachers": "Renamed Teachers",
"rename_teachers": "Rename Teachers",
"rename_teacher": "Rename Teacher",
"select_teacher": "Select Teacher",
"modify_teachers": "Modify Teachers",
"rename_new_teacher": "Rename New Teacher",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -0,0 +1,438 @@
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_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<GradeSubject> subjects;
late UserProvider user;
late DatabaseProvider dbProvider;
late SettingsProvider settings;
@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) {
settings = Provider.of<SettingsProvider>(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: [
Panel(
child: SwitchListTile(
title: Text("italics_toggle".i18n),
onChanged: (value) =>
settings.update(renamedSubjectsItalics: value),
value: settings.renamedSubjectsItalics,
),
),
const SizedBox(
height: 20,
),
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) {
GradeSubject? 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 GradeSubject subject;
final String renamedTo;
final void Function() modifyCallback;
final void Function() removeCallback;
@override
Widget build(BuildContext context) {
return ListTile(
minLeadingWidth: 32.0,
dense: true,
contentPadding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
visualDensity: VisualDensity.compact,
onTap: () {},
leading: Icon(
SubjectIcon.resolveVariant(subject: subject, context: context),
color: AppColors.of(context).text.withOpacity(.75)),
title: InkWell(
onTap: modifyCallback,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
subject.name.capital(),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
color: AppColors.of(context).text.withOpacity(.75)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
renamedTo,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 16),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
trailing: InkWell(
onTap: removeCallback,
child: Icon(FeatherIcons.trash,
color: AppColors.of(context).red.withOpacity(.75)),
),
);
}
}

View File

@@ -27,7 +27,7 @@ import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.i18n.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
import 'package:filcnaplo/models/icon_pack.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/theme.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/theme_screen.dart';
class SettingsHelper {
static const Map<String, String> langMap = {

View File

@@ -27,6 +27,7 @@ import 'package:filcnaplo_mobile_ui/screens/news/news_screen.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/accounts/account_tile.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/accounts/account_view.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/debug/subject_icon_gallery.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/modify_subject_names.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/notifications_screen.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/privacy_view.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/settings_helper.dart';
@@ -39,10 +40,8 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'settings_screen.i18n.dart';
import 'package:flutter/services.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/nickname.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/profile_pic.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/icon_pack.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/modify_subject_names.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/user/nickname.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/user/profile_pic.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/modify_teacher_names.dart';
import 'package:filcnaplo_premium/ui/mobile/settings/welcome_message.dart';
@@ -685,7 +684,17 @@ class _SettingsScreenState extends State<SettingsScreen>
activeColor: Theme.of(context).colorScheme.secondary,
),
),
const PremiumIconPackSelector(),
PanelButton(
onPressed: () {
SettingsHelper.iconPack(context);
},
title: Text("icon_pack".i18n),
leading: const Icon(FeatherIcons.grid),
trailing: Text(
settings.iconPack.name.capital(),
style: const TextStyle(fontSize: 14.0),
),
),
// if ios show live activity color option
if (defaultTargetPlatform == TargetPlatform.iOS)

View File

@@ -0,0 +1,977 @@
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/models/shared_theme.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/action_button.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_kreta_api/providers/share_provider.dart';
import 'package:filcnaplo/ui/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:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'theme_screen.i18n.dart';
import 'package:share_plus/share_plus.dart';
class PremiumCustomAccentColorSetting extends StatefulWidget {
const PremiumCustomAccentColorSetting({Key? key}) : super(key: key);
@override
State<PremiumCustomAccentColorSetting> createState() =>
_PremiumCustomAccentColorSettingState();
}
enum CustomColorMode {
theme,
saved,
accent,
background,
highlight,
icon,
enterId,
}
class _PremiumCustomAccentColorSettingState
extends State<PremiumCustomAccentColorSetting>
with TickerProviderStateMixin {
late final SettingsProvider settings;
late final ShareProvider shareProvider;
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: 5, vsync: this);
_testTabController = TabController(length: 4, vsync: this);
settings = Provider.of<SettingsProvider>(context, listen: false);
shareProvider = Provider.of<ShareProvider>(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);
}
dynamic getCustomColor() {
switch (colorMode) {
case CustomColorMode.theme:
return accentColorMap[settings.accentColor];
case CustomColorMode.saved:
return [
settings.customBackgroundColor,
settings.customHighlightColor,
settings.customAccentColor
];
case CustomColorMode.background:
return settings.customBackgroundColor;
case CustomColorMode.highlight:
return settings.customHighlightColor;
case CustomColorMode.accent:
return settings.customAccentColor;
case CustomColorMode.icon:
return settings.customIconColor;
case CustomColorMode.enterId:
// do nothing here lol
break;
}
}
void updateCustomColor(dynamic v, bool store,
{Color? accent, Color? background, Color? panels, Color? icon}) {
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.saved:
settings.update(customBackgroundColor: v[0], store: store);
settings.update(customHighlightColor: v[1], store: store);
settings.update(customAccentColor: v[3], 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;
case CustomColorMode.icon:
settings.update(customIconColor: v, store: store);
break;
case CustomColorMode.enterId:
settings.update(customBackgroundColor: background, store: store);
settings.update(customHighlightColor: panels, store: store);
settings.update(customAccentColor: accent, store: store);
settings.update(customIconColor: icon, 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),
actions: [
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: IconButton(
focusColor: Colors.transparent,
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
onPressed: () async {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// duration: Duration(milliseconds: 1000),
// content: Text(
// "Hamarosan...",
// ),
// ),
// );
showDialog(
context: context,
builder: (context) => WillPopScope(
onWillPop: () async => false,
child: AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0)),
title: Text("attention".i18n),
content: Text("share_disclaimer".i18n),
actions: [
ActionButton(
label: "understand".i18n,
onTap: () async {
Navigator.of(context).pop();
SharedGradeColors gradeColors =
await shareProvider
.shareCurrentGradeColors(context);
SharedTheme theme =
await shareProvider.shareCurrentTheme(
context,
gradeColors: gradeColors,
);
Share.share(
theme.id,
subject: 'share_subj_theme'.i18n,
);
},
),
],
),
),
);
},
icon: const Icon(
FeatherIcons.share2,
size: 22.0,
),
),
),
],
title: Text(
"theme_prev".i18n,
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(
// https://discord.com/channels/1111649116020285532/1153619667848548452
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [
0.35,
0.75
],
colors: [
settings.customBackgroundColor ??
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,
disableFading: 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": "reFilc 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": "reFilc 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":
"reFilc premium előnyei",
"Kategoria": {
"Uid": "0,_",
"Nev": "_",
"Leiras":
"reFilc premium előnyei",
},
"SortIndex": 0
},
"TantargyNeve":
"reFilc 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": "reFilc",
"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, -8),
blurRadius: 16,
spreadRadius: 18,
),
],
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(
color: unknownColor,
tab: Tab(text: "enter_id".i18n)),
/*ColorTab(
color:
settings.customAccentColor ??
unknownColor,
tab: Tab(
text: "colorpicker_saved"
.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)),
// ColorTab(
// unlocked: hasAccess,
// color: settings.customIconColor ??
// unknownColor,
// tab: Tab(
// text:
// "colorpicker_icon".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.enterId;
});
break;
/*case 1:
setState(() {
colorMode =
CustomColorMode.saved;
});
break;*/
case 2:
setState(() {
colorMode =
CustomColorMode.background;
});
break;
case 3:
setState(() {
colorMode =
CustomColorMode.highlight;
});
break;
case 4:
setState(() {
colorMode =
CustomColorMode.accent;
});
break;
case 5:
setState(() {
colorMode =
CustomColorMode.icon;
});
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
: colorMode ==
CustomColorMode
.highlight
? settings
.customHighlightColor ??
unknownColor
: settings
.customIconColor ??
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);
settings.update(
customIconColor:
const Color(0x00000000),
store: true);
} else {
updateCustomColor(c, true);
}
});
setTheme(settings.theme, true);
},
onThemeIdProvided: (theme) {
// changing grade colors
List<Color> colors = [
theme.gradeColors.oneColor,
theme.gradeColors.twoColor,
theme.gradeColors.threeColor,
theme.gradeColors.fourColor,
theme.gradeColors.fiveColor,
];
settings.update(
gradeColors: colors);
// changing shadow effect
settings.update(
shadowEffect:
theme.shadowEffect);
// changing theme
setState(() {
updateCustomColor(
null,
true,
accent: theme.accentColor,
background:
theme.backgroundColor,
panels: theme.panelsColor,
icon: theme.iconColor,
);
});
setTheme(settings.theme, true);
},
),
),
),
],
),
),
),
),
],
),
),
],
),
),
),
);
},
),
);
}
}
class ColorTab extends StatelessWidget {
const ColorTab(
{Key? key, required this.tab, required this.color, this.unlocked = true})
: super(key: key);
final Tab tab;
final Color color;
final bool unlocked;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Transform.translate(
offset: const Offset(-3, 1),
child: unlocked
? Container(
width: 15,
height: 15,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
border: Border.all(color: Colors.black, width: 2.0),
),
)
: const Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
child: Icon(Icons.lock,
color: Color.fromARGB(255, 82, 82, 82), size: 18),
),
),
tab
],
);
}
}
class PremiumColorPickerItem extends StatelessWidget {
const PremiumColorPickerItem(
{Key? key, required this.label, this.onTap, required this.color})
: super(key: key);
final String label;
final void Function()? onTap;
final Color color;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: InkWell(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
label,
style: TextStyle(
color: AppColors.of(context).text,
fontWeight: FontWeight.w500),
),
),
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: color, shape: BoxShape.circle, border: Border.all()),
),
],
),
),
onTap: onTap,
),
);
}
}

View File

@@ -0,0 +1,67 @@
import 'package:i18n_extension/i18n_extension.dart';
extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"theme_prev": "Preview",
"colorpicker_presets": "Presets",
"colorpicker_background": "Background",
"colorpicker_panels": "Panels",
"colorpicker_accent": "Accent",
"colorpicker_icon": "Icon",
"need_sub": "You need Kupak subscription to use modify this.",
"advanced": "Advanced",
"enter_id": "Enter ID",
"theme_id": "Theme ID...",
"theme_not_found": "Theme not found!",
"attention": "Attention!",
"share_disclaimer":
"By sharing the theme, you agree that the nickname you set and all settings of the theme will be shared publicly.",
"understand": "I understand",
"share_subj_theme": "Share Theme",
},
"hu_hu": {
"theme_prev": "Előnézet",
"colorpicker_presets": "Téma",
"colorpicker_background": "Háttér",
"colorpicker_panels": "Panelek",
"colorpicker_accent": "Színtónus",
"colorpicker_icon": "Ikon",
"need_sub": "A módosításhoz Kupak szintű támogatás szükséges.",
"advanced": "Haladó",
"enter_id": "ID megadása",
"theme_id": "Téma azonosító...",
"theme_not_found": "A téma nem található!",
"attention": "Figyelem!",
"share_disclaimer":
"A téma megosztásával elfogadod, hogy az általad beállított becenév és a téma minden beállítása nyilvánosan megosztásra kerüljön.",
"understand": "Értem",
"share_subj_theme": "Téma Megosztás",
},
"de_de": {
"theme_prev": "Vorschau",
"colorpicker_presets": "Farben",
"colorpicker_background": "Hintergrund",
"colorpicker_panels": "Tafeln",
"colorpicker_accent": "Akzent",
"colorpicker_icon": "Ikone",
"need_sub":
"Sie benötigen ein Kupak-Abonnement, um diese Funktion zu ändern.",
"advanced": "Fortschrittlich",
"enter_id": "Geben Sie die ID ein",
"theme_id": "Themen-ID...",
"theme_not_found": "Thema nicht gefunden!",
"attention": "Achtung!",
"share_disclaimer":
"Durch das Teilen des Themes erklären Sie sich damit einverstanden, dass der von Ihnen festgelegte Spitzname und alle Einstellungen des Themes öffentlich geteilt werden.",
"understand": "Ich verstehe",
"share_subj_theme": "Thema Teilen",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -0,0 +1,105 @@
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/models/user.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';
// ignore: must_be_immutable
class UserMenuNickname extends StatelessWidget {
late User u;
UserMenuNickname(this.u, {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) => UserNicknameEditor(u));
},
icon: const Icon(FeatherIcons.edit2),
title: Text("edit_nickname".i18n),
);
}
}
// ignore: must_be_immutable
class UserNicknameEditor extends StatefulWidget {
late User u;
UserNicknameEditor(this.u, {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(widget.u.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: () {
widget.u.nickname = _userName.text.trim();
Provider.of<DatabaseProvider>(context, listen: false)
.store
.storeUser(widget.u);
Provider.of<UserProvider>(context, listen: false).refresh();
Navigator.of(context).pop(true);
},
),
],
);
}
}

View File

@@ -0,0 +1,234 @@
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/models/user.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';
// ignore: must_be_immutable
class UserMenuProfilePic extends StatelessWidget {
late User u;
UserMenuProfilePic(this.u, {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) => UserProfilePicEditor(u));
},
icon: const Icon(FeatherIcons.camera),
title: Text("edit_profile_picture".i18n),
);
}
}
// ignore: must_be_immutable
class UserProfilePicEditor extends StatefulWidget {
late User u;
UserProfilePicEditor(this.u, {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);
widget.u.picture = base64Image;
Provider.of<DatabaseProvider>(context, listen: false)
.store
.storeUser(widget.u);
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 (widget.u.picture != "")
TextButton(
child: Text(
"remove_profile_picture".i18n,
style: const TextStyle(
fontWeight: FontWeight.w500, color: Colors.red),
),
onPressed: () {
widget.u.picture = "";
Provider.of<DatabaseProvider>(context, listen: false)
.store
.storeUser(widget.u);
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);
},
),
],
);
}
}