Add files via upload

This commit is contained in:
ReinerRego
2023-05-26 21:50:08 +02:00
committed by GitHub
parent 258a6ab8d3
commit baec76c29f
39 changed files with 5892 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel_button.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/settings_helper.dart';
import 'package:filcnaplo_premium/models/premium_scopes.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:filcnaplo_premium/ui/mobile/premium/upsell.dart';
import 'package:flutter/material.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.i18n.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'package:filcnaplo/utils/format.dart';
class PremiumIconPackSelector extends StatelessWidget {
const PremiumIconPackSelector({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final settings = Provider.of<SettingsProvider>(context);
return PanelButton(
onPressed: () {
if (!Provider.of<PremiumProvider>(context, listen: false).hasScope(PremiumScopes.customIcons)) {
PremiumLockedFeatureUpsell.show(context: context, feature: PremiumFeature.iconpack);
return;
}
SettingsHelper.iconPack(context);
},
title: Text("icon_pack".i18n),
leading: const Icon(FeatherIcons.grid),
trailing: Text(settings.iconPack.name.capital()),
);
}
}

View File

@@ -0,0 +1,383 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo/helpers/subject.dart';
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/utils/format.dart';
import 'package:filcnaplo_kreta_api/models/subject.dart';
import 'package:filcnaplo_kreta_api/providers/absence_provider.dart';
import 'package:filcnaplo_kreta_api/providers/grade_provider.dart';
import 'package:filcnaplo_kreta_api/providers/timetable_provider.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel_button.dart';
import 'package:filcnaplo_premium/models/premium_scopes.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:filcnaplo_premium/ui/mobile/premium/upsell.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:provider/provider.dart';
import 'modify_subject_names.i18n.dart';
class MenuRenamedSubjects extends StatelessWidget {
const MenuRenamedSubjects({Key? key, required this.settings}) : super(key: key);
final SettingsProvider settings;
@override
Widget build(BuildContext context) {
return PanelButton(
padding: const EdgeInsets.only(left: 14.0),
onPressed: () {
if (!Provider.of<PremiumProvider>(context, listen: false).hasScope(PremiumScopes.renameSubjects)) {
PremiumLockedFeatureUpsell.show(context: context, feature: PremiumFeature.subjectrename);
return;
}
Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(builder: (context) => const ModifySubjectNames()),
);
},
title: Text(
"rename_subjects".i18n,
style: TextStyle(color: AppColors.of(context).text.withOpacity(settings.renamedSubjectsEnabled ? 1.0 : .5)),
),
leading: settings.renamedSubjectsEnabled
? const Icon(FeatherIcons.penTool)
: Icon(FeatherIcons.penTool, color: AppColors.of(context).text.withOpacity(.25)),
trailingDivider: true,
trailing: Switch(
onChanged: (v) async {
if (!Provider.of<PremiumProvider>(context, listen: false).hasScope(PremiumScopes.renameSubjects)) {
PremiumLockedFeatureUpsell.show(context: context, feature: PremiumFeature.subjectrename);
return;
}
settings.update(renamedSubjectsEnabled: v);
await Provider.of<GradeProvider>(context, listen: false).convertBySettings();
await Provider.of<TimetableProvider>(context, listen: false).convertBySettings();
await Provider.of<AbsenceProvider>(context, listen: false).convertBySettings();
},
value: settings.renamedSubjectsEnabled,
activeColor: Theme.of(context).colorScheme.secondary,
),
);
}
}
class ModifySubjectNames extends StatefulWidget {
const ModifySubjectNames({Key? key}) : super(key: key);
@override
State<ModifySubjectNames> createState() => _ModifySubjectNamesState();
}
class _ModifySubjectNamesState extends State<ModifySubjectNames> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final _subjectName = TextEditingController();
String? selectedSubjectId;
late List<Subject> subjects;
late UserProvider user;
late DatabaseProvider dbProvider;
@override
void initState() {
super.initState();
subjects = Provider.of<GradeProvider>(context, listen: false).grades.map((e) => e.subject).toSet().toList()
..sort((a, b) => a.name.compareTo(b.name));
user = Provider.of<UserProvider>(context, listen: false);
dbProvider = Provider.of<DatabaseProvider>(context, listen: false);
}
Future<Map<String, String>> fetchRenamedSubjects() async {
return await dbProvider.userQuery.renamedSubjects(userId: user.id!);
}
void showRenameDialog() {
showDialog(
context: context,
builder: (context) => StatefulBuilder(builder: (context, setS) {
return AlertDialog(
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(14.0))),
title: Text("rename_subject".i18n),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
DropdownButton2(
items: subjects
.map((item) => DropdownMenuItem<String>(
value: item.id,
child: Text(
item.name,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppColors.of(context).text,
),
overflow: TextOverflow.ellipsis,
),
))
.toList(),
onChanged: (String? v) async {
final renamedSubs = await fetchRenamedSubjects();
setS(() {
selectedSubjectId = v;
if (renamedSubs.containsKey(selectedSubjectId)) {
_subjectName.text = renamedSubs[selectedSubjectId]!;
} else {
_subjectName.text = "";
}
});
},
iconSize: 14,
iconEnabledColor: AppColors.of(context).text,
iconDisabledColor: AppColors.of(context).text,
underline: const SizedBox(),
itemHeight: 40,
itemPadding: const EdgeInsets.only(left: 14, right: 14),
buttonWidth: 50,
dropdownWidth: 300,
dropdownPadding: null,
buttonDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
dropdownDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
),
dropdownElevation: 8,
scrollbarRadius: const Radius.circular(40),
scrollbarThickness: 6,
scrollbarAlwaysShow: true,
offset: const Offset(-10, -10),
buttonSplashColor: Colors.transparent,
customButton: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 2),
borderRadius: BorderRadius.circular(12.0),
),
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 8.0),
child: Text(
selectedSubjectId == null ? "select_subject".i18n : subjects.firstWhere((element) => element.id == selectedSubjectId).name,
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(fontWeight: FontWeight.w700, color: AppColors.of(context).text.withOpacity(0.75)),
overflow: TextOverflow.ellipsis,
maxLines: 2,
textAlign: TextAlign.center,
),
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Icon(FeatherIcons.arrowDown, size: 32),
),
TextField(
controller: _subjectName,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.grey, width: 1.5),
borderRadius: BorderRadius.circular(12.0),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.grey, width: 1.5),
borderRadius: BorderRadius.circular(12.0),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
hintText: "modified_name".i18n,
suffixIcon: IconButton(
icon: const Icon(
FeatherIcons.x,
color: Colors.grey,
),
onPressed: () {
setState(() {
_subjectName.text = "";
});
},
),
),
),
],
),
actions: [
TextButton(
child: Text(
"cancel".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () {
Navigator.of(context).maybePop();
},
),
TextButton(
child: Text(
"done".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () async {
if (selectedSubjectId != null) {
final renamedSubs = await fetchRenamedSubjects();
renamedSubs[selectedSubjectId!] = _subjectName.text;
await dbProvider.userStore.storeRenamedSubjects(renamedSubs, userId: user.id!);
await Provider.of<GradeProvider>(context, listen: false).convertBySettings();
await Provider.of<TimetableProvider>(context, listen: false).convertBySettings();
await Provider.of<AbsenceProvider>(context, listen: false).convertBySettings();
}
Navigator.of(context).pop(true);
setState(() {});
},
),
],
);
}),
).then((val) {
_subjectName.text = "";
selectedSubjectId = null;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text(
"modify_subjects".i18n,
style: TextStyle(color: AppColors.of(context).text),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: showRenameDialog,
borderRadius: BorderRadius.circular(12.0),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 2),
borderRadius: BorderRadius.circular(12.0),
),
padding: const EdgeInsets.symmetric(vertical: 18.0, horizontal: 12.0),
child: Center(
child: Text(
"rename_new_subject".i18n,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
color: AppColors.of(context).text.withOpacity(.85),
),
),
),
),
),
const SizedBox(
height: 30,
),
FutureBuilder<Map<String, String>>(
future: fetchRenamedSubjects(),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data!.isEmpty) return Container();
return Panel(
title: Text("renamed_subjects".i18n),
child: Column(
children: snapshot.data!.keys.map(
(key) {
Subject? subject = subjects.firstWhere((element) => key == element.id);
String renameTo = snapshot.data![key]!;
return RenamedSubjectItem(
subject: subject,
renamedTo: renameTo,
modifyCallback: () {
setState(() {
selectedSubjectId = subject.id;
_subjectName.text = renameTo;
});
showRenameDialog();
},
removeCallback: () {
setState(() {
Map<String, String> subs = Map.from(snapshot.data!);
subs.remove(key);
dbProvider.userStore.storeRenamedSubjects(subs, userId: user.id!);
});
},
);
},
).toList(),
),
);
},
),
],
),
),
));
}
}
class RenamedSubjectItem extends StatelessWidget {
const RenamedSubjectItem({
Key? key,
required this.subject,
required this.renamedTo,
required this.modifyCallback,
required this.removeCallback,
}) : super(key: key);
final Subject subject;
final String renamedTo;
final void Function() modifyCallback;
final void Function() removeCallback;
@override
Widget build(BuildContext context) {
return ListTile(
minLeadingWidth: 32.0,
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
visualDensity: VisualDensity.compact,
onTap: () {},
leading: Icon(SubjectIcon.resolveVariant(subject: subject, context: context), color: AppColors.of(context).text.withOpacity(.75)),
title: InkWell(
onTap: modifyCallback,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
subject.name.capital(),
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14, color: AppColors.of(context).text.withOpacity(.75)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
renamedTo,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 16),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
trailing: InkWell(
onTap: removeCallback,
child: Icon(FeatherIcons.trash, color: AppColors.of(context).red.withOpacity(.75)),
),
);
}
}

View File

@@ -0,0 +1,45 @@
import 'package:i18n_extension/i18n_extension.dart';
extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"renamed_subjects": "Renamed Subjects",
"rename_subjects": "Rename Subjects",
"rename_subject": "Rename Subject",
"select_subject": "Select Subject",
"modified_name": "Modified Name",
"modify_subjects": "Modify Subjects",
"cancel": "Cancel",
"done": "Done",
"rename_new_subject": "Rename New Subject",
},
"hu_hu": {
"renamed_subjects": "Átnevezett Tantárgyaid",
"rename_subjects": "Tantárgyak átnevezése",
"rename_subject": "Tantárgy átnevezése",
"select_subject": "Válassz tantárgyat",
"modified_name": "Módosított név",
"modify_subjects": "Tantárgyak átnevezése",
"cancel": "Mégse",
"done": "Kész",
"rename_new_subject": "Új Tantárgy átnevezése",
},
"de_de": {
"renamed_subjects": "Umbenannte Fächer",
"rename_subjects": "Fächer umbenennen",
"rename_subject": "Fach umbenennen",
"select_subject": "Fach auswählen",
"modified_name": "Geänderter Name",
"modify_subjects": "Fächer ändern",
"cancel": "Abbrechen",
"done": "Erledigt",
"rename_new_subject": "Neues Fach umbenennen",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}

View File

@@ -0,0 +1,93 @@
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu_item.dart';
import 'package:filcnaplo_premium/models/premium_scopes.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:filcnaplo_premium/ui/mobile/premium/upsell.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.i18n.dart';
import 'package:provider/provider.dart';
class UserMenuNickname extends StatelessWidget {
const UserMenuNickname({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BottomSheetMenuItem(
onPressed: () {
if (!Provider.of<PremiumProvider>(context, listen: false).hasScope(PremiumScopes.nickname)) {
PremiumLockedFeatureUpsell.show(context: context, feature: PremiumFeature.profile);
return;
}
showDialog(context: context, builder: (context) => const UserNicknameEditor());
},
icon: const Icon(FeatherIcons.edit2),
title: Text("edit_nickname".i18n),
);
}
}
class UserNicknameEditor extends StatefulWidget {
const UserNicknameEditor({Key? key}) : super(key: key);
@override
State<UserNicknameEditor> createState() => _UserNicknameEditorState();
}
class _UserNicknameEditorState extends State<UserNicknameEditor> {
final _userName = TextEditingController();
late final UserProvider user;
@override
void initState() {
super.initState();
user = Provider.of<UserProvider>(context, listen: false);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text("change_username".i18n),
content: TextField(
controller: _userName,
autofocus: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
label: Text(user.name!),
suffixIcon: IconButton(
icon: const Icon(FeatherIcons.x),
onPressed: () {
setState(() {
_userName.text = "";
});
},
),
),
),
actions: [
TextButton(
child: Text(
"cancel".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () {
Navigator.of(context).maybePop();
},
),
TextButton(
child: Text(
"done".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () {
user.user!.nickname = _userName.text.trim();
Provider.of<DatabaseProvider>(context, listen: false).store.storeUser(user.user!);
Provider.of<UserProvider>(context, listen: false).refresh();
Navigator.of(context).pop(true);
},
),
],
);
}
}

View File

@@ -0,0 +1,208 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:filcnaplo/api/providers/database_provider.dart';
import 'package:filcnaplo/api/providers/user_provider.dart';
import 'package:filcnaplo_mobile_ui/common/bottom_sheet_menu/bottom_sheet_menu_item.dart';
import 'package:filcnaplo_premium/models/premium_scopes.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:filcnaplo_mobile_ui/screens/settings/settings_screen.i18n.dart';
import 'package:provider/provider.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image_crop/image_crop.dart';
class UserMenuProfilePic extends StatelessWidget {
const UserMenuProfilePic({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
if (!Provider.of<PremiumProvider>(context).hasScope(PremiumScopes.nickname)) {
return const SizedBox();
}
return BottomSheetMenuItem(
onPressed: () {
showDialog(context: context, builder: (context) => const UserProfilePicEditor());
},
icon: const Icon(FeatherIcons.camera),
title: Text("edit_profile_picture".i18n),
);
}
}
class UserProfilePicEditor extends StatefulWidget {
const UserProfilePicEditor({Key? key}) : super(key: key);
@override
State<UserProfilePicEditor> createState() => _UserProfilePicEditorState();
}
class _UserProfilePicEditorState extends State<UserProfilePicEditor> {
late final UserProvider user;
final cropKey = GlobalKey<CropState>();
File? _file;
File? _sample;
File? _lastCropped;
File? image;
Future pickImage() async {
try {
final image = await ImagePicker().pickImage(source: ImageSource.gallery);
if (image == null) return;
File imageFile = File(image.path);
final sample = await ImageCrop.sampleImage(
file: imageFile,
preferredSize: context.size!.longestSide.ceil(),
);
_sample?.delete();
_file?.delete();
setState(() {
_sample = sample;
_file = imageFile;
});
} on PlatformException catch (e) {
log('Failed to pick image: $e');
}
}
Widget cropImageWidget() {
return SizedBox(
height: 300,
child: Crop.file(
_sample!,
key: cropKey,
aspectRatio: 1.0,
));
}
Widget openImageWidget() {
return InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14.0),
),
onTap: () => pickImage(),
child: Container(
decoration: BoxDecoration(border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(14.0)),
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 8.0),
child: Column(
children: [
Text(
"click_here".i18n,
style: const TextStyle(fontSize: 22.0, fontWeight: FontWeight.w600),
),
Text(
"select_profile_picture".i18n,
style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500),
)
],
),
),
);
}
Future<void> _cropImage() async {
final scale = cropKey.currentState!.scale;
final area = cropKey.currentState!.area;
if (area == null || _file == null) {
return;
}
final sample = await ImageCrop.sampleImage(
file: _file!,
preferredSize: (2000 / scale).round(),
);
final file = await ImageCrop.cropImage(
file: sample,
area: area,
);
sample.delete();
_lastCropped?.delete();
_lastCropped = file;
List<int> imageBytes = await _lastCropped!.readAsBytes();
String base64Image = base64Encode(imageBytes);
user.user!.picture = base64Image;
Provider.of<DatabaseProvider>(context, listen: false).store.storeUser(user.user!);
Provider.of<UserProvider>(context, listen: false).refresh();
debugPrint('$file');
}
@override
void initState() {
super.initState();
user = Provider.of<UserProvider>(context, listen: false);
}
@override
void dispose() {
super.dispose();
_file?.delete();
_sample?.delete();
_lastCropped?.delete();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(14.0))),
contentPadding: const EdgeInsets.only(top: 10.0),
title: Text("edit_profile_picture".i18n),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: _sample == null ? openImageWidget() : cropImageWidget(),
),
if (user.user!.picture != "")
TextButton(
child: Text(
"remove_profile_picture".i18n,
style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.red),
),
onPressed: () {
user.user!.picture = "";
Provider.of<DatabaseProvider>(context, listen: false).store.storeUser(user.user!);
Provider.of<UserProvider>(context, listen: false).refresh();
Navigator.of(context).pop(true);
},
),
],
),
actions: [
TextButton(
child: Text(
"cancel".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () {
Navigator.of(context).maybePop();
},
),
TextButton(
child: Text(
"done".i18n,
style: const TextStyle(fontWeight: FontWeight.w500),
),
onPressed: () async {
await _cropImage();
Navigator.of(context).pop(true);
},
),
],
);
}
}

View File

@@ -0,0 +1,657 @@
import 'package:filcnaplo/models/settings.dart';
import 'package:filcnaplo/theme/colors/accent.dart';
import 'package:filcnaplo/theme/colors/colors.dart';
import 'package:filcnaplo/theme/observer.dart';
import 'package:filcnaplo/ui/widgets/grade/grade_tile.dart';
import 'package:filcnaplo/ui/widgets/message/message_tile.dart';
import 'package:filcnaplo_kreta_api/models/grade.dart';
import 'package:filcnaplo_kreta_api/models/homework.dart';
import 'package:filcnaplo_kreta_api/models/message.dart';
import 'package:filcnaplo_mobile_ui/common/filter_bar.dart';
import 'package:filcnaplo_mobile_ui/common/panel/panel.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/grade/new_grades.dart';
import 'package:filcnaplo_mobile_ui/common/widgets/homework/homework_tile.dart';
import 'package:filcnaplo_premium/models/premium_scopes.dart';
import 'package:filcnaplo_premium/providers/premium_provider.dart';
import 'package:filcnaplo_premium/ui/mobile/flutter_colorpicker/colorpicker.dart';
import 'package:filcnaplo_premium/ui/mobile/premium/upsell.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'theme.i18n.dart';
class PremiumCustomAccentColorSetting extends StatefulWidget {
const PremiumCustomAccentColorSetting({Key? key}) : super(key: key);
@override
State<PremiumCustomAccentColorSetting> createState() => _PremiumCustomAccentColorSettingState();
}
enum CustomColorMode { theme, accent, background, highlight }
class _PremiumCustomAccentColorSettingState extends State<PremiumCustomAccentColorSetting> with TickerProviderStateMixin {
late final SettingsProvider settings;
bool colorSelection = false;
bool customColorMenu = false;
CustomColorMode colorMode = CustomColorMode.theme;
final customColorInput = TextEditingController();
final unknownColor = Colors.black;
late TabController _testTabController;
late TabController _colorsTabController;
late AnimationController _openAnimController;
late final Animation<double> backgroundAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _openAnimController,
curve: const Interval(0.2, 1.0, curve: Curves.easeInOut),
),
);
late final Animation<double> fullPageAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _openAnimController,
curve: const Interval(0.0, 0.6, curve: Curves.easeInOut),
),
);
late final Animation<double> backContainerAnimation = Tween<double>(begin: 100, end: 0).animate(
CurvedAnimation(
parent: _openAnimController,
curve: const Interval(0.0, 0.9, curve: Curves.easeInOut),
),
);
late final Animation<double> backContentAnimation = Tween<double>(begin: 100, end: 0).animate(
CurvedAnimation(
parent: _openAnimController,
curve: const Interval(0.2, 1.0, curve: Curves.easeInOut),
),
);
late final Animation<double> backContentScaleAnimation = Tween<double>(begin: 0.8, end: 0.9).animate(
CurvedAnimation(
parent: _openAnimController,
curve: const Interval(0.45, 1.0, curve: Curves.easeInOut),
),
);
late final Animation<double> pickerContainerAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _openAnimController,
curve: const Interval(0.25, 0.8, curve: Curves.easeInOut),
),
);
@override
void initState() {
super.initState();
_colorsTabController = TabController(length: 4, vsync: this);
_testTabController = TabController(length: 4, vsync: this);
settings = Provider.of<SettingsProvider>(context, listen: false);
_openAnimController = AnimationController(vsync: this, duration: const Duration(milliseconds: 750));
_openAnimController.forward();
}
@override
void dispose() {
_openAnimController.dispose();
super.dispose();
}
void setTheme(ThemeMode mode, bool store) async {
await settings.update(theme: mode, store: store);
Provider.of<ThemeModeObserver>(context, listen: false).changeTheme(mode, updateNavbarColor: false);
}
Color? getCustomColor() {
switch (colorMode) {
case CustomColorMode.theme:
return accentColorMap[settings.accentColor];
case CustomColorMode.background:
return settings.customBackgroundColor;
case CustomColorMode.highlight:
return settings.customHighlightColor;
case CustomColorMode.accent:
return settings.customAccentColor;
}
}
void updateCustomColor(Color v, bool store) {
if (colorMode != CustomColorMode.theme) settings.update(accentColor: AccentColor.custom, store: store);
switch (colorMode) {
case CustomColorMode.theme:
settings.update(
accentColor: accentColorMap.keys.firstWhere((element) => accentColorMap[element] == v, orElse: () => AccentColor.filc), store: store);
settings.update(customBackgroundColor: AppColors.of(context).background, store: store);
settings.update(customHighlightColor: AppColors.of(context).highlight, store: store);
settings.update(customAccentColor: v, store: store);
break;
case CustomColorMode.background:
settings.update(customBackgroundColor: v, store: store);
break;
case CustomColorMode.highlight:
settings.update(customHighlightColor: v, store: store);
break;
case CustomColorMode.accent:
settings.update(customAccentColor: v, store: store);
break;
}
}
@override
Widget build(BuildContext context) {
bool hasAccess = Provider.of<PremiumProvider>(context).hasScope(PremiumScopes.customColors);
bool isBackgroundDifferent = Theme.of(context).colorScheme.background != AppColors.of(context).background;
ThemeMode currentTheme = Theme.of(context).brightness == Brightness.light ? ThemeMode.light : ThemeMode.dark;
return WillPopScope(
onWillPop: () async {
Provider.of<ThemeModeObserver>(context, listen: false).changeTheme(settings.theme, updateNavbarColor: true);
return true;
},
child: AnimatedBuilder(
animation: _openAnimController,
builder: (context, child) {
final backgroundGradientBottomColor = isBackgroundDifferent
? Theme.of(context).colorScheme.background
: HSVColor.fromColor(Theme.of(context).colorScheme.background)
.withValue(currentTheme == ThemeMode.dark ? 0.1 * _openAnimController.value : 1.0 - (0.1 * _openAnimController.value))
.withAlpha(1.0)
.toColor();
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: backgroundGradientBottomColor,
));
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [0.0, 0.75],
colors: isBackgroundDifferent
? [
Theme.of(context)
.colorScheme
.background
.withOpacity(1 - ((currentTheme == ThemeMode.dark ? 0.65 : 0.45) * backgroundAnimation.value)),
backgroundGradientBottomColor,
]
: [backgroundGradientBottomColor, backgroundGradientBottomColor],
),
),
child: Opacity(
opacity: fullPageAnimation.value,
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
surfaceTintColor: Theme.of(context).scaffoldBackgroundColor,
leading: BackButton(color: AppColors.of(context).text),
title: Text(
"Preview",
style: TextStyle(color: AppColors.of(context).text),
),
backgroundColor: Colors.transparent,
elevation: 0,
),
body: Stack(
children: [
Opacity(
opacity: 1 - backContainerAnimation.value * (1 / 100),
child: Transform.translate(
offset: Offset(0, backContainerAnimation.value),
child: Container(
height: double.infinity,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: const [
0.35,
0.75
], colors: [
Theme.of(context).colorScheme.background,
isBackgroundDifferent
? HSVColor.fromColor(Theme.of(context).colorScheme.background)
.withSaturation(
(HSVColor.fromColor(Theme.of(context).colorScheme.background).saturation - 0.15).clamp(0.0, 1.0))
.toColor()
: backgroundGradientBottomColor,
]),
),
margin: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
),
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: SizedBox(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.only(top: 24.0),
child: Opacity(
opacity: 1 - backContentAnimation.value * (1 / 100),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Transform.translate(
offset: Offset(0, -24 + backContentAnimation.value),
child: Transform.scale(
scale: backContentScaleAnimation.value,
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 6.0),
child: FilterBar(
items: const [
Tab(text: "All"),
Tab(text: "Grades"),
Tab(text: "Messages"),
Tab(text: "Absences"),
],
controller: _testTabController,
padding: EdgeInsets.zero,
censored: true,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 8.0),
child: NewGradesSurprise(
[
Grade.fromJson(
{
"Uid": "0,Ertekeles",
"RogzitesDatuma": "2022-01-01T23:00:00Z",
"KeszitesDatuma": "2022-01-01T23:00:00Z",
"LattamozasDatuma": null,
"Tantargy": {
"Uid": "0",
"Nev": "Filc szakirodalom",
"Kategoria": {"Uid": "0,_", "Nev": "_", "Leiras": "Nem mondom meg"},
"SortIndex": 2
},
"Tema": "Kupak csomag vásárlás vizsga",
"Tipus": {
"Uid": "0,_",
"Nev": "_",
"Leiras": "Évközi jegy/értékelés",
},
"Mod": {
"Uid": "0,_",
"Nev": "_",
"Leiras": "_ feladat",
},
"ErtekFajta": {
"Uid": "1,Osztalyzat",
"Nev": "Osztalyzat",
"Leiras": "Elégtelen (1) és Jeles (5) között az öt alapértelmezett érték"
},
"ErtekeloTanarNeve": "Premium",
"Jelleg": "Ertekeles",
"SzamErtek": 5,
"SzovegesErtek": "Jeles(5)",
"SulySzazalekErteke": 100,
"SzovegesErtekelesRovidNev": null,
"OsztalyCsoport": {"Uid": "0"},
"SortIndex": 2
},
),
],
censored: true,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 6.0),
child: Panel(
child: GradeTile(
Grade.fromJson(
{
"Uid": "0,Ertekeles",
"RogzitesDatuma": "2022-01-01T23:00:00Z",
"KeszitesDatuma": "2022-01-01T23:00:00Z",
"LattamozasDatuma": null,
"Tantargy": {
"Uid": "0",
"Nev": "Filc szakosztály",
"Kategoria": {"Uid": "0,_", "Nev": "_", "Leiras": "Nem mondom meg"},
"SortIndex": 2
},
"Tema": "Kupak csomag vásárlás vizsga",
"Tipus": {
"Uid": "0,_",
"Nev": "_",
"Leiras": "Évközi jegy/értékelés",
},
"Mod": {
"Uid": "0,_",
"Nev": "_",
"Leiras": "_ feladat",
},
"ErtekFajta": {
"Uid": "1,Osztalyzat",
"Nev": "Osztalyzat",
"Leiras": "Elégtelen (1) és Jeles (5) között az öt alapértelmezett érték"
},
"ErtekeloTanarNeve": "Premium",
"Jelleg": "Ertekeles",
"SzamErtek": 5,
"SzovegesErtek": "Jeles(5)",
"SulySzazalekErteke": 100,
"SzovegesErtekelesRovidNev": null,
"OsztalyCsoport": {"Uid": "0"},
"SortIndex": 2
},
),
padding: EdgeInsets.zero,
censored: true,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 6.0),
child: Panel(
child: HomeworkTile(
Homework.fromJson(
{
"Uid": "0",
"Tantargy": {
"Uid": "0",
"Nev": "Filc premium előnyei",
"Kategoria": {
"Uid": "0,_",
"Nev": "_",
"Leiras": "Filc premium előnyei",
},
"SortIndex": 0
},
"TantargyNeve": "Filc premium előnyei",
"RogzitoTanarNeve": "Kupak János",
"Szoveg": "45 perc filctollal való rajzolás",
"FeladasDatuma": "2022-01-01T23:00:00Z",
"HataridoDatuma": "2022-01-01T23:00:00Z",
"RogzitesIdopontja": "2022-01-01T23:00:00Z",
"IsTanarRogzitette": true,
"IsTanuloHaziFeladatEnabled": false,
"IsMegoldva": false,
"IsBeadhato": false,
"OsztalyCsoport": {"Uid": "0"},
"IsCsatolasEngedelyezes": false
},
),
padding: EdgeInsets.zero,
censored: true,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 6.0),
child: Panel(
child: MessageTile(
Message.fromJson(
{
"azonosito": 0,
"isElolvasva": true,
"isToroltElem": false,
"tipus": {
"azonosito": 1,
"kod": "BEERKEZETT",
"rovidNev": "Beérkezett üzenet",
"nev": "Beérkezett üzenet",
"leiras": "Beérkezett üzenet"
},
"uzenet": {
"azonosito": 0,
"kuldesDatum": "2022-01-01T23:00:00",
"feladoNev": "Filc Napló",
"feladoTitulus": "Nagyon magas szintű személy",
"szoveg":
"<p>Kedves Felhasználó!</p><p><br></p><p>A prémium vásárlásakor kapott filctollal 90%-al több esély van jó jegyek szerzésére.</p>",
"targy": "Filctoll használati útmutató",
"statusz": {
"azonosito": 2,
"kod": "KIKULDVE",
"rovidNev": "Kiküldve",
"nev": "Kiküldve",
"leiras": "Kiküldve"
},
"cimzettLista": [
{
"azonosito": 0,
"kretaAzonosito": 0,
"nev": "Tinta Józsi",
"tipus": {
"azonosito": 0,
"kod": "TANULO",
"rovidNev": "Tanuló",
"nev": "Tanuló",
"leiras": "Tanuló"
}
},
],
"csatolmanyok": [
{"azonosito": 0, "fajlNev": "Filctoll.doc"}
]
}
},
),
censored: true,
),
),
),
],
),
),
),
),
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Wrap(
children: [
Opacity(
opacity: pickerContainerAnimation.value,
child: SizedBox(
width: double.infinity,
child: Container(
padding: const EdgeInsets.only(bottom: 12.0),
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: backgroundGradientBottomColor,
offset: const Offset(0, -4),
blurRadius: 16,
spreadRadius: 12,
),
],
gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: const [
0.0,
0.175
], colors: [
backgroundGradientBottomColor,
backgroundGradientBottomColor,
]),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: FilterBar(
items: [
ColorTab(
color: accentColorMap[settings.accentColor] ?? unknownColor,
tab: Tab(text: "colorpicker_presets".i18n)),
ColorTab(
unlocked: hasAccess,
color: settings.customBackgroundColor ?? unknownColor,
tab: Tab(text: "colorpicker_background".i18n)),
ColorTab(
unlocked: hasAccess,
color: settings.customHighlightColor ?? unknownColor,
tab: Tab(text: "colorpicker_panels".i18n)),
ColorTab(
unlocked: hasAccess,
color: settings.customAccentColor ?? unknownColor,
tab: Tab(text: "colorpicker_accent".i18n)),
],
onTap: (index) {
if (!hasAccess) {
index = 0;
_colorsTabController.animateTo(0, duration: Duration.zero);
PremiumLockedFeatureUpsell.show(context: context, feature: PremiumFeature.customcolors);
}
switch (index) {
case 0:
setState(() {
colorMode = CustomColorMode.theme;
});
break;
case 1:
setState(() {
colorMode = CustomColorMode.background;
});
break;
case 2:
setState(() {
colorMode = CustomColorMode.highlight;
});
break;
case 3:
setState(() {
colorMode = CustomColorMode.accent;
});
break;
}
},
controller: _colorsTabController,
padding: EdgeInsets.zero,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: SafeArea(
child: FilcColorPicker(
colorMode: colorMode,
pickerColor: colorMode == CustomColorMode.accent
? settings.customAccentColor ?? unknownColor
: colorMode == CustomColorMode.background
? settings.customBackgroundColor ?? unknownColor
: colorMode == CustomColorMode.theme
? (accentColorMap[settings.accentColor] ?? AppColors.of(context).text) // idk what else
: settings.customHighlightColor ?? unknownColor,
onColorChanged: (c) {
setState(() {
updateCustomColor(c, false);
});
setTheme(settings.theme, false);
},
onColorChangeEnd: (c, {adaptive}) {
setState(() {
if (adaptive == true) {
settings.update(accentColor: AccentColor.adaptive);
settings.update(customBackgroundColor: AppColors.of(context).background, store: true);
settings.update(customHighlightColor: AppColors.of(context).highlight, store: true);
} else {
updateCustomColor(c, true);
}
});
setTheme(settings.theme, true);
},
),
),
),
],
),
),
),
),
],
),
),
],
),
),
),
);
},
),
);
}
}
class ColorTab extends StatelessWidget {
const ColorTab({Key? key, required this.tab, required this.color, this.unlocked = true}) : super(key: key);
final Tab tab;
final Color color;
final bool unlocked;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Transform.translate(
offset: const Offset(-3, 1),
child: unlocked
? Container(
width: 15,
height: 15,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
border: Border.all(color: Colors.black, width: 2.0),
),
)
: const Padding(
padding: EdgeInsets.symmetric(horizontal: 2),
child: Icon(Icons.lock, color: Color.fromARGB(255, 82, 82, 82), size: 18),
),
),
tab
],
);
}
}
class PremiumColorPickerItem extends StatelessWidget {
const PremiumColorPickerItem({Key? key, required this.label, this.onTap, required this.color}) : super(key: key);
final String label;
final void Function()? onTap;
final Color color;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: InkWell(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
label,
style: TextStyle(color: AppColors.of(context).text, fontWeight: FontWeight.w500),
),
),
Container(
width: 30,
height: 30,
decoration: BoxDecoration(color: color, shape: BoxShape.circle, border: Border.all()),
),
],
),
),
onTap: onTap,
),
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:i18n_extension/i18n_extension.dart';
extension SettingsLocalization on String {
static final _t = Translations.byLocale("hu_hu") +
{
"en_en": {
"colorpicker_presets": "Presets",
"colorpicker_background": "Background",
"colorpicker_panels": "Panels",
"colorpicker_accent": "Accent",
"need_sub": "You need Kupak subscription to use modify this.",
},
"hu_hu": {
"colorpicker_presets": "Téma",
"colorpicker_background": "Háttér",
"colorpicker_panels": "Panelek",
"colorpicker_accent": "Színtónus",
"need_sub": "A módosításhoz Kupak szintű támogatás szükséges.",
},
"de_de": {
"colorpicker_presets": "Farben",
"colorpicker_background": "Hintergrund",
"colorpicker_panels": "Tafeln",
"colorpicker_accent": "Akzent",
"need_sub": "Sie benötigen ein Kupak-Abonnement, um diese Funktion zu ändern.",
},
};
String get i18n => localize(this, _t);
String fill(List<Object> params) => localizeFill(this, params);
String plural(int value) => localizePlural(value, this, _t);
String version(Object modifier) => localizeVersion(modifier, this, _t);
}