Frontend first draft

This commit is contained in:
Arseni
2025-11-13 15:06:15 +03:00
parent e47f343afb
commit ddb54ddfdc
504 changed files with 25498 additions and 1 deletions

View File

@@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pshared/utils/snackbar.dart';
Future<void> copyToClipboard(BuildContext context, String text, String hint, {int delaySeconds = 3}) async {
final res = Clipboard.setData(ClipboardData(text: text));
notifyUser(context, hint, delaySeconds: delaySeconds);
return res;
}

View File

@@ -0,0 +1,22 @@
String currencyCodeToSymbol(String currencyCode) {
switch (currencyCode) {
case 'USD':
return '\$';
case 'PLN':
return '';
case 'EUR':
return '';
case 'GBP':
return '£';
case 'HUF':
return 'Ft';
case 'RUB':
return '';
default:
return currencyCode;
}
}
String currencyToString(String currencyCode, double amount) {
return '${currencyCodeToSymbol(currencyCode)}\u00A0${amount.toStringAsFixed(2)}';
}

View File

@@ -0,0 +1,9 @@
extension DateTimeSerializer on DateTime {
static String toBackendString(DateTime dt) {
return dt.toUtc().toIso8601String();
}
static DateTime fromBackendString(String dateStr) {
return DateTime.parse(dateStr);
}
}

View File

@@ -0,0 +1,91 @@
import 'package:intl/intl.dart';
import 'package:flutter/material.dart';
import 'package:jovial_svg/jovial_svg.dart';
import 'package:country_flags/country_flags.dart';
String _locale2Flag(Locale l) {
if (l.languageCode == 'en') {
return 'gb';
}
if (l.languageCode == 'uk') {
return 'ua';
}
if (l.languageCode == 'el') {
return 'gr';
}
return l.languageCode;
}
final Map<String, String> localeNames = {
'ca': 'Català',
'en': 'English',
'es': 'Español',
'fr': 'Français',
'de': 'Deutsch',
'uk': 'Українська',
'el': 'Ελληνικά',
'ru': 'Русский',
'pt': 'Português',
'pl': 'Polski',
'it': 'Italiano',
'nl': 'Nederlands',
};
class _CatalanFlag extends StatelessWidget {
final double? width;
final double? height;
final BoxFit? fit;
_CatalanFlag({
required this.width,
required this.height,
required this.fit,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
height: height,
child: ScalableImageWidget.fromSISource(
key: const Key('svgFlagCa'),
si: ScalableImageSource.fromSI(
DefaultAssetBundle.of(context),
'packages/pshared/assets/flag_of_catalonia.si',
),
fit: BoxFit.contain,
),
);
}
}
Widget getCountryFlag(Locale locale, {double? height = 24, double? width = 35}) {
return locale.languageCode.toLowerCase() == 'ca'
? _CatalanFlag(width: width, height: height, fit: BoxFit.contain)
: CountryFlag.fromCountryCode(
_locale2Flag(locale),
height: height,
width: width,
shape: Rectangle(),
);
}
String getLocaleName(Locale locale) {
return localeNames[locale.languageCode] ?? Intl.canonicalizedLocale(locale.toString()).toUpperCase();
}
Widget getLocaleNameWidget(Locale locale) {
return Text(getLocaleName(locale), overflow: TextOverflow.ellipsis);
}
Widget getFlaggedLocale(Locale locale) {
return ListTile(
leading: getCountryFlag(locale),
title: getLocaleNameWidget(locale),
);
}

View File

@@ -0,0 +1,2 @@
export 'client/io.dart'
if (dart.library.html) 'http_client/web.dart';

View File

@@ -0,0 +1,55 @@
import 'dart:io' as io show HttpClient, HttpHeaders;
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
const _sessionCookie = 'session_id';
@override
http.Client buildHttpClient() => _SessionClient(IOClient(io.HttpClient()));
class _SessionClient extends http.BaseClient {
final http.Client _inner;
String? _sessionId;
_SessionClient(this._inner);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
if (_sessionId != null) {
request.headers[io.HttpHeaders.cookieHeader] = '$_sessionCookie=$_sessionId';
}
request.followRedirects = false;
request.maxRedirects = 0;
http.StreamedResponse response = await _inner.send(request);
_captureCookie(response.headers[io.HttpHeaders.setCookieHeader]);
while (response.isRedirect) {
final location = response.headers['location'];
if (location == null) break;
final redirected = http.Request(request.method, Uri.parse(location))
..headers.addAll(request.headers)
..bodyBytes = await response.stream.toBytes()
..followRedirects = false
..maxRedirects = 0;
response = await _inner.send(redirected);
_captureCookie(response.headers[io.HttpHeaders.setCookieHeader]);
}
return response;
}
void _captureCookie(String? setCookieHeader) {
if (setCookieHeader == null) return;
final match = RegExp('$_sessionCookie=([^;]+)',
caseSensitive: false)
.firstMatch(setCookieHeader);
if (match != null) _sessionId = match.group(1);
}
}

View File

@@ -0,0 +1,4 @@
import 'package:http/http.dart';
Client buildHttpClient() => throw UnsupportedError('buildHttpClient() was called without a proper platform implementation.');

View File

@@ -0,0 +1,9 @@
import 'package:http/browser_client.dart';
import 'package:http/http.dart' as http;
@override
http.Client buildHttpClient() {
final bc = BrowserClient();
bc.withCredentials = true;
return bc;
}

View File

@@ -0,0 +1,165 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:pshared/api/responses/file_uploaded.dart';
import 'package:pshared/api/responses/message.dart';
import 'package:pshared/api/responses/error/connectivity.dart';
import 'package:pshared/api/responses/error/server.dart';
import 'package:pshared/config/constants.dart';
Uri _uri(String service, String url) {
// Ensure the base URL ends with a slash
final normalizedBaseUrl = Constants.apiUrl.endsWith('/')
? Constants.apiUrl
: '${Constants.apiUrl}/';
// Remove leading slash from service, if any
final normalizedService = service.startsWith('/') ? service.substring(1) : service;
// Remove leading slash from url, if any
final normalizedUrl = url.startsWith('/') ? url.substring(1) : url;
// Only append a trailing slash to the service
// if the URL is non-empty
final serviceWithOptionalSlash = normalizedUrl.isNotEmpty ? '$normalizedService/' : normalizedService;
return Uri.parse(normalizedBaseUrl).resolve(serviceWithOptionalSlash).resolve(normalizedUrl);
}
Map<String, String> _authHeader(Map<String, String> headers, String? authToken) {
if (authToken != null && authToken.isNotEmpty) {
headers['Authorization'] = 'Bearer $authToken';
}
return headers;
}
Map<String, String> _headers({String? authToken}) {
final headers = {'Content-Type': 'application/json'};
return _authHeader(headers, authToken);
}
Future<http.Response> postRequest(String service, String url, Map<String, dynamic> body, {String? authToken}) async {
final response = await http.post(_uri(service, url),
headers: _headers(authToken: authToken),
body: json.encode(body),
);
return response;
}
Future<http.Response> getRequest(String service, String url, {String? authToken}) async {
final response = await http.get(_uri(service, url),
headers: _headers(authToken: authToken),
);
return response;
}
Future<http.Response> putRequest(String service, String url, Map<String, dynamic> body, {String? authToken}) async {
final response = await http.put(_uri(service, url),
headers: _headers(authToken: authToken),
body: json.encode(body),
);
return response;
}
Future<http.Response> patchRequest(String service, String url, Map<String, dynamic> body, {String? authToken}) async {
final response = await http.patch(_uri(service, url),
headers: _headers(authToken: authToken),
body: json.encode(body),
);
return response;
}
Future<http.Response> deleteRequest(String service, String url, Map<String, dynamic> body, {String? authToken}) async {
final response = await http.delete(_uri(service, url),
headers: _headers(authToken: authToken),
body: json.encode(body),
);
return response;
}
Future<http.StreamedResponse> _fileUploadRequest(String service, String url, String fileName, String fileType, String mediaType, List<int> bytes, {String? authToken}) async {
var request = http.MultipartRequest('POST', _uri(service, url));
var multipartFile = http.MultipartFile.fromBytes(
fileType,
bytes,
contentType: MediaType.parse(mediaType),
filename: fileName,
);
request.files.add(multipartFile);
if (authToken != null && authToken.isNotEmpty) {
request.headers['Authorization'] = 'Bearer $authToken';
}
return request.send();
}
void _throwConnectivityError(http.Response response, Object e) {
throw ConnectivityError(
code: response.statusCode,
message: e is FormatException
? 'Invalid response format. error: ${e.toString()}'
: 'Unknown error occurred, error: ${e.toString()}',
);
}
Future<Map<String, dynamic>> _handleResponse(Future<http.Response> r) async {
late http.Response response;
try {
response = await r;
} catch(e) {
throw ConnectivityError(message: e.toString());
}
late HTTPMessage message;
try {
message = HTTPMessage.fromJson(json.decode(response.body));
} catch(e) {
_throwConnectivityError(response, e);
}
if (response.statusCode < 200 || response.statusCode >= 300) {
late ErrorResponse error;
try {
error = ErrorResponse.fromJson(message.data);
} catch(e) {
_throwConnectivityError(response, e);
}
throw error;
}
return message.data;
}
Future<Map<String, dynamic>> getPOSTResponse(String service, String url, Map<String, dynamic> body, {String? authToken}) async {
return _handleResponse(postRequest(service, url, body, authToken: authToken));
}
Future<Map<String, dynamic>> getGETResponse(String service, String url, {String? authToken}) async {
return _handleResponse(getRequest(service, url, authToken: authToken));
}
Future<Map<String, dynamic>> getPUTResponse(String service, String url, Map<String, dynamic> body, {String? authToken}) async {
return _handleResponse(putRequest(service, url, body, authToken: authToken));
}
Future<Map<String, dynamic>> getPATCHResponse(String service, String url, Map<String, dynamic> body, {String? authToken}) async {
return _handleResponse(patchRequest(service, url, body, authToken: authToken));
}
Future<Map<String, dynamic>> getDELETEResponse(String service, String url, Map<String, dynamic> body, {String? authToken}) async {
return _handleResponse(deleteRequest(service, url, body, authToken: authToken));
}
Future<FileUploaded?> getFileUploadResponse(String service, String url, String fileName, String fileType, String mediaType, List<int> bytes, {String? authToken}) async {
final streamedResponse = await _fileUploadRequest(service, url, fileName, fileType, mediaType, bytes, authToken: authToken);
return FileUploaded.fromJson(await _handleResponse(http.Response.fromStream(streamedResponse)));
}

View File

@@ -0,0 +1,45 @@
import 'package:image/image.dart' as img;
import 'package:share_plus/share_plus.dart';
import 'package:pshared/api/errors/failed_to_read_image.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/utils/image/transformed.dart';
bool _imageTooBig(img.Image image, int maxDimension) {
return image.width > maxDimension && image.height > maxDimension;
}
bool _imageTooSmall(img.Image image, int maxDimension) {
return image.width < maxDimension && image.height < maxDimension;
}
TransformedImage _getImageBytes(img.Image image, int maxDimension) {
List<int> imageBytes;
if (_imageTooBig(image, maxDimension) || _imageTooSmall(image, maxDimension)) {
final double scale = image.width < image.height
? maxDimension / image.width
: maxDimension / image.height;
int newWidth = (image.width * scale).toInt();
int newHeight = (image.height * scale).toInt();
final img.Image resizedImage = img.copyResize(image, width: newWidth, height: newHeight);
image = resizedImage;
}
imageBytes = img.encodePng(image);
return TransformedImage(imageBytes, 'image/png');
}
Future<TransformedImage> defaultTransformImage(XFile file, {int? maxDimension}) async {
maxDimension = maxDimension ?? Constants.defaultDimensionLength;
img.Image? image = img.decodeImage(await file.readAsBytes());
if (image == null) {
throw ErrorFailedToReadImage();
}
return _getImageBytes(image, maxDimension);
}

View File

@@ -0,0 +1,6 @@
class TransformedImage {
final List<int> bytes;
final String imageType;
TransformedImage(this.bytes, this.imageType);
}

View File

@@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/models/settings/localizations.dart' as loc;
import 'package:pshared/provider/locale.dart';
String currentLocale(BuildContext context) => Provider.of<LocaleProvider>(context, listen: false).locale.languageCode;
String _localizedString(
BuildContext context,
String Function(loc.Localizations locs, String locale, {String? fallback}) localizationFunction,
loc.Localizations locs, {
String? fallback,
}) => localizationFunction(locs, currentLocale(context), fallback: fallback);
String _anyLocalizedString(
BuildContext context,
String Function(loc.Localizations locs, String locale, {String? fallback}) localizationFunction,
loc.Localizations locs, {
String? fallback,
}) => localizationFunction(
locs,
currentLocale(context),
fallback: localizationFunction(
locs,
Constants.defaultLocale.languageCode,
fallback: locs.isEmpty
? fallback
: localizationFunction(
locs,
locs.keys.first,
fallback: fallback,
),
),
);
String name(BuildContext context, loc.Localizations locs, {String? fallback}) {
return _localizedString(context, loc.Localization.name, locs, fallback: fallback);
}
String anyName(BuildContext context, loc.Localizations locs, {String? fallback}) {
return _anyLocalizedString(context, loc.Localization.name, locs, fallback: fallback);
}
String hint(BuildContext context, loc.Localizations locs, {String? fallback}) {
return _localizedString(context, loc.Localization.hint, locs, fallback: fallback);
}
String anyHint(BuildContext context, loc.Localizations locs, {String? fallback}) {
return _anyLocalizedString(context, loc.Localization.hint, locs, fallback: fallback);
}
String link(BuildContext context, loc.Localizations locs, {String? fallback}) {
return _localizedString(context, loc.Localization.link, locs, fallback: fallback);
}
String anyLink(BuildContext context, loc.Localizations locs, {String? fallback}) {
return _anyLocalizedString(context, loc.Localization.link, locs, fallback: fallback);
}
String error(BuildContext context, loc.Localizations locs, {String? fallback}) {
return _localizedString(context, loc.Localization.error, locs, fallback: fallback);
}
String anyError(BuildContext context, loc.Localizations locs, {String? fallback}) {
return _anyLocalizedString(context, loc.Localization.error, locs, fallback: fallback);
}
String address(BuildContext context, loc.Localizations locs, {String? fallback}) {
return _localizedString(context, loc.Localization.address, locs, fallback: fallback);
}
String anyAddress(BuildContext context, loc.Localizations locs, {String? fallback}) {
return _anyLocalizedString(context, loc.Localization.address, locs, fallback: fallback);
}
String details(BuildContext context, loc.Localizations locs, {String? fallback}) {
return _localizedString(context, loc.Localization.details, locs, fallback: fallback);
}
String anyDetails(BuildContext context, loc.Localizations locs, {String? fallback}) {
return _anyLocalizedString(context, loc.Localization.details, locs, fallback: fallback);
}
String route(BuildContext context, loc.Localizations locs, {String? fallback}) {
return _localizedString(context, loc.Localization.route, locs, fallback: fallback);
}
String anyRoute(BuildContext context, loc.Localizations locs, {String? fallback}) {
return _anyLocalizedString(context, loc.Localization.route, locs, fallback: fallback);
}
String anyLocationName(BuildContext context, loc.Localizations locs, {String? fallback}) {
return _anyLocalizedString(context, loc.Localization.locationName, locs, fallback: fallback);
}
String translate(BuildContext context, loc.Localizations locs, String key, {String? fallback}) {
return loc.Localization.translate(locs, currentLocale(context), key, fallback: fallback);
}
String anyTranslateFromLang(loc.Localizations locs, String lang, String key, {String? fallback}) =>
loc.Localization.translate(locs, lang, key,
fallback: loc.Localization.translate(locs, Constants.defaultLocale.languageCode, key,
fallback: loc.Localization.translate(locs, locs.keys.first, key, fallback: fallback),
),
);
String anyTranslate(BuildContext context, loc.Localizations locs, String key, {String? fallback}) =>
anyTranslateFromLang(locs, currentLocale(context), key, fallback: fallback);
String dateToLocalFormat(BuildContext context, DateTime dateTime) {
String locale = Provider.of<LocaleProvider>(context, listen: false).locale.toString();
final dateFormat = DateFormat('E, ', locale).add_yMd();
return dateFormat.format(dateTime);
}
String dateTimeToLocalFormat(BuildContext context, DateTime dateTime) {
String locale = Provider.of<LocaleProvider>(context, listen: false).locale.toString();
final dateFormat = DateFormat('E, ', locale).add_yMd().add_jm();
return dateFormat.format(dateTime);
}
String dateTimeToLocalFormatAuto(BuildContext context, DateTime dateTime, bool? dateOnly) {
return (dateOnly ?? false) ? dateToLocalFormat(context, dateTime) : dateTimeToLocalFormat(context, dateTime);
}
String dateTimeOrNullToLocalFormat(BuildContext context, DateTime? dateTime, {String? fallback}) {
if (dateTime == null) return fallback ?? '';
return dateTimeToLocalFormat(context, dateTime);
}

View File

@@ -0,0 +1,17 @@
class NameInitials {
static const unknown = '?';
}
String getNameInitials(String name) {
if (name.isEmpty) return NameInitials.unknown;
// Split the name by whitespace.
final words = name.trim().split(RegExp(r'\s+'));
if (words.isEmpty) return NameInitials.unknown;
// If there's only one word, return its first letter.
if (words.length == 1) return words.first[0].toUpperCase();
// Otherwise, use the first letter of the first and last words.
return (words.first[0] + words.last[0]).toUpperCase();
}

View File

@@ -0,0 +1,7 @@
import 'package:flutter/material.dart';
Rect? sharePositionOrigin(BuildContext context) {
final RenderBox box = context.findRenderObject() as RenderBox;
return box.localToGlobal(Offset.zero) & box.size;
}

View File

@@ -0,0 +1,31 @@
import 'dart:async';
import 'package:flutter/material.dart';
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> notifyUserX(ScaffoldMessengerState sm, String message, { int delaySeconds = 3 })
{
return sm.showSnackBar(
SnackBar(
content: Text(message),
duration: Duration(seconds: delaySeconds),
),
);
}
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> notifyUser(BuildContext context, String message, { int delaySeconds = 3 }) {
return notifyUserX(ScaffoldMessenger.of(context), message, delaySeconds: delaySeconds);
}
Future<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>> postNotifyUser(
BuildContext context, String message, {int delaySeconds = 3}) {
final completer = Completer<ScaffoldFeatureController<SnackBar, SnackBarClosedReason>>();
WidgetsBinding.instance.addPostFrameCallback((_) {
final controller = notifyUser(context, message, delaySeconds: delaySeconds);
completer.complete(controller);
});
return completer.future;
}