Compare commits
37 Commits
4970836c60
...
v0.1.18
| Author | SHA1 | Date | |
|---|---|---|---|
| c7c5b3682d | |||
| 321a310add | |||
| 3bc7d713dd | |||
| 1d9ace45a1 | |||
| 588843a989 | |||
| f780638269 | |||
| 37447b4a53 | |||
| c283de7a45 | |||
| 483db8bfbd | |||
| a4d471dc62 | |||
| 970d78943b | |||
| a66c25c784 | |||
| 609a477b75 | |||
| 6d847aa2bb | |||
| f0b3c11e63 | |||
| 143206cd72 | |||
| 36e4eaee17 | |||
| 11a43a39fa | |||
| c0f92fac58 | |||
| be6a44e7f0 | |||
| 36e035c09c | |||
| 0309678650 | |||
| 885e638265 | |||
| 68a2a31d07 | |||
| baf664a3ad | |||
| 6d51ef9ad0 | |||
| eddf2acbde | |||
| 7211560b8d | |||
| db61809939 | |||
| be171aa4bc | |||
| ca27cfeb96 | |||
| 8bdf036c1a | |||
| 6fb873163b | |||
| 2d93d1a9d7 | |||
| a4d760a970 | |||
| d0feca1ba8 | |||
| 12459bb4cb |
79
.gitea/workflows/android_build.yml
Normal file
79
.gitea/workflows/android_build.yml
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
name: Flutter APK Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_apk:
|
||||||
|
name: Build Flutter APK
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# - name: Cache pub deps
|
||||||
|
# uses: actions/cache@v3
|
||||||
|
# with:
|
||||||
|
# path: ~/.pub-cache
|
||||||
|
# key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.yaml') }}
|
||||||
|
# restore-keys: ${{ runner.os }}-pub-
|
||||||
|
|
||||||
|
- name: Setup Java (Temurin 17)
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
with:
|
||||||
|
packages: "platform-tools platforms;android-36 build-tools;36.0.0"
|
||||||
|
|
||||||
|
- name: Setup Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
- run: flutter --version
|
||||||
|
- run: flutter doctor
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Build APK
|
||||||
|
run: flutter build apk --release
|
||||||
|
|
||||||
|
- name: Upload APK artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: flutter-apk
|
||||||
|
path: build/app/outputs/flutter-apk/app-release.apk
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Create Gitea release
|
||||||
|
uses: akkuman/gitea-release-action@v1
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.RUNNER_CREATE_RELEASE }}
|
||||||
|
with:
|
||||||
|
# tag & name: adjust to your versioning
|
||||||
|
tag_name: "v0.1.${{ github.run_number }}"
|
||||||
|
name: "Flutter Android v0.1.${{ github.run_number }}"
|
||||||
|
body: "Automated build from CI"
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
files: |
|
||||||
|
build/app/outputs/flutter-apk/app-release.apk
|
||||||
|
gitea_url: "https://git.skup.in"
|
||||||
|
owner: "marco"
|
||||||
|
repo: "maps_bookmarks"
|
||||||
2
android/app/proguard-rules.pro
vendored
Normal file
2
android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-keep class io.flutter.plugins.sharedpreferences.** { *; }
|
||||||
|
-keep class io.flutter.plugins.sharedpreferences.LegacySharedPreferencesPlugin { *; }
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<application
|
<application
|
||||||
android:label="maps_bookmarks"
|
android:label="maps_bookmarks"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
@@ -6,7 +7,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTask"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
@@ -46,5 +47,10 @@
|
|||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain"/>
|
||||||
</intent>
|
</intent>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
</intent>
|
||||||
|
<package android:name="com.google.android.apps.maps" />
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.os.Bundle
|
|||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
class MainActivity: FlutterActivity() {
|
||||||
private var sharedText: String? = null
|
private var sharedText: String? = null
|
||||||
@@ -31,6 +32,8 @@ class MainActivity: FlutterActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
flutterEngine.plugins.add(SharedPreferencesPlugin())
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
||||||
.setMethodCallHandler { call, result ->
|
.setMethodCallHandler { call, result ->
|
||||||
if (call.method == "getSharedText") {
|
if (call.method == "getSharedText") {
|
||||||
|
|||||||
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
@@ -1,21 +1,80 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'pages/collections_page.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'pages/collection_page.dart';
|
||||||
|
import 'pages/collections_list_page.dart';
|
||||||
|
import 'pages/search_page.dart';
|
||||||
|
import 'service/search_provider.dart';
|
||||||
|
import 'service/shared_link_provider.dart';
|
||||||
|
import 'service/storage.dart';
|
||||||
|
import 'service/share_intent_service.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
void main() {
|
void main() async {
|
||||||
runApp(const MapsBookmarks());
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await Storage.initialize();
|
||||||
|
runApp(
|
||||||
|
MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider(create: (_) => SharedLinkProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => SearchProvider()),
|
||||||
|
],
|
||||||
|
child: const MapsBookmarks(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MapsBookmarks extends StatelessWidget {
|
class MapsBookmarks extends StatefulWidget {
|
||||||
const MapsBookmarks({super.key});
|
const MapsBookmarks({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MapsBookmarks> createState() => _MapsBookmarksState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MapsBookmarksState extends State<MapsBookmarks>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
|
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
final ShareIntentService _shareIntentService = ShareIntentService();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
_checkForSharedContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
_checkForSharedContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkForSharedContent() async {
|
||||||
|
final sharedText = await _shareIntentService.getSharedMapsLink();
|
||||||
|
|
||||||
|
if (sharedText.isNotEmpty && mounted) {
|
||||||
|
context.read<SharedLinkProvider>().setCurrentMapsLink(sharedText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
theme: ThemeData(
|
navigatorKey: _navigatorKey,
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
theme: lightTheme,
|
||||||
),
|
darkTheme: darkTheme,
|
||||||
initialRoute: CollectionsPage.routeName,
|
initialRoute: CollectionsListPage.routeName,
|
||||||
routes: {CollectionsPage.routeName: (context) => const CollectionsPage()},
|
routes: {
|
||||||
|
CollectionsListPage.routeName: (context) => const CollectionsListPage(),
|
||||||
|
CollectionPage.routeName: (context) => const CollectionPage(),
|
||||||
|
SearchPage.routeName: (context) => const SearchPage(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,45 @@
|
|||||||
import 'package:hive/hive.dart';
|
class Bookmark {
|
||||||
|
Bookmark({
|
||||||
|
required this.collectionId,
|
||||||
|
required this.name,
|
||||||
|
required this.link,
|
||||||
|
required this.description,
|
||||||
|
int? createdAt,
|
||||||
|
}) : createdAt = createdAt ?? DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
class Bookmark extends HiveObject {
|
factory Bookmark.fromJson(Map<String, dynamic> json) => Bookmark(
|
||||||
Bookmark({required this.name, required this.link});
|
collectionId: json['collectionId'] as int,
|
||||||
|
name: json['name'] as String,
|
||||||
factory Bookmark.fromJson(Map<String, dynamic> json) =>
|
link: json['link'] as String,
|
||||||
Bookmark(name: json['name'] as String, link: json['link'] as String);
|
description: json['description'] as String,
|
||||||
|
createdAt: json['createdAt'] as int,
|
||||||
|
);
|
||||||
|
|
||||||
|
int collectionId;
|
||||||
String link;
|
String link;
|
||||||
String name;
|
String name;
|
||||||
|
String description;
|
||||||
|
int createdAt;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {'name': name, 'link': link};
|
int get id => createdAt;
|
||||||
|
|
||||||
|
DateTime get createdDate => DateTime.fromMillisecondsSinceEpoch(createdAt);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'collectionId': collectionId,
|
||||||
|
'name': name,
|
||||||
|
'link': link,
|
||||||
|
'description': description,
|
||||||
|
'createdAt': createdAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
Bookmark copyWith({String? link, String? name, String? description}) {
|
||||||
|
return Bookmark(
|
||||||
|
collectionId: collectionId,
|
||||||
|
name: name ?? this.name,
|
||||||
|
link: link ?? this.link,
|
||||||
|
description: description ?? this.description,
|
||||||
|
createdAt: createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,34 @@
|
|||||||
import 'package:hive/hive.dart';
|
class Collection {
|
||||||
|
Collection({required this.name, int? createdAt})
|
||||||
|
: createdAt = createdAt ?? DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
class Collection extends HiveObject {
|
factory Collection.fromJson(Map<String, dynamic> json) => Collection(
|
||||||
Collection({required this.name});
|
name: json['name'] as String,
|
||||||
|
createdAt: json['createdAt'] as int,
|
||||||
factory Collection.fromJson(Map<String, dynamic> json) =>
|
);
|
||||||
Collection(name: json['name'] as String);
|
|
||||||
|
|
||||||
|
int createdAt; // used as Id with millisecondsSinceEpoch
|
||||||
String name;
|
String name;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {'name': name};
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other is Collection) {
|
||||||
|
return hashCode == other.hashCode;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => id.hashCode;
|
||||||
|
|
||||||
|
int get id => createdAt;
|
||||||
|
|
||||||
|
DateTime get createdDate => DateTime.fromMillisecondsSinceEpoch(createdAt);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {'name': name, 'createdAt': createdAt};
|
||||||
|
|
||||||
|
Collection copyWith({String? name}) {
|
||||||
|
return Collection(name: name ?? this.name, createdAt: createdAt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
lib/model/maps_link_metadata.dart
Normal file
26
lib/model/maps_link_metadata.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
class MapsLinkMetadata {
|
||||||
|
final String url;
|
||||||
|
final String placeName;
|
||||||
|
final String latitude;
|
||||||
|
final String longitude;
|
||||||
|
final String address;
|
||||||
|
final String description;
|
||||||
|
|
||||||
|
const MapsLinkMetadata({
|
||||||
|
required this.url,
|
||||||
|
this.placeName = '',
|
||||||
|
this.latitude = '',
|
||||||
|
this.longitude = '',
|
||||||
|
this.address = '',
|
||||||
|
this.description = '',
|
||||||
|
});
|
||||||
|
|
||||||
|
String get coordinates {
|
||||||
|
if (hasCoordinates) {
|
||||||
|
return '$latitude, $longitude';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get hasCoordinates => latitude.isNotEmpty && longitude.isNotEmpty;
|
||||||
|
}
|
||||||
113
lib/pages/collection_page.dart
Normal file
113
lib/pages/collection_page.dart
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../model/bookmark.dart';
|
||||||
|
import '../model/maps_link_metadata.dart';
|
||||||
|
import '../service/bookmarks_provider.dart';
|
||||||
|
import '../service/notifying.dart';
|
||||||
|
import '../service/shared_link_provider.dart';
|
||||||
|
import '../service/storage.dart';
|
||||||
|
import '../service/url_launcher.dart';
|
||||||
|
import '../widgets/create_bookmark_dialog.dart';
|
||||||
|
|
||||||
|
class CollectionPage extends StatefulWidget {
|
||||||
|
const CollectionPage({super.key});
|
||||||
|
|
||||||
|
static const String routeName = '/bookmarks';
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CollectionPage> createState() => _CollectionPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CollectionPageState extends State<CollectionPage> {
|
||||||
|
MapsLinkMetadata? selectedMapsLink;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (selectedMapsLink != null) onAddButtonPressed();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAddButtonPressed() => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => CreateBookmarkDialog(
|
||||||
|
collectionId: BookmarksProvider.selectedCollectionId!,
|
||||||
|
onSavePressed: onBookmarkSaved,
|
||||||
|
selectedMapsLink: selectedMapsLink,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
void editBookmark(Bookmark selectedBookmark) => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => CreateBookmarkDialog(
|
||||||
|
collectionId: BookmarksProvider.selectedCollectionId!,
|
||||||
|
selectedBookmark: selectedBookmark,
|
||||||
|
onSavePressed: onBookmarkSaved,
|
||||||
|
onDeletePressed: () {
|
||||||
|
Storage.deleteBookmarkById(
|
||||||
|
selectedBookmark.id,
|
||||||
|
).whenComplete(() => setState(() {}));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
void onBookmarkSaved(Bookmark bookmark) {
|
||||||
|
Storage.addOrUpdateBookmark(bookmark);
|
||||||
|
setState(() {});
|
||||||
|
context.read<SharedLinkProvider>().removeCurrentMapsLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget bookmarksListItemBuilder(BuildContext context, Bookmark bookmark) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(bookmark.name),
|
||||||
|
onTap: () => launchUrlFromString(bookmark.link).then((errorCode) {
|
||||||
|
if (context.mounted) {
|
||||||
|
return Notifying.showUrlErrorSnackbar(context, errorCode);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
onLongPress: () => editBookmark(bookmark),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
SharedLinkProvider provider = context.watch<SharedLinkProvider>();
|
||||||
|
selectedMapsLink = provider.currentMapsLinkMetadata;
|
||||||
|
|
||||||
|
if (BookmarksProvider.selectedCollectionId == null) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
final bookmarks = Storage.loadBookmarksForCollection(
|
||||||
|
BookmarksProvider.selectedCollectionId!,
|
||||||
|
);
|
||||||
|
final collection = Storage.loadCollections().firstWhere(
|
||||||
|
(c) => c.id == BookmarksProvider.selectedCollectionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: selectedMapsLink != null
|
||||||
|
? Text('Add to ${collection.name}')
|
||||||
|
: Text(collection.name),
|
||||||
|
actions: [
|
||||||
|
if (selectedMapsLink != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => provider.removeCurrentMapsLink(),
|
||||||
|
child: Text('Cancel'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
itemBuilder: (context, index) =>
|
||||||
|
bookmarksListItemBuilder(context, bookmarks.elementAt(index)),
|
||||||
|
itemCount: bookmarks.length,
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: onAddButtonPressed,
|
||||||
|
child: Icon(selectedMapsLink != null ? Icons.save : Icons.add),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
116
lib/pages/collections_list_page.dart
Normal file
116
lib/pages/collections_list_page.dart
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../model/collection.dart';
|
||||||
|
import '../service/bookmarks_provider.dart';
|
||||||
|
import '../service/shared_link_provider.dart';
|
||||||
|
import '../service/storage.dart';
|
||||||
|
import '../widgets/create_bookmark_collection_dialog.dart';
|
||||||
|
import 'collection_page.dart';
|
||||||
|
import 'search_page.dart' show SearchPage;
|
||||||
|
|
||||||
|
class CollectionsListPage extends StatefulWidget {
|
||||||
|
const CollectionsListPage({super.key});
|
||||||
|
|
||||||
|
static const String routeName = '/collections';
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CollectionsListPage> createState() => _CollectionsListPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CollectionsListPageState extends State<CollectionsListPage> {
|
||||||
|
bool addingNewBookmark = false;
|
||||||
|
|
||||||
|
Widget bottomSheetBuilder(BuildContext context) {
|
||||||
|
final titleTextFieldController = TextEditingController(
|
||||||
|
text: context
|
||||||
|
.read<SharedLinkProvider>()
|
||||||
|
.currentMapsLinkMetadata!
|
||||||
|
.placeName,
|
||||||
|
);
|
||||||
|
return SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: TextField(controller: titleTextFieldController),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAddButtonPressed() => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
CreateBookmarkCollectionDialog(onSavePressed: onCollectionSaved),
|
||||||
|
);
|
||||||
|
|
||||||
|
void onCollectionSaved(Collection collection) {
|
||||||
|
Storage.addOrUpdateCollection(collection);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget collectionsListItemBuilder(
|
||||||
|
BuildContext context,
|
||||||
|
Collection collection,
|
||||||
|
) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(collection.name),
|
||||||
|
onTap: () => navigateToCollection(collection.id),
|
||||||
|
onLongPress: () => onEditCollection(collection),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void navigateToCollection(int collectionId) {
|
||||||
|
BookmarksProvider.selectedCollectionId = collectionId;
|
||||||
|
Navigator.pushNamed(context, CollectionPage.routeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onEditCollection(Collection selectedCollection) => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => CreateBookmarkCollectionDialog(
|
||||||
|
selectedCollection: selectedCollection,
|
||||||
|
onSavePressed: onCollectionSaved,
|
||||||
|
onDeletePressed: () {
|
||||||
|
Storage.deleteCollection(
|
||||||
|
selectedCollection,
|
||||||
|
).whenComplete(() => setState(() {}));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final collections = Storage.loadCollections();
|
||||||
|
final provider = context.watch<SharedLinkProvider>();
|
||||||
|
addingNewBookmark = provider.currentMapsLinkMetadata != null;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: addingNewBookmark
|
||||||
|
? Text('Choose Collection')
|
||||||
|
: Text('Collections'),
|
||||||
|
actions: [
|
||||||
|
if (addingNewBookmark)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => provider.removeCurrentMapsLink(),
|
||||||
|
child: Text('Cancel'),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
onPressed: () =>
|
||||||
|
Navigator.of(context).pushNamed(SearchPage.routeName),
|
||||||
|
icon: Icon(Icons.search),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: onAddButtonPressed,
|
||||||
|
child: Icon(Icons.add),
|
||||||
|
),
|
||||||
|
body: collections.isNotEmpty
|
||||||
|
? ListView.builder(
|
||||||
|
itemBuilder: (context, index) => collectionsListItemBuilder(
|
||||||
|
context,
|
||||||
|
collections.elementAt(index),
|
||||||
|
),
|
||||||
|
itemCount: collections.length,
|
||||||
|
)
|
||||||
|
: Center(child: Text('Create your first Collection to get started!')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../widgets/create_bookmark_collection_dialog.dart';
|
|
||||||
|
|
||||||
class CollectionsPage extends StatefulWidget {
|
|
||||||
const CollectionsPage({super.key});
|
|
||||||
static const String routeName = '/collections';
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CollectionsPage> createState() => _CollectionsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CollectionsPageState extends State<CollectionsPage> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: () => showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => CreateBookmarkCollectionDialog(),
|
|
||||||
),
|
|
||||||
child: Icon(Icons.add),
|
|
||||||
),
|
|
||||||
body: ListView(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onAddButtonPressed() => showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => CreateBookmarkCollectionDialog(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
27
lib/pages/search_page.dart
Normal file
27
lib/pages/search_page.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../service/search_provider.dart';
|
||||||
|
import '../widgets/search_widgets/search_bar_widget.dart';
|
||||||
|
import '../widgets/search_widgets/search_results_widget.dart';
|
||||||
|
|
||||||
|
class SearchPage extends StatelessWidget {
|
||||||
|
const SearchPage({super.key});
|
||||||
|
|
||||||
|
static const String routeName = '/search';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text('Search')),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
SearchBarWidget(
|
||||||
|
onEditingComplete: context.read<SearchProvider>().setSearchText,
|
||||||
|
onResetSearch: context.read<SearchProvider>().removeSearchText,
|
||||||
|
),
|
||||||
|
Expanded(child: SearchResultsWidget()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
lib/service/bookmarks_provider.dart
Normal file
3
lib/service/bookmarks_provider.dart
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
class BookmarksProvider {
|
||||||
|
static int? selectedCollectionId;
|
||||||
|
}
|
||||||
79
lib/service/maps_launcher_service.dart
Normal file
79
lib/service/maps_launcher_service.dart
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// service/maps_launcher_service.dart
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
import 'package:android_intent_plus/android_intent.dart';
|
||||||
|
|
||||||
|
class MapsLauncherService {
|
||||||
|
/// Opens a URL in Google Maps app
|
||||||
|
/// Falls back to browser if Maps app is not installed
|
||||||
|
static Future<bool> openInGoogleMaps(String url) async {
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
// Handle iOS or other platforms if needed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to open in Google Maps app
|
||||||
|
final intent = AndroidIntent(
|
||||||
|
action: 'action_view',
|
||||||
|
data: url,
|
||||||
|
package: 'com.google.android.apps.maps',
|
||||||
|
);
|
||||||
|
|
||||||
|
await intent.launch();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
// Google Maps not installed, try browser as fallback
|
||||||
|
try {
|
||||||
|
final browserIntent = AndroidIntent(action: 'action_view', data: url);
|
||||||
|
await browserIntent.launch();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
print('Failed to open maps link: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens navigation to specific coordinates
|
||||||
|
static Future<bool> navigateToCoordinates(
|
||||||
|
String latitude,
|
||||||
|
String longitude,
|
||||||
|
) async {
|
||||||
|
final url = 'google.navigation:q=$latitude,$longitude';
|
||||||
|
return openInGoogleMaps(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens a search query in Google Maps
|
||||||
|
static Future<bool> searchInMaps(String query) async {
|
||||||
|
final encodedQuery = Uri.encodeComponent(query);
|
||||||
|
final url = 'geo:0,0?q=$encodedQuery';
|
||||||
|
return openInGoogleMaps(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shares a Google Maps link or location via Android share sheet
|
||||||
|
static Future<bool> shareLocation({
|
||||||
|
required String text,
|
||||||
|
String? subject,
|
||||||
|
}) async {
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final intent = AndroidIntent(
|
||||||
|
action: 'action_send',
|
||||||
|
type: 'text/plain',
|
||||||
|
arguments: {
|
||||||
|
'android.intent.extra.TEXT': text,
|
||||||
|
if (subject != null) 'android.intent.extra.SUBJECT': subject,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await intent.launch();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
print('Failed to share location: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
lib/service/notifying.dart
Normal file
37
lib/service/notifying.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'url_launcher.dart' show UrlLaunchErrorCode;
|
||||||
|
|
||||||
|
class Notifying {
|
||||||
|
static void showSnackbar(
|
||||||
|
BuildContext context, {
|
||||||
|
required String text,
|
||||||
|
bool isError = false,
|
||||||
|
}) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
text,
|
||||||
|
style: isError
|
||||||
|
? TextStyle(color: Theme.of(context).colorScheme.error)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void showUrlErrorSnackbar(
|
||||||
|
BuildContext context,
|
||||||
|
UrlLaunchErrorCode errorCode,
|
||||||
|
) {
|
||||||
|
String errorText = '';
|
||||||
|
if (errorCode == UrlLaunchErrorCode.none) {
|
||||||
|
return;
|
||||||
|
} else if (errorCode == UrlLaunchErrorCode.couldNotLaunch) {
|
||||||
|
errorText = 'Could not launch Url';
|
||||||
|
} else {
|
||||||
|
errorText = 'Invalid Url';
|
||||||
|
}
|
||||||
|
showSnackbar(context, text: errorText, isError: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
lib/service/search_provider.dart
Normal file
17
lib/service/search_provider.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SearchProvider extends ChangeNotifier {
|
||||||
|
String _searchText = '';
|
||||||
|
|
||||||
|
void setSearchText(String searchText, {bool silent = false}) {
|
||||||
|
_searchText = searchText;
|
||||||
|
if (!silent) notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeSearchText({bool silent = false}) {
|
||||||
|
_searchText = '';
|
||||||
|
if (!silent) notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
String get searchText => _searchText;
|
||||||
|
}
|
||||||
25
lib/service/share_intent_service.dart
Normal file
25
lib/service/share_intent_service.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class ShareIntentService {
|
||||||
|
factory ShareIntentService() {
|
||||||
|
_instance ??= ShareIntentService._internal();
|
||||||
|
return _instance!;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShareIntentService._internal();
|
||||||
|
|
||||||
|
static ShareIntentService? _instance;
|
||||||
|
static const _platform = MethodChannel('app.channel.shared.data');
|
||||||
|
|
||||||
|
Future<String> getSharedMapsLink() async {
|
||||||
|
final String? sharedText = await _platform.invokeMethod('getSharedText');
|
||||||
|
if (sharedText != null && _isGoogleMapsLink(sharedText)) return sharedText;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isGoogleMapsLink(String text) {
|
||||||
|
return text.contains('maps.google.com') ||
|
||||||
|
text.contains('maps.app.goo.gl') ||
|
||||||
|
text.contains('goo.gl/maps');
|
||||||
|
}
|
||||||
|
}
|
||||||
24
lib/service/shared_link_provider.dart
Normal file
24
lib/service/shared_link_provider.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:metadata_fetch/metadata_fetch.dart';
|
||||||
|
|
||||||
|
import '../model/maps_link_metadata.dart';
|
||||||
|
|
||||||
|
class SharedLinkProvider extends ChangeNotifier {
|
||||||
|
MapsLinkMetadata? _currentMapsLinkMetadata;
|
||||||
|
|
||||||
|
void setCurrentMapsLink(String mapsLink, {bool silent = false}) async {
|
||||||
|
final metadata = await MetadataFetch.extract(mapsLink);
|
||||||
|
_currentMapsLinkMetadata = MapsLinkMetadata(
|
||||||
|
url: mapsLink,
|
||||||
|
placeName: metadata?.title ?? '',
|
||||||
|
);
|
||||||
|
if (!silent) notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeCurrentMapsLink({bool silent = false}) {
|
||||||
|
_currentMapsLinkMetadata = null;
|
||||||
|
if (!silent) notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
MapsLinkMetadata? get currentMapsLinkMetadata => _currentMapsLinkMetadata;
|
||||||
|
}
|
||||||
158
lib/service/storage.dart
Normal file
158
lib/service/storage.dart
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import 'dart:convert' show jsonDecode, jsonEncode;
|
||||||
|
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../model/bookmark.dart';
|
||||||
|
import '../model/collection.dart';
|
||||||
|
|
||||||
|
class Storage {
|
||||||
|
static const String _bookmarksKey = 'bookmarks';
|
||||||
|
static const String _collectionsKey = 'collections';
|
||||||
|
static SharedPreferencesWithCache? _prefsWithCache;
|
||||||
|
static const String _statsKey = 'stats';
|
||||||
|
|
||||||
|
static Future<void> initialize() async {
|
||||||
|
_prefsWithCache = await SharedPreferencesWithCache.create(
|
||||||
|
cacheOptions: const SharedPreferencesWithCacheOptions(
|
||||||
|
allowList: <String>{_collectionsKey, _bookmarksKey, _statsKey},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Collection> loadCollections() {
|
||||||
|
final jsonString = _prefs.getString(_collectionsKey) ?? '[]';
|
||||||
|
final jsonList = jsonDecode(jsonString) as List;
|
||||||
|
return jsonList
|
||||||
|
.map((json) => Collection.fromJson(json as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Bookmark> loadBookmarks() {
|
||||||
|
final jsonString = _prefs.getString(_bookmarksKey) ?? '[]';
|
||||||
|
final jsonList = jsonDecode(jsonString) as List;
|
||||||
|
return jsonList
|
||||||
|
.map((json) => Bookmark.fromJson(json as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> saveCollections(List<Collection> collections) async {
|
||||||
|
final jsonList = collections.map((c) => c.toJson()).toList();
|
||||||
|
await _prefs.setString(_collectionsKey, jsonEncode(jsonList));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> saveBookmarks(List<Bookmark> bookmarks) async {
|
||||||
|
final jsonList = bookmarks.map((b) => b.toJson()).toList();
|
||||||
|
await _prefs.setString(_bookmarksKey, jsonEncode(jsonList));
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Bookmark> loadBookmarksForCollection(int collectionId) {
|
||||||
|
final allBookmarks = loadBookmarks();
|
||||||
|
return allBookmarks.where((b) => b.collectionId == collectionId).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> addBookmark(Bookmark bookmark) async {
|
||||||
|
final bookmarks = loadBookmarks();
|
||||||
|
bookmarks.add(bookmark);
|
||||||
|
await saveBookmarks(bookmarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> addOrUpdateBookmark(Bookmark bookmark) async {
|
||||||
|
final bookmarks = loadBookmarks();
|
||||||
|
final index = bookmarks.indexWhere((b) => b.id == bookmark.id);
|
||||||
|
|
||||||
|
if (index == -1) {
|
||||||
|
bookmarks.add(bookmark);
|
||||||
|
} else if (index >= 0) {
|
||||||
|
bookmarks[index] = bookmark;
|
||||||
|
}
|
||||||
|
await saveBookmarks(bookmarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> addOrUpdateCollection(Collection collection) async {
|
||||||
|
final collections = loadCollections();
|
||||||
|
final index = collections.indexWhere((b) => b.id == collection.id);
|
||||||
|
|
||||||
|
if (index == -1) {
|
||||||
|
collections.add(collection);
|
||||||
|
} else if (index >= 0) {
|
||||||
|
collections[index] = collection;
|
||||||
|
}
|
||||||
|
await saveCollections(collections);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> updateBookmarkById(
|
||||||
|
int bookmarkId, {
|
||||||
|
String? name,
|
||||||
|
String? description,
|
||||||
|
String? link,
|
||||||
|
}) async {
|
||||||
|
final bookmarks = loadBookmarks();
|
||||||
|
final index = bookmarks.indexWhere((b) => b.id == bookmarkId);
|
||||||
|
|
||||||
|
if (index == -1) return;
|
||||||
|
|
||||||
|
if (name != null) bookmarks[index].name = name;
|
||||||
|
if (description != null) bookmarks[index].description = description;
|
||||||
|
if (link != null) bookmarks[index].link = link;
|
||||||
|
|
||||||
|
await saveBookmarks(bookmarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> deleteBookmark(Bookmark bookmark) async {
|
||||||
|
final bookmarks = loadBookmarks();
|
||||||
|
bookmarks.remove(bookmark);
|
||||||
|
await saveBookmarks(bookmarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> deleteBookmarkById(int bookmarkId) async {
|
||||||
|
final bookmarks = loadBookmarks();
|
||||||
|
bookmarks.removeWhere((b) => b.id == bookmarkId);
|
||||||
|
await saveBookmarks(bookmarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> deleteBookmarksForCollection(int collectionId) async {
|
||||||
|
final bookmarks = loadBookmarks();
|
||||||
|
bookmarks.removeWhere((b) => b.collectionId == collectionId);
|
||||||
|
await saveBookmarks(bookmarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> deleteCollection(Collection collection) async {
|
||||||
|
final collections = loadCollections();
|
||||||
|
final bookmarks = loadBookmarks();
|
||||||
|
bookmarks.removeWhere((bookmark) => bookmark.collectionId == collection.id);
|
||||||
|
collections.remove(collection);
|
||||||
|
await saveBookmarks(bookmarks);
|
||||||
|
await saveCollections(collections);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, int> getStats() {
|
||||||
|
final statsJson = _prefs.getString(_statsKey) ?? '{}';
|
||||||
|
final stats = jsonDecode(statsJson) as Map<String, dynamic>;
|
||||||
|
return {
|
||||||
|
'totalCollections': stats['totalCollections'] ?? 0,
|
||||||
|
'totalBookmarks': stats['totalBookmarks'] ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> updateStats() async {
|
||||||
|
final collections = loadCollections();
|
||||||
|
final bookmarks = loadBookmarks();
|
||||||
|
|
||||||
|
final stats = {
|
||||||
|
'totalCollections': collections.length,
|
||||||
|
'totalBookmarks': bookmarks.length,
|
||||||
|
'lastUpdated': DateTime.now().millisecondsSinceEpoch,
|
||||||
|
};
|
||||||
|
|
||||||
|
await _prefs.setString(_statsKey, jsonEncode(stats));
|
||||||
|
}
|
||||||
|
|
||||||
|
static SharedPreferencesWithCache get _prefs {
|
||||||
|
if (_prefsWithCache == null) {
|
||||||
|
throw StateError(
|
||||||
|
'BookmarkStorage not initialized. Call initialize() first.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _prefsWithCache!;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
lib/service/url_launcher.dart
Normal file
14
lib/service/url_launcher.dart
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
Future<UrlLaunchErrorCode> launchUrlFromString(String url) async {
|
||||||
|
final Uri? uri = Uri.tryParse(url);
|
||||||
|
final isValid =
|
||||||
|
uri != null && uri.hasAbsolutePath && uri.scheme.startsWith('http');
|
||||||
|
if (!isValid) return UrlLaunchErrorCode.invalidUrl;
|
||||||
|
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
|
||||||
|
return UrlLaunchErrorCode.couldNotLaunch;
|
||||||
|
}
|
||||||
|
return UrlLaunchErrorCode.none;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UrlLaunchErrorCode { none, couldNotLaunch, invalidUrl }
|
||||||
15
lib/theme.dart
Normal file
15
lib/theme.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
ThemeData get lightTheme => ThemeData.from(colorScheme: _lightColorScheme);
|
||||||
|
|
||||||
|
ThemeData get darkTheme => ThemeData.from(colorScheme: _darkColorScheme);
|
||||||
|
|
||||||
|
ColorScheme get _darkColorScheme => ColorScheme.fromSeed(
|
||||||
|
seedColor: Colors.deepPurple,
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
);
|
||||||
|
|
||||||
|
ColorScheme get _lightColorScheme => ColorScheme.fromSeed(
|
||||||
|
seedColor: Colors.deepPurple,
|
||||||
|
brightness: Brightness.light,
|
||||||
|
);
|
||||||
@@ -1,14 +1,37 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../model/collection.dart';
|
||||||
|
import 'edit_dialog_widgets/edit_dialog_actions.dart' show EditDialogActions;
|
||||||
|
import 'edit_dialog_widgets/edit_dialog_title.dart';
|
||||||
|
|
||||||
class CreateBookmarkCollectionDialog extends StatelessWidget {
|
class CreateBookmarkCollectionDialog extends StatelessWidget {
|
||||||
const CreateBookmarkCollectionDialog({super.key});
|
const CreateBookmarkCollectionDialog({
|
||||||
|
super.key,
|
||||||
|
required this.onSavePressed,
|
||||||
|
this.onDeletePressed,
|
||||||
|
this.selectedCollection,
|
||||||
|
});
|
||||||
|
|
||||||
|
final void Function()? onDeletePressed;
|
||||||
|
final void Function(Collection collection) onSavePressed;
|
||||||
|
final Collection? selectedCollection;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final nameController = TextEditingController();
|
||||||
|
|
||||||
|
if (selectedCollection != null) {
|
||||||
|
nameController.text = selectedCollection!.name;
|
||||||
|
}
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text('Create Collection'),
|
title: EditDialogTitle(
|
||||||
|
dialogType: DialogType.collection,
|
||||||
|
onDeletePressed: onDeletePressed,
|
||||||
|
),
|
||||||
content: TextField(
|
content: TextField(
|
||||||
|
controller: nameController,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
maxLength: 50,
|
maxLength: 50,
|
||||||
@@ -17,12 +40,21 @@ class CreateBookmarkCollectionDialog extends StatelessWidget {
|
|||||||
FilteringTextInputFormatter.deny(RegExp(r'\s\s+')),
|
FilteringTextInputFormatter.deny(RegExp(r'\s\s+')),
|
||||||
],
|
],
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
// TODO: Localize
|
||||||
labelText: 'Collection Name',
|
labelText: 'Collection Name',
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
FloatingActionButton(onPressed: () {}, child: Icon(Icons.save)),
|
EditDialogActions(
|
||||||
|
onSavePressed: () {
|
||||||
|
final bookmark =
|
||||||
|
selectedCollection?.copyWith(name: nameController.text) ??
|
||||||
|
Collection(name: nameController.text);
|
||||||
|
onSavePressed(bookmark);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
127
lib/widgets/create_bookmark_dialog.dart
Normal file
127
lib/widgets/create_bookmark_dialog.dart
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../model/bookmark.dart';
|
||||||
|
import '../model/maps_link_metadata.dart';
|
||||||
|
import 'edit_dialog_widgets/edit_dialog_actions.dart';
|
||||||
|
import 'edit_dialog_widgets/edit_dialog_title.dart';
|
||||||
|
|
||||||
|
class CreateBookmarkDialog extends StatelessWidget {
|
||||||
|
const CreateBookmarkDialog({
|
||||||
|
super.key,
|
||||||
|
required this.collectionId,
|
||||||
|
this.onSavePressed,
|
||||||
|
this.onDeletePressed,
|
||||||
|
this.selectedBookmark,
|
||||||
|
this.selectedMapsLink,
|
||||||
|
});
|
||||||
|
|
||||||
|
final void Function(Bookmark bookmark)? onSavePressed;
|
||||||
|
final void Function()? onDeletePressed;
|
||||||
|
final int collectionId;
|
||||||
|
final Bookmark? selectedBookmark;
|
||||||
|
final MapsLinkMetadata? selectedMapsLink;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final nameController = TextEditingController();
|
||||||
|
final linkController = TextEditingController();
|
||||||
|
final descriptionController = TextEditingController();
|
||||||
|
|
||||||
|
if (selectedMapsLink != null) {
|
||||||
|
nameController.text = selectedMapsLink!.placeName;
|
||||||
|
linkController.text = selectedMapsLink!.url;
|
||||||
|
descriptionController.text = selectedMapsLink!.description;
|
||||||
|
} else if (selectedBookmark != null) {
|
||||||
|
nameController.text = selectedBookmark!.name;
|
||||||
|
linkController.text = selectedBookmark!.link;
|
||||||
|
descriptionController.text = selectedBookmark!.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: EditDialogTitle(
|
||||||
|
dialogType: DialogType.bookmark,
|
||||||
|
onDeletePressed: onDeletePressed,
|
||||||
|
),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(padding: EdgeInsetsGeometry.only(top: 10)),
|
||||||
|
TextField(
|
||||||
|
controller: nameController,
|
||||||
|
autofocus: true,
|
||||||
|
maxLines: 1,
|
||||||
|
maxLength: 50,
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.allow(
|
||||||
|
RegExp(r'[a-zA-Z0-9äöüÄÖÜß\s]'),
|
||||||
|
),
|
||||||
|
FilteringTextInputFormatter.deny(RegExp(r'\s\s+')),
|
||||||
|
],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Bookmark Title',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: linkController,
|
||||||
|
maxLines: 1,
|
||||||
|
maxLength: 50,
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.allow(
|
||||||
|
RegExp(r'[a-zA-Z0-9äöüÄÖÜß\s/:\.]'),
|
||||||
|
),
|
||||||
|
FilteringTextInputFormatter.deny(RegExp(r'\s\s+')),
|
||||||
|
],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Url',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
controller: descriptionController,
|
||||||
|
maxLines: 3,
|
||||||
|
maxLength: 300,
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.allow(
|
||||||
|
RegExp(r'[a-zA-Z0-9äöüÄÖÜß\s]'),
|
||||||
|
),
|
||||||
|
FilteringTextInputFormatter.deny(RegExp(r'\s\s+')),
|
||||||
|
],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Description',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
EditDialogActions(
|
||||||
|
onSavePressed: () {
|
||||||
|
final bookmark =
|
||||||
|
selectedBookmark?.copyWith(
|
||||||
|
name: nameController.text,
|
||||||
|
link: linkController.text,
|
||||||
|
description: descriptionController.text,
|
||||||
|
) ??
|
||||||
|
Bookmark(
|
||||||
|
collectionId: collectionId,
|
||||||
|
name: nameController.text,
|
||||||
|
link: linkController.text,
|
||||||
|
description: descriptionController.text,
|
||||||
|
);
|
||||||
|
onSavePressed?.call(bookmark);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
lib/widgets/edit_dialog_widgets/edit_dialog_actions.dart
Normal file
22
lib/widgets/edit_dialog_widgets/edit_dialog_actions.dart
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import 'package:flutter/material.dart'
|
||||||
|
show TextButton, FloatingActionButton, Icons;
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class EditDialogActions extends StatelessWidget {
|
||||||
|
const EditDialogActions({super.key, required this.onSavePressed});
|
||||||
|
final VoidCallback onSavePressed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: Text('Cancel'),
|
||||||
|
),
|
||||||
|
FloatingActionButton(onPressed: onSavePressed, child: Icon(Icons.save)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
lib/widgets/edit_dialog_widgets/edit_dialog_title.dart
Normal file
40
lib/widgets/edit_dialog_widgets/edit_dialog_title.dart
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import 'package:flutter/material.dart' show TextButton, Theme;
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class EditDialogTitle extends StatelessWidget {
|
||||||
|
const EditDialogTitle({
|
||||||
|
super.key,
|
||||||
|
this.onDeletePressed,
|
||||||
|
required this.dialogType,
|
||||||
|
});
|
||||||
|
final VoidCallback? onDeletePressed;
|
||||||
|
final DialogType dialogType;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
// TODO: Localize
|
||||||
|
if (dialogType == DialogType.bookmark)
|
||||||
|
Text('Create Bookmark')
|
||||||
|
else
|
||||||
|
Text('Create Collection'),
|
||||||
|
|
||||||
|
if (onDeletePressed != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
onDeletePressed!.call();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Delete',
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DialogType { bookmark, collection }
|
||||||
34
lib/widgets/search_widgets/search_bar_widget.dart
Normal file
34
lib/widgets/search_widgets/search_bar_widget.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SearchBarWidget extends StatelessWidget {
|
||||||
|
const SearchBarWidget({
|
||||||
|
super.key,
|
||||||
|
required this.onEditingComplete,
|
||||||
|
required this.onResetSearch,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Function(String searchString) onEditingComplete;
|
||||||
|
final Function() onResetSearch;
|
||||||
|
|
||||||
|
void onChanged(String text, BuildContext context) {
|
||||||
|
if (context.mounted) onEditingComplete(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final searchTextController = TextEditingController();
|
||||||
|
return TextField(
|
||||||
|
controller: searchTextController,
|
||||||
|
onChanged: (text) => onChanged(text, context),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
searchTextController.clear();
|
||||||
|
onResetSearch();
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.delete_outline_outlined),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
lib/widgets/search_widgets/search_results_widget.dart
Normal file
56
lib/widgets/search_widgets/search_results_widget.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../model/bookmark.dart';
|
||||||
|
import '../../service/notifying.dart';
|
||||||
|
import '../../service/search_provider.dart';
|
||||||
|
import '../../service/storage.dart' show Storage;
|
||||||
|
import '../../service/url_launcher.dart';
|
||||||
|
|
||||||
|
class SearchResultsWidget extends StatefulWidget {
|
||||||
|
const SearchResultsWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SearchResultsWidget> createState() => _SearchResultsWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SearchResultsWidgetState extends State<SearchResultsWidget> {
|
||||||
|
final List<Bookmark> allBookmarks = Storage.loadBookmarks();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void deactivate() {
|
||||||
|
context.read<SearchProvider>().removeSearchText(silent: true);
|
||||||
|
super.deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget bookmarkListItemBuilder(BuildContext context, int index) {
|
||||||
|
final bookmark = filteredBookmarks.elementAt(index);
|
||||||
|
return ListTile(
|
||||||
|
title: Text(bookmark.name),
|
||||||
|
onTap: () => launchUrlFromString(bookmark.link).then((errorCode) {
|
||||||
|
if (context.mounted && errorCode != UrlLaunchErrorCode.none) {
|
||||||
|
Notifying.showUrlErrorSnackbar(context, errorCode);
|
||||||
|
} else if (context.mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<Bookmark> get filteredBookmarks => allBookmarks.where(
|
||||||
|
(bookmark) => bookmark.name.toLowerCase().contains(
|
||||||
|
context.watch<SearchProvider>().searchText.toLowerCase(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (filteredBookmarks.isNotEmpty) {
|
||||||
|
return ListView.builder(
|
||||||
|
itemBuilder: bookmarkListItemBuilder,
|
||||||
|
itemCount: filteredBookmarks.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Center(child: Text('Start searching'));
|
||||||
|
}
|
||||||
|
}
|
||||||
279
pubspec.lock
279
pubspec.lock
@@ -49,6 +49,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -65,6 +73,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
version: "1.3.3"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -74,15 +98,44 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: flutter_lints
|
name: flutter_lints
|
||||||
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
|
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "6.0.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_web_plugins:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.6"
|
||||||
|
http:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http
|
||||||
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.6.0"
|
||||||
|
http_parser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: http_parser
|
||||||
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.2"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -111,10 +164,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: lints
|
name: lints
|
||||||
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
|
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.1"
|
version: "6.0.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -135,10 +188,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.17.0"
|
||||||
|
metadata_fetch:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: metadata_fetch
|
||||||
|
sha256: "24a713eaddbebea3dc3036a6c1d6f7c57e187fff5f0ef07be3e3ebbb7820c3e7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.2"
|
||||||
|
nested:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: nested
|
||||||
|
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -147,6 +216,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
|
path_provider_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_linux
|
||||||
|
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.1"
|
||||||
|
path_provider_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_platform_interface
|
||||||
|
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
path_provider_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_windows
|
||||||
|
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -155,6 +248,78 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.6"
|
version: "3.1.6"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
|
provider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: provider
|
||||||
|
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.5+1"
|
||||||
|
shared_preferences:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shared_preferences
|
||||||
|
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.3"
|
||||||
|
shared_preferences_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_android
|
||||||
|
sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.18"
|
||||||
|
shared_preferences_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_foundation
|
||||||
|
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.6"
|
||||||
|
shared_preferences_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_linux
|
||||||
|
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_platform_interface
|
||||||
|
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_web
|
||||||
|
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.3"
|
||||||
|
shared_preferences_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_windows
|
||||||
|
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -192,6 +357,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
|
string_validator:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_validator
|
||||||
|
sha256: "240f4c98027dfbe8639c8271ef18cc9de735b47067aa15a720cfed9576a512b1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -204,10 +377,82 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "0.7.7"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
url_launcher:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: url_launcher
|
||||||
|
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.2"
|
||||||
|
url_launcher_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_android
|
||||||
|
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.28"
|
||||||
|
url_launcher_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_ios
|
||||||
|
sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.3.6"
|
||||||
|
url_launcher_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_linux
|
||||||
|
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.2"
|
||||||
|
url_launcher_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_macos
|
||||||
|
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.5"
|
||||||
|
url_launcher_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_platform_interface
|
||||||
|
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
url_launcher_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_web
|
||||||
|
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
url_launcher_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_windows
|
||||||
|
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.5"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -224,6 +469,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.2"
|
version: "15.0.2"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
|
xdg_directories:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xdg_directories
|
||||||
|
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.9.2 <4.0.0"
|
dart: ">=3.9.2 <4.0.0"
|
||||||
flutter: ">=3.18.0-18.0.pre.54"
|
flutter: ">=3.35.0"
|
||||||
|
|||||||
@@ -14,12 +14,16 @@ dependencies:
|
|||||||
|
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
android_intent_plus: ^6.0.0
|
android_intent_plus: ^6.0.0
|
||||||
|
shared_preferences: ^2.3.2
|
||||||
|
provider: ^6.1.5+1
|
||||||
|
metadata_fetch: ^0.4.2
|
||||||
|
url_launcher: ^6.3.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user