Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Copy this file to .env.dev or .env.prod and adjust variables accordingly
API_BASE_URL=https://api.shikkanime.fr
BASE_URL=https://www.shikkanime.fr
5 changes: 5 additions & 0 deletions lib/core/config/env_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ abstract final class EnvConfig {
'API_BASE_URL',
defaultValue: 'http://localhost:37100/api',
);

static const baseUrl = String.fromEnvironment(
'BASE_URL',
defaultValue: 'http://localhost:37100',
);
}
11 changes: 11 additions & 0 deletions lib/core/logger/app_logger.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'package:flutter/foundation.dart';

class AppLogger {
static void print(String message) {
final entry = '${DateTime.now().toIso8601String()} - $message';

if (kDebugMode) {
debugPrint(entry);
}
}
}
8 changes: 5 additions & 3 deletions lib/core/network/http_client.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'dart:convert';

import 'package:application/core/config/env_config.dart';
import 'package:application/core/logger/app_logger.dart';
import 'package:application/core/network/api_result.dart';
import 'package:material_ui/material_ui.dart';
import 'package:http/http.dart' as http;

class HttpClient {
Expand Down Expand Up @@ -42,13 +42,15 @@ class HttpClient {
try {
final response = await request().timeout(timeout);
stopWatch.stop();
debugPrint(
AppLogger.print(
'Request to ${response.request?.url} took ${stopWatch.elapsedMilliseconds} ms',
);
return _checkStatus(response);
} on Exception catch (e) {
stopWatch.stop();
debugPrint('Request failed after ${stopWatch.elapsedMilliseconds} ms');
AppLogger.print(
'Request failed after ${stopWatch.elapsedMilliseconds} ms',
);
return ApiFailure(e.toString());
}
}
Expand Down
34 changes: 34 additions & 0 deletions lib/core/services/platform_launch_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'package:application/core/logger/app_logger.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';

class PlatformLaunchService {
const PlatformLaunchService();

static const _modes = <LaunchMode>[
.externalNonBrowserApplication,
.externalApplication,
.platformDefault,
];

Future<bool> launch(String rawUrl) async {
final Uri? uri = Uri.tryParse(rawUrl);

if (uri == null || !uri.isScheme('https')) {
AppLogger.print('Invalid URL: $rawUrl');
return false;
}

for (final mode in _modes) {
try {
if (await launchUrl(uri, mode: mode)) {
return true;
}
} on PlatformException catch (e) {
AppLogger.print('Failed to launch URL with mode $mode: $e');
}
}

return false;
}
}
5 changes: 5 additions & 0 deletions lib/core/theme/app_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ sealed class AppTheme {
canvasColor: Colors.white,
textColor: Colors.black,
inverseTextColor: Colors.white,
warningTextColor: Colors.black,
greyColor: Colors.grey[800]!,
snackBarBackgroundColor: Colors.white,
elevatedButtonBackgroundColor: Colors.white,
Expand All @@ -24,6 +25,7 @@ sealed class AppTheme {
canvasColor: const Color(0xff161616),
textColor: Colors.white,
inverseTextColor: Colors.black,
warningTextColor: Colors.black,
greyColor: Colors.grey[400]!,
snackBarBackgroundColor: Colors.grey[900]!,
elevatedButtonBackgroundColor: const Color(0xff282828),
Expand All @@ -38,6 +40,7 @@ sealed class AppTheme {
required Color canvasColor,
required Color textColor,
required Color inverseTextColor,
required Color warningTextColor,
required Color greyColor,
required Color snackBarBackgroundColor,
required Color elevatedButtonBackgroundColor,
Expand All @@ -61,6 +64,7 @@ sealed class AppTheme {
extensions: [
AppThemeExtension(
inverseTextColor: inverseTextColor,
warningTextColor: warningTextColor,
iconImage: iconImage,
),
],
Expand All @@ -87,6 +91,7 @@ sealed class AppTheme {
),
bodyMedium: TextStyle(color: greyColor),
bodySmall: TextStyle(color: greyColor, fontSize: bodySmallFontSize),
labelSmall: TextStyle(color: textColor, fontSize: bodySmallFontSize),
),
iconTheme: IconThemeData(color: greyColor),
snackBarTheme: SnackBarThemeData(
Expand Down
9 changes: 9 additions & 0 deletions lib/core/theme/app_theme_colors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ import 'package:material_ui/material_ui.dart';
class AppThemeExtension extends ThemeExtension<AppThemeExtension> {
const AppThemeExtension({
required this.inverseTextColor,
required this.warningTextColor,
required this.iconImage,
});

final Color inverseTextColor;
final Color warningTextColor;
final ImageProvider iconImage;

@override
ThemeExtension<AppThemeExtension> copyWith({
Color? inverseTextColor,
Color? warningTextColor,
ImageProvider? iconImage,
}) {
return AppThemeExtension(
inverseTextColor: inverseTextColor ?? this.inverseTextColor,
warningTextColor: warningTextColor ?? this.warningTextColor,
iconImage: iconImage ?? this.iconImage,
);
}
Expand All @@ -36,6 +40,11 @@ class AppThemeExtension extends ThemeExtension<AppThemeExtension> {
other.inverseTextColor,
t,
)!,
warningTextColor: Color.lerp(
warningTextColor,
other.warningTextColor,
t,
)!,
iconImage: iconImage,
);
}
Expand Down
10 changes: 7 additions & 3 deletions lib/core/widgets/app_blur_badge.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ class AppBlurBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
final badgeBackgroundColor = Theme.of(context).scaffoldBackgroundColor
.withValues(alpha: 0.3);
.withValues(alpha: 0.4);

return ClipRRect(
borderRadius: const .all(.circular(16)),
borderRadius: const .all(.circular(8)),
child: BackdropFilter(
filter: .blur(sigmaX: 8, sigmaY: 8),
child: DecoratedBox(
decoration: BoxDecoration(color: badgeBackgroundColor),
decoration: BoxDecoration(
color: badgeBackgroundColor,
border: .all(color: badgeBackgroundColor, width: 0.5),
borderRadius: const .all(.circular(8)),
),
child: Padding(
padding: const .symmetric(horizontal: 8, vertical: 4),
child: child,
Expand Down
85 changes: 32 additions & 53 deletions lib/core/widgets/app_elevated_dropdown_button.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import 'package:application/core/widgets/show_app_menu.dart';
import 'package:material_ui/material_ui.dart';

class AppElevatedDropdownButton<T> extends StatelessWidget {
const AppElevatedDropdownButton(
this._globalKey, {
const AppElevatedDropdownButton({
super.key,
this.value,
required this.items,
required this.onChanged,
});

final GlobalKey _globalKey;
final T? value;
final List<AppElevatedPopupMenuEntry<T>> items;
final ValueChanged<T> onChanged;
Expand All @@ -26,57 +25,37 @@ class AppElevatedDropdownButton<T> extends StatelessWidget {
Widget build(BuildContext context) {
final selectedItem = _getSelectedItem();

return ElevatedButton(
key: _globalKey,
onPressed: () async {
final renderBox =
_globalKey.currentContext?.findRenderObject() as RenderBox?;
final overlayBox =
Navigator.of(context).overlay?.context.findRenderObject()
as RenderBox?;

if (renderBox == null || overlayBox == null || items.isEmpty) return;

final topLeft = renderBox.localToGlobal(.zero, ancestor: overlayBox);
final bottomRight = renderBox.localToGlobal(
renderBox.size.bottomRight(.zero),
ancestor: overlayBox,
);
final maxHeight = MediaQuery.heightOf(context) * 0.3;
final menuHeight = (items.length * kMinInteractiveDimension)
.clamp(0.0, maxHeight)
.toDouble();
final menuWidth = renderBox.size.width < 240
? 240.0
: renderBox.size.width;

final selection = await showMenu<_MenuSelection<T>>(
context: context,
position: .fromRect(
.fromPoints(topLeft, bottomRight),
Offset.zero & overlayBox.size,
),
constraints: .tightFor(width: menuWidth),
items: [
_LazyPopupMenuEntry<T>(
height: menuHeight,
selectedValue: value,
items: items,
),
return Builder(
builder: (context) => ElevatedButton(
onPressed: () async {
final maxHeight = MediaQuery.heightOf(context) * 0.3;
final menuHeight = (items.length * kMinInteractiveDimension)
.clamp(0.0, maxHeight)
.toDouble();

final selection = await showAppPopupMenu<_MenuSelection<T>>(
context: context,
items: [
_LazyPopupMenuEntry<T>(
height: menuHeight,
selectedValue: value,
items: items,
),
],
);

if (selection != null) {
onChanged(selection.value);
}
},
child: Flex(
spacing: 8,
direction: .horizontal,
children: [
if (selectedItem != null) selectedItem.child,
const Icon(Icons.arrow_drop_down),
],
);

if (selection != null) {
onChanged(selection.value);
}
},
child: Flex(
spacing: 8,
direction: .horizontal,
children: [
if (selectedItem != null) selectedItem.child,
const Icon(Icons.arrow_drop_down),
],
),
),
);
}
Expand Down
83 changes: 83 additions & 0 deletions lib/core/widgets/platforms/available_platforms_badge.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'package:application/core/widgets/app_blur_badge.dart';
import 'package:application/core/widgets/platforms/platform_image.dart';
import 'package:application/core/widgets/platforms/platforms_stack.dart';
import 'package:application/core/widgets/show_app_menu.dart';
import 'package:application/l10n/app_localizations.dart';
import 'package:application/models/platform_model.dart';
import 'package:application/models/source_model.dart';
import 'package:material_ui/material_ui.dart';

class AvailablePlatformsBadge extends StatelessWidget {
const AvailablePlatformsBadge({
super.key,
required this.sources,
required this.onSourcePress,
});

final Iterable<SourceModel> sources;
final Future<void> Function(SourceModel source) onSourcePress;

Set<PlatformModel> get _platforms =>
sources.map((source) => source.platform).toSet();

@override
Widget build(BuildContext context) {
final platforms = _platforms;
final labelSmall = Theme.of(context).textTheme.labelSmall;

return Positioned(
left: 8,
bottom: 8,
child: Builder(
builder: (context) => GestureDetector(
onTap: () {
if (platforms.length == 1) {
onSourcePress(sources.first);
return;
}

showAppPopupMenu(
context: context,
items: [
for (final platform in platforms)
PopupMenuItem(
child: ListTile(
leading: PlatformImage(platform, width: 16, height: 16),
title: Text(platform.name),
trailing: const Icon(Icons.north_east),
),
onTap: () {
for (final source in sources) {
if (source.platform.name == platform.name) {
onSourcePress(source);
break;
}
}
},
),
],
);
},
child: AppBlurBadge(
child: Flex(
direction: .horizontal,
spacing: 4,
children: [
Icon(
Icons.open_in_new,
color: labelSmall?.color,
size: labelSmall?.fontSize,
),
Text(
AppLocalizations.of(context)!.availableOn,
style: labelSmall,
),
PlatformsStack(platforms: platforms),
],
),
),
),
),
);
}
}
Loading
Loading