- fixed: duplicates on the frontpage - new: search by tag
This commit is contained in:
parent
e945844151
commit
c35308fbc1
@ -30,7 +30,7 @@
|
|||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data android:name="flutter_deeplinking_enabled" android:value="true"/>
|
<meta-data android:name="flutter_deeplinking_enabled" android:value="false"/>
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
@ -28,20 +28,20 @@ class F0ckApp extends ConsumerWidget {
|
|||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/',
|
path: '/',
|
||||||
builder: (context, state) {
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
return const MediaGrid();
|
return const MediaGrid();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/:rest(.*)',
|
path: '/:rest(.*)',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final isInternalLink = (state.extra is bool && state.extra == true);
|
final bool isInternalLink = (state.extra is bool && state.extra == true);
|
||||||
final fullPath = state.matchedLocation;
|
final String fullPath = state.matchedLocation;
|
||||||
|
|
||||||
final regExp = RegExp(
|
final regExp = RegExp(
|
||||||
r'^(?:/tag/(?<tag>.+?))?(?:/(?<mime>image|audio|video))?(?:/(?<itemid>\d+))?$',
|
r'^(?:/tag/(?<tag>.+?))?(?:/(?<mime>image|audio|video))?(?:/(?<itemid>\d+))?$',
|
||||||
);
|
);
|
||||||
final match = regExp.firstMatch(fullPath);
|
final RegExpMatch? match = regExp.firstMatch(fullPath);
|
||||||
|
|
||||||
if (match == null) {
|
if (match == null) {
|
||||||
return const Scaffold(body: Center(child: Text('Ungültiger Link')));
|
return const Scaffold(body: Center(child: Text('Ungültiger Link')));
|
||||||
@ -51,12 +51,12 @@ class F0ckApp extends ConsumerWidget {
|
|||||||
final String? mime = match.namedGroup('mime');
|
final String? mime = match.namedGroup('mime');
|
||||||
final String? idStr = match.namedGroup('itemid');
|
final String? idStr = match.namedGroup('itemid');
|
||||||
final int? itemId = idStr != null ? int.tryParse(idStr) : null;
|
final int? itemId = idStr != null ? int.tryParse(idStr) : null;
|
||||||
const preloadOffset = 50;
|
const int preloadOffset = 50;
|
||||||
|
|
||||||
return Consumer(
|
return Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (BuildContext context, WidgetRef ref, Widget? child) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
final mediaNotifier = ref.read(mediaProvider.notifier);
|
final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
|
||||||
if (!isInternalLink) {
|
if (!isInternalLink) {
|
||||||
mediaNotifier.setType(mime ?? "alles");
|
mediaNotifier.setType(mime ?? "alles");
|
||||||
mediaNotifier.setTag(tag);
|
mediaNotifier.setTag(tag);
|
||||||
@ -79,7 +79,7 @@ class F0ckApp extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final theme = ref.watch(themeNotifierProvider);
|
final ThemeData theme = ref.watch(themeNotifierProvider);
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
routerConfig: _router,
|
routerConfig: _router,
|
||||||
|
15
lib/models/suggestion_model.dart
Normal file
15
lib/models/suggestion_model.dart
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
class Suggestion {
|
||||||
|
final String tag;
|
||||||
|
final int tagged;
|
||||||
|
final double score;
|
||||||
|
|
||||||
|
Suggestion({required this.tag, required this.tagged, required this.score});
|
||||||
|
|
||||||
|
factory Suggestion.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Suggestion(
|
||||||
|
tag: json['tag'].toString(),
|
||||||
|
tagged: int.tryParse(json['tagged'].toString()) ?? 0,
|
||||||
|
score: (json['score'] as num).toDouble(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -102,8 +102,17 @@ class MediaNotifier extends StateNotifier<MediaState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void addMediaItems(List<MediaItem> newItems) {
|
void addMediaItems(List<MediaItem> newItems) {
|
||||||
final updated = List<MediaItem>.from(state.mediaItems)..addAll(newItems);
|
final Set<int> existingIds = state.mediaItems
|
||||||
state = state.replace(mediaItems: updated);
|
.map((item) => item.id)
|
||||||
|
.toSet();
|
||||||
|
final List<MediaItem> filteredItems = newItems
|
||||||
|
.where((item) => !existingIds.contains(item.id))
|
||||||
|
.toList();
|
||||||
|
if (filteredItems.isNotEmpty) {
|
||||||
|
final List<MediaItem> updated = List<MediaItem>.from(state.mediaItems)
|
||||||
|
..addAll(filteredItems);
|
||||||
|
state = state.replace(mediaItems: updated);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadMedia({int? id}) async {
|
Future<void> loadMedia({int? id}) async {
|
||||||
|
@ -56,13 +56,13 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
|||||||
try {
|
try {
|
||||||
await ref.read(mediaProvider.notifier).loadMedia();
|
await ref.read(mediaProvider.notifier).loadMedia();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_showError("Fehler beim Laden der Medien: $e");
|
_showMsg("Fehler beim Laden der Medien: $e");
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => isLoading = false);
|
setState(() => isLoading = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showError(String message) {
|
void _showMsg(String message) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
..removeCurrentSnackBar()
|
..removeCurrentSnackBar()
|
||||||
@ -70,15 +70,15 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _downloadMedia() async {
|
Future<void> _downloadMedia() async {
|
||||||
final mediaState = ref.read(mediaProvider);
|
final MediaState mediaState = ref.read(mediaProvider);
|
||||||
final currentItem = mediaState.mediaItems[_currentIndex];
|
final MediaItem currentItem = mediaState.mediaItems[_currentIndex];
|
||||||
|
|
||||||
if (Platform.isAndroid || Platform.isIOS) {
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
var status = await Permission.storage.status;
|
PermissionStatus status = await Permission.storage.status;
|
||||||
if (!status.isGranted) {
|
if (!status.isGranted) {
|
||||||
status = await Permission.storage.request();
|
status = await Permission.storage.request();
|
||||||
if (!status.isGranted) {
|
if (!status.isGranted) {
|
||||||
_showError("Speicherberechtigung wurde nicht erteilt.");
|
_showMsg("Speicherberechtigung wurde nicht erteilt.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,17 +86,17 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
|||||||
|
|
||||||
String localPath;
|
String localPath;
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
final directory = await getExternalStorageDirectory();
|
final Directory? directory = await getExternalStorageDirectory();
|
||||||
localPath = "${directory!.path}/Download/fApp";
|
localPath = "${directory!.path}/Download/fApp";
|
||||||
} else if (Platform.isIOS) {
|
} else if (Platform.isIOS) {
|
||||||
final directory = await getApplicationDocumentsDirectory();
|
final Directory directory = await getApplicationDocumentsDirectory();
|
||||||
localPath = directory.path;
|
localPath = directory.path;
|
||||||
} else {
|
} else {
|
||||||
final directory = await getTemporaryDirectory();
|
final Directory directory = await getTemporaryDirectory();
|
||||||
localPath = directory.path;
|
localPath = directory.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
final savedDir = Directory(localPath);
|
final Directory savedDir = Directory(localPath);
|
||||||
if (!await savedDir.exists()) {
|
if (!await savedDir.exists()) {
|
||||||
await savedDir.create(recursive: true);
|
await savedDir.create(recursive: true);
|
||||||
}
|
}
|
||||||
@ -111,10 +111,10 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_showError('Download gestartet: ${currentItem.mediaUrl}');
|
_showMsg('Download gestartet: ${currentItem.mediaUrl}');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_showError('Download fehlgeschlagen: $e');
|
_showMsg('Download fehlgeschlagen: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,11 +126,10 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final mediaState = ref.watch(mediaProvider);
|
final MediaState mediaState = ref.watch(mediaProvider);
|
||||||
final int itemIndex = mediaState.mediaItems.indexWhere(
|
final int itemIndex = mediaState.mediaItems.indexWhere(
|
||||||
(item) => item.id == widget.initialItemId,
|
(item) => item.id == widget.initialItemId,
|
||||||
);
|
);
|
||||||
print('itemIndex: ${itemIndex}; initial: ${widget.initialItemId}');
|
|
||||||
|
|
||||||
if (itemIndex == -1) {
|
if (itemIndex == -1) {
|
||||||
Future.microtask(() {
|
Future.microtask(() {
|
||||||
@ -168,7 +167,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.fullscreen),
|
icon: const Icon(Icons.fullscreen),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_showError('download ist wip');
|
_showMsg('fullscreen ist wip');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
@ -261,7 +260,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMediaItem(MediaItem item, bool isActive) {
|
Widget _buildMediaItem(MediaItem item, bool isActive) {
|
||||||
final mediaNotifier = ref.read(mediaProvider.notifier);
|
final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:f0ckapp/providers/media_provider.dart';
|
import 'package:f0ckapp/providers/media_provider.dart';
|
||||||
import 'package:f0ckapp/utils/appversion_util.dart';
|
import 'package:f0ckapp/utils/appversion_util.dart';
|
||||||
import 'package:f0ckapp/providers/theme_provider.dart';
|
import 'package:f0ckapp/providers/theme_provider.dart';
|
||||||
|
import 'package:f0ckapp/utils/customsearchdelegate_util.dart';
|
||||||
|
|
||||||
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
|
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
|
||||||
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
|
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
|
||||||
@ -78,6 +79,17 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.search),
|
||||||
|
onPressed: () {
|
||||||
|
showSearch(
|
||||||
|
context: context,
|
||||||
|
delegate: CustomSearchDelegate(),
|
||||||
|
);
|
||||||
|
//mediaNotifier.setTag('drachenlord');
|
||||||
|
//_scrollController.jumpTo(0);
|
||||||
|
},
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
mediaState.random ? Icons.shuffle_on_outlined : Icons.shuffle,
|
mediaState.random ? Icons.shuffle_on_outlined : Icons.shuffle,
|
||||||
|
@ -4,8 +4,9 @@ import 'package:http/http.dart' as http;
|
|||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
import 'package:f0ckapp/models/mediaitem_model.dart';
|
import 'package:f0ckapp/models/mediaitem_model.dart';
|
||||||
|
import 'package:f0ckapp/models/suggestion_model.dart';
|
||||||
|
|
||||||
final storage = FlutterSecureStorage();
|
final FlutterSecureStorage storage = FlutterSecureStorage();
|
||||||
|
|
||||||
Future<List<MediaItem>> fetchMedia({
|
Future<List<MediaItem>> fetchMedia({
|
||||||
int? older,
|
int? older,
|
||||||
@ -48,6 +49,36 @@ Future<MediaItem> fetchMediaDetail(int itemId) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<Suggestion>> fetchSuggestions(String query) async {
|
||||||
|
final Uri uri = Uri.parse(
|
||||||
|
'https://f0ck.me/api/v2/admin/tags/suggest?q=$query',
|
||||||
|
); // wip: new route in pyapi
|
||||||
|
final response = await http.get(uri);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final Map<String, dynamic> decoded = jsonDecode(response.body);
|
||||||
|
if (decoded['success'] == true && decoded.containsKey('suggestions')) {
|
||||||
|
final List<dynamic> suggestionsList = decoded['suggestions'];
|
||||||
|
return suggestionsList
|
||||||
|
.map(
|
||||||
|
(dynamic jsonItem) =>
|
||||||
|
Suggestion.fromJson(jsonItem as Map<String, dynamic>),
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
..sort(
|
||||||
|
(Suggestion a, Suggestion b) =>
|
||||||
|
(b.score * b.tagged).compareTo(a.score * a.tagged),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw Exception('Nichts gefunden.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception(
|
||||||
|
'Fehler beim Abrufen der Vorschläge: ${response.statusCode}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> login(String username, String password) async {
|
Future<bool> login(String username, String password) async {
|
||||||
final Uri url = Uri.parse('https://api.f0ck.me/login');
|
final Uri url = Uri.parse('https://api.f0ck.me/login');
|
||||||
|
|
||||||
@ -57,8 +88,8 @@ Future<bool> login(String username, String password) async {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = jsonDecode(response.body);
|
final dynamic data = jsonDecode(response.body);
|
||||||
final token = data['token'];
|
final dynamic token = data['token'];
|
||||||
|
|
||||||
await storage.write(key: "token", value: token);
|
await storage.write(key: "token", value: token);
|
||||||
|
|
||||||
|
86
lib/utils/customsearchdelegate_util.dart
Normal file
86
lib/utils/customsearchdelegate_util.dart
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:f0ckapp/services/api_service.dart';
|
||||||
|
import 'package:f0ckapp/models/suggestion_model.dart';
|
||||||
|
import 'package:f0ckapp/providers/media_provider.dart';
|
||||||
|
|
||||||
|
class CustomSearchDelegate extends SearchDelegate<String> {
|
||||||
|
@override
|
||||||
|
List<Widget> buildActions(BuildContext context) {
|
||||||
|
return [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
query = '';
|
||||||
|
showSuggestions(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildLeading(BuildContext context) {
|
||||||
|
return IconButton(
|
||||||
|
icon: Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => close(context, 'null'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildResults(BuildContext context) {
|
||||||
|
return Center(child: Text('Suchergebnisse für: "$query"'));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildSuggestions(BuildContext context) {
|
||||||
|
if (query.isEmpty) {
|
||||||
|
return Container(padding: EdgeInsets.all(16.0), child: Text(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
final Future<List<Suggestion>> futureSuggestions = Future.delayed(
|
||||||
|
Duration(milliseconds: 300),
|
||||||
|
() => fetchSuggestions(query),
|
||||||
|
);
|
||||||
|
|
||||||
|
return FutureBuilder<List<Suggestion>>(
|
||||||
|
future: futureSuggestions,
|
||||||
|
builder: (BuildContext context, AsyncSnapshot<List<Suggestion>> snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Center(child: Text("Fehler: ${snapshot.error}"));
|
||||||
|
}
|
||||||
|
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||||
|
return Center(child: Text("Keine Vorschläge gefunden."));
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Suggestion> suggestions = snapshot.data!;
|
||||||
|
return Consumer(
|
||||||
|
builder: (BuildContext context, WidgetRef ref, Widget? child) {
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: suggestions.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final Suggestion suggestion = suggestions[index];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(suggestion.tag),
|
||||||
|
subtitle: Text(
|
||||||
|
'Getaggt: ${suggestion.tagged}x • Score: ${suggestion.score.toStringAsFixed(2)}',
|
||||||
|
style: TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
ref.read(mediaProvider.notifier).setTag(suggestion.tag);
|
||||||
|
close(context, suggestion.tag);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -45,7 +45,7 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
|
|||||||
}
|
}
|
||||||
_controller.setLooping(true);
|
_controller.setLooping(true);
|
||||||
|
|
||||||
final muted = ref.read(mediaProvider).muted;
|
final bool muted = ref.read(mediaProvider).muted;
|
||||||
_controller.setVolume(muted ? 0.0 : 1.0);
|
_controller.setVolume(muted ? 0.0 : 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final muted = ref.watch(mediaProvider).muted;
|
final bool muted = ref.watch(mediaProvider).muted;
|
||||||
if (_controller.value.isInitialized &&
|
if (_controller.value.isInitialized &&
|
||||||
_controller.value.volume != (muted ? 0.0 : 1.0)) {
|
_controller.value.volume != (muted ? 0.0 : 1.0)) {
|
||||||
_controller.setVolume(muted ? 0.0 : 1.0);
|
_controller.setVolume(muted ? 0.0 : 1.0);
|
||||||
|
@ -17,8 +17,8 @@ class VideoControlsOverlay extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final mediaState = ref.watch(mediaProvider);
|
final MediaState mediaState = ref.watch(mediaProvider);
|
||||||
final mediaNotifier = ref.read(mediaProvider.notifier);
|
final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.1.10+40
|
version: 1.1.11+41
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.0-100.2.beta
|
sdk: ^3.9.0-100.2.beta
|
||||||
|
Loading…
x
Reference in New Issue
Block a user