- fixed: duplicates on the frontpage - new: search by tag
This commit is contained in:
		@@ -30,7 +30,7 @@
 | 
			
		||||
                <action android:name="android.intent.action.MAIN"/>
 | 
			
		||||
                <category android:name="android.intent.category.LAUNCHER"/>
 | 
			
		||||
            </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">
 | 
			
		||||
                <action android:name="android.intent.action.VIEW"/>
 | 
			
		||||
                <category android:name="android.intent.category.DEFAULT"/>
 | 
			
		||||
 
 | 
			
		||||
@@ -28,20 +28,20 @@ class F0ckApp extends ConsumerWidget {
 | 
			
		||||
    routes: [
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/',
 | 
			
		||||
        builder: (context, state) {
 | 
			
		||||
        builder: (BuildContext context, GoRouterState state) {
 | 
			
		||||
          return const MediaGrid();
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/:rest(.*)',
 | 
			
		||||
        builder: (context, state) {
 | 
			
		||||
          final isInternalLink = (state.extra is bool && state.extra == true);
 | 
			
		||||
          final fullPath = state.matchedLocation;
 | 
			
		||||
          final bool isInternalLink = (state.extra is bool && state.extra == true);
 | 
			
		||||
          final String fullPath = state.matchedLocation;
 | 
			
		||||
 | 
			
		||||
          final regExp = RegExp(
 | 
			
		||||
            r'^(?:/tag/(?<tag>.+?))?(?:/(?<mime>image|audio|video))?(?:/(?<itemid>\d+))?$',
 | 
			
		||||
          );
 | 
			
		||||
          final match = regExp.firstMatch(fullPath);
 | 
			
		||||
          final RegExpMatch? match = regExp.firstMatch(fullPath);
 | 
			
		||||
 | 
			
		||||
          if (match == null) {
 | 
			
		||||
            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? idStr = match.namedGroup('itemid');
 | 
			
		||||
          final int? itemId = idStr != null ? int.tryParse(idStr) : null;
 | 
			
		||||
          const preloadOffset = 50;
 | 
			
		||||
          const int preloadOffset = 50;
 | 
			
		||||
 | 
			
		||||
          return Consumer(
 | 
			
		||||
            builder: (context, ref, child) {
 | 
			
		||||
            builder: (BuildContext context, WidgetRef ref, Widget? child) {
 | 
			
		||||
              WidgetsBinding.instance.addPostFrameCallback((_) async {
 | 
			
		||||
                final mediaNotifier = ref.read(mediaProvider.notifier);
 | 
			
		||||
                final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
 | 
			
		||||
                if (!isInternalLink) {
 | 
			
		||||
                  mediaNotifier.setType(mime ?? "alles");
 | 
			
		||||
                  mediaNotifier.setTag(tag);
 | 
			
		||||
@@ -79,7 +79,7 @@ class F0ckApp extends ConsumerWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final theme = ref.watch(themeNotifierProvider);
 | 
			
		||||
    final ThemeData theme = ref.watch(themeNotifierProvider);
 | 
			
		||||
    return MaterialApp.router(
 | 
			
		||||
      debugShowCheckedModeBanner: false,
 | 
			
		||||
      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,9 +102,18 @@ class MediaNotifier extends StateNotifier<MediaState> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void addMediaItems(List<MediaItem> newItems) {
 | 
			
		||||
    final updated = List<MediaItem>.from(state.mediaItems)..addAll(newItems);
 | 
			
		||||
    final Set<int> existingIds = state.mediaItems
 | 
			
		||||
        .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 {
 | 
			
		||||
    if (state.isLoading) return;
 | 
			
		||||
 
 | 
			
		||||
@@ -56,13 +56,13 @@ class _DetailViewState extends ConsumerState<DetailView> {
 | 
			
		||||
    try {
 | 
			
		||||
      await ref.read(mediaProvider.notifier).loadMedia();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      _showError("Fehler beim Laden der Medien: $e");
 | 
			
		||||
      _showMsg("Fehler beim Laden der Medien: $e");
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => isLoading = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _showError(String message) {
 | 
			
		||||
  void _showMsg(String message) {
 | 
			
		||||
    if (!mounted) return;
 | 
			
		||||
    ScaffoldMessenger.of(context)
 | 
			
		||||
      ..removeCurrentSnackBar()
 | 
			
		||||
@@ -70,15 +70,15 @@ class _DetailViewState extends ConsumerState<DetailView> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _downloadMedia() async {
 | 
			
		||||
    final mediaState = ref.read(mediaProvider);
 | 
			
		||||
    final currentItem = mediaState.mediaItems[_currentIndex];
 | 
			
		||||
    final MediaState mediaState = ref.read(mediaProvider);
 | 
			
		||||
    final MediaItem currentItem = mediaState.mediaItems[_currentIndex];
 | 
			
		||||
 | 
			
		||||
    if (Platform.isAndroid || Platform.isIOS) {
 | 
			
		||||
      var status = await Permission.storage.status;
 | 
			
		||||
      PermissionStatus status = await Permission.storage.status;
 | 
			
		||||
      if (!status.isGranted) {
 | 
			
		||||
        status = await Permission.storage.request();
 | 
			
		||||
        if (!status.isGranted) {
 | 
			
		||||
          _showError("Speicherberechtigung wurde nicht erteilt.");
 | 
			
		||||
          _showMsg("Speicherberechtigung wurde nicht erteilt.");
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@@ -86,17 +86,17 @@ class _DetailViewState extends ConsumerState<DetailView> {
 | 
			
		||||
 | 
			
		||||
    String localPath;
 | 
			
		||||
    if (Platform.isAndroid) {
 | 
			
		||||
      final directory = await getExternalStorageDirectory();
 | 
			
		||||
      final Directory? directory = await getExternalStorageDirectory();
 | 
			
		||||
      localPath = "${directory!.path}/Download/fApp";
 | 
			
		||||
    } else if (Platform.isIOS) {
 | 
			
		||||
      final directory = await getApplicationDocumentsDirectory();
 | 
			
		||||
      final Directory directory = await getApplicationDocumentsDirectory();
 | 
			
		||||
      localPath = directory.path;
 | 
			
		||||
    } else {
 | 
			
		||||
      final directory = await getTemporaryDirectory();
 | 
			
		||||
      final Directory directory = await getTemporaryDirectory();
 | 
			
		||||
      localPath = directory.path;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final savedDir = Directory(localPath);
 | 
			
		||||
    final Directory savedDir = Directory(localPath);
 | 
			
		||||
    if (!await savedDir.exists()) {
 | 
			
		||||
      await savedDir.create(recursive: true);
 | 
			
		||||
    }
 | 
			
		||||
@@ -111,10 +111,10 @@ class _DetailViewState extends ConsumerState<DetailView> {
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (mounted) {
 | 
			
		||||
        _showError('Download gestartet: ${currentItem.mediaUrl}');
 | 
			
		||||
        _showMsg('Download gestartet: ${currentItem.mediaUrl}');
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      _showError('Download fehlgeschlagen: $e');
 | 
			
		||||
      _showMsg('Download fehlgeschlagen: $e');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -126,11 +126,10 @@ class _DetailViewState extends ConsumerState<DetailView> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final mediaState = ref.watch(mediaProvider);
 | 
			
		||||
    final MediaState mediaState = ref.watch(mediaProvider);
 | 
			
		||||
    final int itemIndex = mediaState.mediaItems.indexWhere(
 | 
			
		||||
      (item) => item.id == widget.initialItemId,
 | 
			
		||||
    );
 | 
			
		||||
    print('itemIndex: ${itemIndex}; initial: ${widget.initialItemId}');
 | 
			
		||||
 | 
			
		||||
    if (itemIndex == -1) {
 | 
			
		||||
      Future.microtask(() {
 | 
			
		||||
@@ -168,7 +167,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
 | 
			
		||||
          IconButton(
 | 
			
		||||
            icon: const Icon(Icons.fullscreen),
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              _showError('download ist wip');
 | 
			
		||||
              _showMsg('fullscreen ist wip');
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
          IconButton(
 | 
			
		||||
@@ -261,7 +260,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Widget _buildMediaItem(MediaItem item, bool isActive) {
 | 
			
		||||
    final mediaNotifier = ref.read(mediaProvider.notifier);
 | 
			
		||||
    final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
 | 
			
		||||
 | 
			
		||||
    return SingleChildScrollView(
 | 
			
		||||
      child: Column(
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:f0ckapp/providers/media_provider.dart';
 | 
			
		||||
import 'package:f0ckapp/utils/appversion_util.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> mediaModes = ["sfw", "nsfw", "untagged", "all"];
 | 
			
		||||
@@ -78,6 +79,17 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
            icon: Icon(Icons.search),
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              showSearch(
 | 
			
		||||
                context: context,
 | 
			
		||||
                delegate: CustomSearchDelegate(),
 | 
			
		||||
              );
 | 
			
		||||
              //mediaNotifier.setTag('drachenlord');
 | 
			
		||||
              //_scrollController.jumpTo(0);
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
          IconButton(
 | 
			
		||||
            icon: Icon(
 | 
			
		||||
              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:f0ckapp/models/mediaitem_model.dart';
 | 
			
		||||
import 'package:f0ckapp/models/suggestion_model.dart';
 | 
			
		||||
 | 
			
		||||
final storage = FlutterSecureStorage();
 | 
			
		||||
final FlutterSecureStorage storage = FlutterSecureStorage();
 | 
			
		||||
 | 
			
		||||
Future<List<MediaItem>> fetchMedia({
 | 
			
		||||
  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 {
 | 
			
		||||
  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) {
 | 
			
		||||
    final data = jsonDecode(response.body);
 | 
			
		||||
    final token = data['token'];
 | 
			
		||||
    final dynamic data = jsonDecode(response.body);
 | 
			
		||||
    final dynamic token = data['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);
 | 
			
		||||
 | 
			
		||||
    final muted = ref.read(mediaProvider).muted;
 | 
			
		||||
    final bool muted = ref.read(mediaProvider).muted;
 | 
			
		||||
    _controller.setVolume(muted ? 0.0 : 1.0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -82,7 +82,7 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final muted = ref.watch(mediaProvider).muted;
 | 
			
		||||
    final bool muted = ref.watch(mediaProvider).muted;
 | 
			
		||||
    if (_controller.value.isInitialized &&
 | 
			
		||||
        _controller.value.volume != (muted ? 0.0 : 1.0)) {
 | 
			
		||||
      _controller.setVolume(muted ? 0.0 : 1.0);
 | 
			
		||||
 
 | 
			
		||||
@@ -17,8 +17,8 @@ class VideoControlsOverlay extends ConsumerWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, ref) {
 | 
			
		||||
    final mediaState = ref.watch(mediaProvider);
 | 
			
		||||
    final mediaNotifier = ref.read(mediaProvider.notifier);
 | 
			
		||||
    final MediaState mediaState = ref.watch(mediaProvider);
 | 
			
		||||
    final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
 | 
			
		||||
 | 
			
		||||
    return Stack(
 | 
			
		||||
      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
 | 
			
		||||
# 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.
 | 
			
		||||
version: 1.1.10+40
 | 
			
		||||
version: 1.1.11+41
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: ^3.9.0-100.2.beta
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user