Compare commits
46 Commits
2d93d1a9d7
...
v0.1.25
| Author | SHA1 | Date | |
|---|---|---|---|
| dca8c64555 | |||
| c80606b7d0 | |||
| be6020d6c5 | |||
| 5eb58d7cf2 | |||
| 306a38a36a | |||
| 1aaea5f6d9 | |||
| 99a8aaa409 | |||
| 85d57c6e1c | |||
| 77a647d17d | |||
| 3a54a077f3 | |||
| cf88a9a371 | |||
| 5feb535cf3 | |||
| 2d23207497 | |||
| 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 |
71
.gitea/workflows/android_build.yml
Normal file
71
.gitea/workflows/android_build.yml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
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: 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: "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"
|
||||||
@@ -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>
|
||||||
|
|||||||
3
l10n.yaml
Normal file
3
l10n.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
arb-dir: lib/l10n
|
||||||
|
template-arb-file: app_en.arb
|
||||||
|
output-localization-file: app_localizations.dart
|
||||||
27
lib/l10n/app_de.arb
Normal file
27
lib/l10n/app_de.arb
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"@@locale": "de",
|
||||||
|
"addToCollection": "Speichern in {collection_name}",
|
||||||
|
"@addToCollection": {
|
||||||
|
"placeholders": {
|
||||||
|
"collection_name" : {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"chooseCollection": "Sammlung auswählen",
|
||||||
|
"collections": "Sammlungen",
|
||||||
|
"tipCreateCollections": "Erstelle deine erste Sammlung!",
|
||||||
|
"search": "Suche",
|
||||||
|
"createBookmark": "Lesezeichen erstellen",
|
||||||
|
"createCollection": "Sammlung erstellen",
|
||||||
|
"create": "Erstellen",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"add": "Hinzufügen",
|
||||||
|
"startSearching": "Suche etwas",
|
||||||
|
"tipNoResults": "Keine Suchergebnisse gefunden",
|
||||||
|
"collectionName": "Name der Sammlung",
|
||||||
|
"bookmarkTitle": "Titel des Lesezeichens",
|
||||||
|
"url": "Url",
|
||||||
|
"description": "Beschreibung"
|
||||||
|
}
|
||||||
27
lib/l10n/app_en.arb
Normal file
27
lib/l10n/app_en.arb
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"@@locale": "en",
|
||||||
|
"addToCollection": "Add to {collection_name}",
|
||||||
|
"@addToCollection": {
|
||||||
|
"placeholders": {
|
||||||
|
"collection_name" : {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"chooseCollection": "Choose Collection",
|
||||||
|
"collections": "Collections",
|
||||||
|
"tipCreateCollections": "Create your first Collection to get started!",
|
||||||
|
"search": "Search",
|
||||||
|
"createBookmark": "Create Bookmark",
|
||||||
|
"createCollection": "Create Collection",
|
||||||
|
"create": "Create",
|
||||||
|
"delete": "Delete",
|
||||||
|
"add": "Add",
|
||||||
|
"startSearching": "Start searching",
|
||||||
|
"tipNoResults": "There are no results that match your search",
|
||||||
|
"collectionName": "Collection Name",
|
||||||
|
"bookmarkTitle": "Bookmark Title",
|
||||||
|
"url": "Url",
|
||||||
|
"description": "Description"
|
||||||
|
}
|
||||||
236
lib/l10n/app_localizations.dart
Normal file
236
lib/l10n/app_localizations.dart
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:intl/intl.dart' as intl;
|
||||||
|
|
||||||
|
import 'app_localizations_de.dart';
|
||||||
|
import 'app_localizations_en.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
|
||||||
|
/// Callers can lookup localized strings with an instance of AppLocalizations
|
||||||
|
/// returned by `AppLocalizations.of(context)`.
|
||||||
|
///
|
||||||
|
/// Applications need to include `AppLocalizations.delegate()` in their app's
|
||||||
|
/// `localizationDelegates` list, and the locales they support in the app's
|
||||||
|
/// `supportedLocales` list. For example:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// import 'l10n/app_localizations.dart';
|
||||||
|
///
|
||||||
|
/// return MaterialApp(
|
||||||
|
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
/// supportedLocales: AppLocalizations.supportedLocales,
|
||||||
|
/// home: MyApplicationHome(),
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Update pubspec.yaml
|
||||||
|
///
|
||||||
|
/// Please make sure to update your pubspec.yaml to include the following
|
||||||
|
/// packages:
|
||||||
|
///
|
||||||
|
/// ```yaml
|
||||||
|
/// dependencies:
|
||||||
|
/// # Internationalization support.
|
||||||
|
/// flutter_localizations:
|
||||||
|
/// sdk: flutter
|
||||||
|
/// intl: any # Use the pinned version from flutter_localizations
|
||||||
|
///
|
||||||
|
/// # Rest of dependencies
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## iOS Applications
|
||||||
|
///
|
||||||
|
/// iOS applications define key application metadata, including supported
|
||||||
|
/// locales, in an Info.plist file that is built into the application bundle.
|
||||||
|
/// To configure the locales supported by your app, you’ll need to edit this
|
||||||
|
/// file.
|
||||||
|
///
|
||||||
|
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
||||||
|
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
||||||
|
/// project’s Runner folder.
|
||||||
|
///
|
||||||
|
/// Next, select the Information Property List item, select Add Item from the
|
||||||
|
/// Editor menu, then select Localizations from the pop-up menu.
|
||||||
|
///
|
||||||
|
/// Select and expand the newly-created Localizations item then, for each
|
||||||
|
/// locale your application supports, add a new item and select the locale
|
||||||
|
/// you wish to add from the pop-up menu in the Value field. This list should
|
||||||
|
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
||||||
|
/// property.
|
||||||
|
abstract class AppLocalizations {
|
||||||
|
AppLocalizations(String locale)
|
||||||
|
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||||
|
|
||||||
|
final String localeName;
|
||||||
|
|
||||||
|
static AppLocalizations? of(BuildContext context) {
|
||||||
|
return Localizations.of<AppLocalizations>(context, AppLocalizations);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const LocalizationsDelegate<AppLocalizations> delegate =
|
||||||
|
_AppLocalizationsDelegate();
|
||||||
|
|
||||||
|
/// A list of this localizations delegate along with the default localizations
|
||||||
|
/// delegates.
|
||||||
|
///
|
||||||
|
/// Returns a list of localizations delegates containing this delegate along with
|
||||||
|
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
|
||||||
|
/// and GlobalWidgetsLocalizations.delegate.
|
||||||
|
///
|
||||||
|
/// Additional delegates can be added by appending to this list in
|
||||||
|
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||||
|
/// of delegates is preferred or required.
|
||||||
|
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||||
|
<LocalizationsDelegate<dynamic>>[
|
||||||
|
delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A list of this localizations delegate's supported locales.
|
||||||
|
static const List<Locale> supportedLocales = <Locale>[
|
||||||
|
Locale('de'),
|
||||||
|
Locale('en'),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// No description provided for @addToCollection.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add to {collection_name}'**
|
||||||
|
String addToCollection(String collection_name);
|
||||||
|
|
||||||
|
/// No description provided for @cancel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Cancel'**
|
||||||
|
String get cancel;
|
||||||
|
|
||||||
|
/// No description provided for @chooseCollection.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Choose Collection'**
|
||||||
|
String get chooseCollection;
|
||||||
|
|
||||||
|
/// No description provided for @collections.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Collections'**
|
||||||
|
String get collections;
|
||||||
|
|
||||||
|
/// No description provided for @tipCreateCollections.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Create your first Collection to get started!'**
|
||||||
|
String get tipCreateCollections;
|
||||||
|
|
||||||
|
/// No description provided for @search.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Search'**
|
||||||
|
String get search;
|
||||||
|
|
||||||
|
/// No description provided for @createBookmark.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Create Bookmark'**
|
||||||
|
String get createBookmark;
|
||||||
|
|
||||||
|
/// No description provided for @createCollection.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Create Collection'**
|
||||||
|
String get createCollection;
|
||||||
|
|
||||||
|
/// No description provided for @create.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Create'**
|
||||||
|
String get create;
|
||||||
|
|
||||||
|
/// No description provided for @delete.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Delete'**
|
||||||
|
String get delete;
|
||||||
|
|
||||||
|
/// No description provided for @add.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add'**
|
||||||
|
String get add;
|
||||||
|
|
||||||
|
/// No description provided for @startSearching.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Start searching'**
|
||||||
|
String get startSearching;
|
||||||
|
|
||||||
|
/// No description provided for @tipNoResults.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'There are no results that match your search'**
|
||||||
|
String get tipNoResults;
|
||||||
|
|
||||||
|
/// No description provided for @collectionName.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Collection Name'**
|
||||||
|
String get collectionName;
|
||||||
|
|
||||||
|
/// No description provided for @bookmarkTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Bookmark Title'**
|
||||||
|
String get bookmarkTitle;
|
||||||
|
|
||||||
|
/// No description provided for @url.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Url'**
|
||||||
|
String get url;
|
||||||
|
|
||||||
|
/// No description provided for @description.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Description'**
|
||||||
|
String get description;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppLocalizationsDelegate
|
||||||
|
extends LocalizationsDelegate<AppLocalizations> {
|
||||||
|
const _AppLocalizationsDelegate();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<AppLocalizations> load(Locale locale) {
|
||||||
|
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isSupported(Locale locale) =>
|
||||||
|
<String>['de', 'en'].contains(locale.languageCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||||
|
// Lookup logic when only language code is specified.
|
||||||
|
switch (locale.languageCode) {
|
||||||
|
case 'de':
|
||||||
|
return AppLocalizationsDe();
|
||||||
|
case 'en':
|
||||||
|
return AppLocalizationsEn();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw FlutterError(
|
||||||
|
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||||
|
'an issue with the localizations generation tool. Please file an issue '
|
||||||
|
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||||
|
'that was used.',
|
||||||
|
);
|
||||||
|
}
|
||||||
63
lib/l10n/app_localizations_de.dart
Normal file
63
lib/l10n/app_localizations_de.dart
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// ignore: unused_import
|
||||||
|
import 'package:intl/intl.dart' as intl;
|
||||||
|
import 'app_localizations.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
|
||||||
|
/// The translations for German (`de`).
|
||||||
|
class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
AppLocalizationsDe([String locale = 'de']) : super(locale);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String addToCollection(String collection_name) {
|
||||||
|
return 'Speichern in $collection_name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get cancel => 'Abbrechen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chooseCollection => 'Sammlung auswählen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get collections => 'Sammlungen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tipCreateCollections => 'Erstelle deine erste Sammlung!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get search => 'Suche';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createBookmark => 'Lesezeichen erstellen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createCollection => 'Sammlung erstellen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get create => 'Erstellen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get delete => 'Löschen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get add => 'Hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get startSearching => 'Suche etwas';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tipNoResults => 'Keine Suchergebnisse gefunden';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get collectionName => 'Name der Sammlung';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get bookmarkTitle => 'Titel des Lesezeichens';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get url => 'Url';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get description => 'Beschreibung';
|
||||||
|
}
|
||||||
64
lib/l10n/app_localizations_en.dart
Normal file
64
lib/l10n/app_localizations_en.dart
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// ignore: unused_import
|
||||||
|
import 'package:intl/intl.dart' as intl;
|
||||||
|
import 'app_localizations.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
|
||||||
|
/// The translations for English (`en`).
|
||||||
|
class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String addToCollection(String collection_name) {
|
||||||
|
return 'Add to $collection_name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get cancel => 'Cancel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chooseCollection => 'Choose Collection';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get collections => 'Collections';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tipCreateCollections =>
|
||||||
|
'Create your first Collection to get started!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get search => 'Search';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createBookmark => 'Create Bookmark';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createCollection => 'Create Collection';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get create => 'Create';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get delete => 'Delete';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get add => 'Add';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get startSearching => 'Start searching';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tipNoResults => 'There are no results that match your search';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get collectionName => 'Collection Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get bookmarkTitle => 'Bookmark Title';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get url => 'Url';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get description => 'Description';
|
||||||
|
}
|
||||||
@@ -1,27 +1,88 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'pages/bookmarks_page.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'pages/collections_page.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'l10n/app_localizations.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/storage.dart';
|
||||||
|
import 'service/share_intent_service.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await Storage.initialize();
|
await Storage.initialize();
|
||||||
runApp(const MapsBookmarks());
|
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(
|
localizationsDelegates: [
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
|
AppLocalizations.delegate,
|
||||||
),
|
GlobalMaterialLocalizations.delegate,
|
||||||
initialRoute: CollectionsPage.routeName,
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: [Locale('en'), Locale('de')],
|
||||||
|
navigatorKey: _navigatorKey,
|
||||||
|
theme: lightTheme,
|
||||||
|
darkTheme: darkTheme,
|
||||||
|
initialRoute: CollectionsListPage.routeName,
|
||||||
routes: {
|
routes: {
|
||||||
CollectionsPage.routeName: (context) => const CollectionsPage(),
|
CollectionsListPage.routeName: (context) => const CollectionsListPage(),
|
||||||
BookmarksPage.routeName: (context) => const BookmarksPage(),
|
CollectionPage.routeName: (context) => const CollectionPage(),
|
||||||
|
SearchPage.routeName: (context) => const SearchPage(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,4 +32,14 @@ class Bookmark {
|
|||||||
'description': description,
|
'description': description,
|
||||||
'createdAt': createdAt,
|
'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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,28 @@ class Collection {
|
|||||||
createdAt: json['createdAt'] as int,
|
createdAt: json['createdAt'] as int,
|
||||||
);
|
);
|
||||||
|
|
||||||
String name;
|
|
||||||
int createdAt; // used as Id with millisecondsSinceEpoch
|
int createdAt; // used as Id with millisecondsSinceEpoch
|
||||||
|
String 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;
|
int get id => createdAt;
|
||||||
|
|
||||||
DateTime get createdDate => DateTime.fromMillisecondsSinceEpoch(createdAt);
|
DateTime get createdDate => DateTime.fromMillisecondsSinceEpoch(createdAt);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {'name': name, 'createdAt': 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;
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../service/bookmarks_provider.dart';
|
|
||||||
import '../service/storage.dart';
|
|
||||||
|
|
||||||
class BookmarksPage extends StatelessWidget {
|
|
||||||
const BookmarksPage({super.key});
|
|
||||||
|
|
||||||
static const String routeName = '/bookmarks';
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (BookmarksProvider.selectedCollectionId == null) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
final bookmarks = Storage.loadBookmarksForCollection(
|
|
||||||
BookmarksProvider.selectedCollectionId!,
|
|
||||||
);
|
|
||||||
|
|
||||||
BookmarksProvider.selectedCollectionId == null;
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(
|
|
||||||
Storage.loadCollections()
|
|
||||||
.firstWhere((c) => c.id == BookmarksProvider.selectedCollectionId)
|
|
||||||
.name,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: ListView(
|
|
||||||
children: bookmarks.map((e) => ListTile(title: Text(e.name))).toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
122
lib/pages/collection_page.dart
Normal file
122
lib/pages/collection_page.dart
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../l10n/app_localizations.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(
|
||||||
|
AppLocalizations.of(context)!.addToCollection(collection.name),
|
||||||
|
)
|
||||||
|
: Text(collection.name),
|
||||||
|
actions: [
|
||||||
|
if (selectedMapsLink != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => provider.removeCurrentMapsLink(),
|
||||||
|
child: Text(AppLocalizations.of(context)!.cancel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.9,
|
||||||
|
child: ListView.separated(
|
||||||
|
itemBuilder: (context, index) =>
|
||||||
|
bookmarksListItemBuilder(context, bookmarks.elementAt(index)),
|
||||||
|
itemCount: bookmarks.length,
|
||||||
|
separatorBuilder: (context, index) => SizedBox(height: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: onAddButtonPressed,
|
||||||
|
child: Icon(selectedMapsLink != null ? Icons.save : Icons.add),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
131
lib/pages/collections_list_page.dart
Normal file
131
lib/pages/collections_list_page.dart
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../l10n/app_localizations.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;
|
||||||
|
final bookmarkCountMap = Storage.loadPerCollectionBookmarkCount();
|
||||||
|
|
||||||
|
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),
|
||||||
|
leading: const Icon(Icons.list_rounded),
|
||||||
|
trailing: Text(
|
||||||
|
bookmarkCountMap[collection.id]?.toString() ?? '0',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(AppLocalizations.of(context)!.chooseCollection)
|
||||||
|
: Text(AppLocalizations.of(context)!.collections),
|
||||||
|
actions: [
|
||||||
|
if (addingNewBookmark)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => provider.removeCurrentMapsLink(),
|
||||||
|
child: Text(AppLocalizations.of(context)!.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
|
||||||
|
? Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.9,
|
||||||
|
child: ListView.separated(
|
||||||
|
itemBuilder: (context, index) => collectionsListItemBuilder(
|
||||||
|
context,
|
||||||
|
collections.elementAt(index),
|
||||||
|
),
|
||||||
|
itemCount: collections.length,
|
||||||
|
separatorBuilder: (context, index) => SizedBox(height: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Center(
|
||||||
|
child: Text(AppLocalizations.of(context)!.tipCreateCollections),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../model/collection.dart';
|
|
||||||
import '../service/bookmarks_provider.dart';
|
|
||||||
import '../service/storage.dart';
|
|
||||||
import '../widgets/create_bookmark_collection_dialog.dart';
|
|
||||||
import 'bookmarks_page.dart';
|
|
||||||
|
|
||||||
class CollectionsPage extends StatefulWidget {
|
|
||||||
const CollectionsPage({super.key});
|
|
||||||
static const String routeName = '/collections';
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CollectionsPage> createState() => _CollectionsPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CollectionsPageState extends State<CollectionsPage> {
|
|
||||||
final collections = Storage.loadCollections();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: onAddButtonPressed,
|
|
||||||
child: Icon(Icons.add),
|
|
||||||
),
|
|
||||||
body: ListView.builder(
|
|
||||||
itemBuilder: itemBuilder,
|
|
||||||
itemCount: collections.length,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onAddButtonPressed() => showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) =>
|
|
||||||
CreateBookmarkCollectionDialog(onSavePressed: onCollectionSaved),
|
|
||||||
);
|
|
||||||
|
|
||||||
void onCollectionSaved(String name) {
|
|
||||||
collections.add(Collection(name: name));
|
|
||||||
setState(() {});
|
|
||||||
Storage.saveCollections(collections);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget itemBuilder(BuildContext context, int index) {
|
|
||||||
final collection = collections.elementAt(index);
|
|
||||||
return ListTile(
|
|
||||||
title: Text(collection.name),
|
|
||||||
onTap: () {
|
|
||||||
BookmarksProvider.selectedCollectionId = collection.id;
|
|
||||||
Navigator.pushNamed(context, BookmarksPage.routeName);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
35
lib/pages/search_page.dart
Normal file
35
lib/pages/search_page.dart
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../l10n/app_localizations.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(AppLocalizations.of(context)!.search)),
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.9,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(padding: EdgeInsetsGeometry.only(top: 10)),
|
||||||
|
SearchBarWidget(
|
||||||
|
onEditingComplete: context.read<SearchProvider>().setSearchText,
|
||||||
|
onResetSearch: context.read<SearchProvider>().removeSearchText,
|
||||||
|
),
|
||||||
|
Padding(padding: EdgeInsetsGeometry.only(top: 10)),
|
||||||
|
Expanded(child: SearchResultsWidget()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
@@ -6,10 +6,10 @@ import '../model/bookmark.dart';
|
|||||||
import '../model/collection.dart';
|
import '../model/collection.dart';
|
||||||
|
|
||||||
class Storage {
|
class Storage {
|
||||||
static const String _collectionsKey = 'collections';
|
|
||||||
static const String _bookmarksKey = 'bookmarks';
|
static const String _bookmarksKey = 'bookmarks';
|
||||||
static const String _statsKey = 'stats';
|
static const String _collectionsKey = 'collections';
|
||||||
static SharedPreferencesWithCache? _prefsWithCache;
|
static SharedPreferencesWithCache? _prefsWithCache;
|
||||||
|
static const String _statsKey = 'stats';
|
||||||
|
|
||||||
static Future<void> initialize() async {
|
static Future<void> initialize() async {
|
||||||
_prefsWithCache = await SharedPreferencesWithCache.create(
|
_prefsWithCache = await SharedPreferencesWithCache.create(
|
||||||
@@ -19,15 +19,6 @@ class Storage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static SharedPreferencesWithCache get _prefs {
|
|
||||||
if (_prefsWithCache == null) {
|
|
||||||
throw StateError(
|
|
||||||
'BookmarkStorage not initialized. Call initialize() first.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return _prefsWithCache!;
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<Collection> loadCollections() {
|
static List<Collection> loadCollections() {
|
||||||
final jsonString = _prefs.getString(_collectionsKey) ?? '[]';
|
final jsonString = _prefs.getString(_collectionsKey) ?? '[]';
|
||||||
final jsonList = jsonDecode(jsonString) as List;
|
final jsonList = jsonDecode(jsonString) as List;
|
||||||
@@ -36,12 +27,7 @@ class Storage {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> saveCollections(List<Collection> collections) async {
|
static List<Bookmark> loadBookmarks() {
|
||||||
final jsonList = collections.map((c) => c.toJson()).toList();
|
|
||||||
await _prefs.setString(_collectionsKey, jsonEncode(jsonList));
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<Bookmark> loadAllBookmarks() {
|
|
||||||
final jsonString = _prefs.getString(_bookmarksKey) ?? '[]';
|
final jsonString = _prefs.getString(_bookmarksKey) ?? '[]';
|
||||||
final jsonList = jsonDecode(jsonString) as List;
|
final jsonList = jsonDecode(jsonString) as List;
|
||||||
return jsonList
|
return jsonList
|
||||||
@@ -49,48 +35,102 @@ class Storage {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> saveAllBookmarks(List<Bookmark> bookmarks) async {
|
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();
|
final jsonList = bookmarks.map((b) => b.toJson()).toList();
|
||||||
await _prefs.setString(_bookmarksKey, jsonEncode(jsonList));
|
await _prefs.setString(_bookmarksKey, jsonEncode(jsonList));
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Bookmark> loadBookmarksForCollection(int collectionId) {
|
static List<Bookmark> loadBookmarksForCollection(int collectionId) {
|
||||||
final allBookmarks = loadAllBookmarks();
|
final allBookmarks = loadBookmarks();
|
||||||
return allBookmarks.where((b) => b.collectionId == collectionId).toList();
|
return allBookmarks.where((b) => b.collectionId == collectionId).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Map<int, int> loadPerCollectionBookmarkCount() {
|
||||||
|
return loadBookmarks().fold(<int, int>{}, (map, bookmark) {
|
||||||
|
map[bookmark.collectionId] ??= 0;
|
||||||
|
map[bookmark.collectionId] = map[bookmark.collectionId]! + 1;
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static Future<void> addBookmark(Bookmark bookmark) async {
|
static Future<void> addBookmark(Bookmark bookmark) async {
|
||||||
final bookmarks = loadAllBookmarks();
|
final bookmarks = loadBookmarks();
|
||||||
bookmarks.add(bookmark);
|
bookmarks.add(bookmark);
|
||||||
await saveAllBookmarks(bookmarks);
|
await saveBookmarks(bookmarks);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> deleteBookmarkById(int bookmarkId) async {
|
static Future<void> addOrUpdateBookmark(Bookmark bookmark) async {
|
||||||
final bookmarks = loadAllBookmarks();
|
final bookmarks = loadBookmarks();
|
||||||
bookmarks.removeWhere((b) => b.id == bookmarkId);
|
final index = bookmarks.indexWhere((b) => b.id == bookmark.id);
|
||||||
await saveAllBookmarks(bookmarks);
|
|
||||||
|
if (index == -1) {
|
||||||
|
bookmarks.add(bookmark);
|
||||||
|
} else if (index >= 0) {
|
||||||
|
bookmarks[index] = bookmark;
|
||||||
|
}
|
||||||
|
await saveBookmarks(bookmarks);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> deleteBookmarksForCollection(int collectionId) async {
|
static Future<void> addOrUpdateCollection(Collection collection) async {
|
||||||
final bookmarks = loadAllBookmarks();
|
final collections = loadCollections();
|
||||||
bookmarks.removeWhere((b) => b.collectionId == collectionId);
|
final index = collections.indexWhere((b) => b.id == collection.id);
|
||||||
await saveAllBookmarks(bookmarks);
|
|
||||||
|
if (index == -1) {
|
||||||
|
collections.add(collection);
|
||||||
|
} else if (index >= 0) {
|
||||||
|
collections[index] = collection;
|
||||||
|
}
|
||||||
|
await saveCollections(collections);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> updateBookmarkById(
|
static Future<void> updateBookmarkById(
|
||||||
int bookmarkId, {
|
int bookmarkId, {
|
||||||
String? name,
|
String? name,
|
||||||
String? description,
|
String? description,
|
||||||
|
String? link,
|
||||||
}) async {
|
}) async {
|
||||||
final bookmarks = loadAllBookmarks();
|
final bookmarks = loadBookmarks();
|
||||||
final index = bookmarks.indexWhere((b) => b.id == bookmarkId);
|
final index = bookmarks.indexWhere((b) => b.id == bookmarkId);
|
||||||
|
|
||||||
if (index == -1) return;
|
if (index == -1) return;
|
||||||
|
|
||||||
if (name != null) bookmarks[index].name = name;
|
if (name != null) bookmarks[index].name = name;
|
||||||
if (description != null) bookmarks[index].description = description;
|
if (description != null) bookmarks[index].description = description;
|
||||||
|
if (link != null) bookmarks[index].link = link;
|
||||||
|
|
||||||
await saveAllBookmarks(bookmarks);
|
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() {
|
static Map<String, int> getStats() {
|
||||||
@@ -104,7 +144,7 @@ class Storage {
|
|||||||
|
|
||||||
static Future<void> updateStats() async {
|
static Future<void> updateStats() async {
|
||||||
final collections = loadCollections();
|
final collections = loadCollections();
|
||||||
final bookmarks = loadAllBookmarks();
|
final bookmarks = loadBookmarks();
|
||||||
|
|
||||||
final stats = {
|
final stats = {
|
||||||
'totalCollections': collections.length,
|
'totalCollections': collections.length,
|
||||||
@@ -114,4 +154,13 @@ class Storage {
|
|||||||
|
|
||||||
await _prefs.setString(_statsKey, jsonEncode(stats));
|
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 }
|
||||||
26
lib/theme.dart
Normal file
26
lib/theme.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
const _seed = Colors.deepPurple;
|
||||||
|
|
||||||
|
ColorScheme get _lightColorScheme =>
|
||||||
|
ColorScheme.fromSeed(seedColor: _seed, brightness: Brightness.light);
|
||||||
|
|
||||||
|
ColorScheme get _darkColorScheme =>
|
||||||
|
ColorScheme.fromSeed(seedColor: _seed, brightness: Brightness.dark);
|
||||||
|
|
||||||
|
ThemeData get lightTheme => _baseTheme(_lightColorScheme);
|
||||||
|
|
||||||
|
ThemeData get darkTheme => _baseTheme(_darkColorScheme);
|
||||||
|
|
||||||
|
ThemeData _baseTheme(ColorScheme scheme) =>
|
||||||
|
ThemeData.from(colorScheme: scheme, useMaterial3: true).copyWith(
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
listTileTheme: ListTileThemeData(
|
||||||
|
tileColor: scheme.surfaceContainer,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadiusGeometry.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -1,18 +1,36 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../l10n/app_localizations.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({
|
const CreateBookmarkCollectionDialog({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onSavePressed,
|
required this.onSavePressed,
|
||||||
|
this.onDeletePressed,
|
||||||
|
this.selectedCollection,
|
||||||
});
|
});
|
||||||
final void Function(String name) onSavePressed;
|
|
||||||
|
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();
|
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,
|
controller: nameController,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
@@ -23,17 +41,18 @@ class CreateBookmarkCollectionDialog extends StatelessWidget {
|
|||||||
FilteringTextInputFormatter.deny(RegExp(r'\s\s+')),
|
FilteringTextInputFormatter.deny(RegExp(r'\s\s+')),
|
||||||
],
|
],
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Collection Name',
|
labelText: AppLocalizations.of(context)!.collectionName,
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
FloatingActionButton(
|
EditDialogActions(
|
||||||
onPressed: () {
|
onSavePressed: () {
|
||||||
onSavePressed(nameController.text);
|
final bookmark =
|
||||||
|
selectedCollection?.copyWith(name: nameController.text) ??
|
||||||
|
Collection(name: nameController.text);
|
||||||
|
onSavePressed(bookmark);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Icon(Icons.save),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
119
lib/widgets/create_bookmark_dialog.dart
Normal file
119
lib/widgets/create_bookmark_dialog.dart
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../l10n/app_localizations.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: AppLocalizations.of(context)!.bookmarkTitle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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: AppLocalizations.of(context)!.url,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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: AppLocalizations.of(context)!.description,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
lib/widgets/edit_dialog_widgets/edit_dialog_actions.dart
Normal file
24
lib/widgets/edit_dialog_widgets/edit_dialog_actions.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:flutter/material.dart'
|
||||||
|
show TextButton, FloatingActionButton, Icons;
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../l10n/app_localizations.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(AppLocalizations.of(context)!.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 }
|
||||||
37
lib/widgets/search_widgets/search_bar_widget.dart
Normal file
37
lib/widgets/search_widgets/search_bar_widget.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../l10n/app_localizations.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),
|
||||||
|
),
|
||||||
|
labelText: AppLocalizations.of(context)!.search,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
lib/widgets/search_widgets/search_results_widget.dart
Normal file
58
lib/widgets/search_widgets/search_results_widget.dart
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../l10n/app_localizations.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.separated(
|
||||||
|
itemBuilder: bookmarkListItemBuilder,
|
||||||
|
itemCount: filteredBookmarks.length,
|
||||||
|
separatorBuilder: (context, index) => SizedBox(height: 10),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Center(child: Text(AppLocalizations.of(context)!.tipNoResults));
|
||||||
|
}
|
||||||
|
}
|
||||||
167
pubspec.lock
167
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:
|
||||||
@@ -94,6 +102,11 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
flutter_localizations:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -104,6 +117,38 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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"
|
||||||
|
intl:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.20.2"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -156,10 +201,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:
|
||||||
@@ -208,6 +269,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
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:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -220,18 +289,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74
|
sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.12"
|
version: "2.4.18"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_foundation
|
name: shared_preferences_foundation
|
||||||
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
|
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.4"
|
version: "2.5.6"
|
||||||
shared_preferences_linux:
|
shared_preferences_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -301,6 +370,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:
|
||||||
@@ -313,10 +390,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:
|
||||||
@@ -351,4 +500,4 @@ packages:
|
|||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.9.2 <4.0.0"
|
dart: ">=3.9.2 <4.0.0"
|
||||||
flutter: ">=3.29.0"
|
flutter: ">=3.35.0"
|
||||||
|
|||||||
10
pubspec.yaml
10
pubspec.yaml
@@ -3,7 +3,7 @@ description: "A new way to save google maps bookmarks"
|
|||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.0.0+1
|
version: 0.0.18
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.2
|
sdk: ^3.9.2
|
||||||
@@ -15,6 +15,12 @@ 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
|
shared_preferences: ^2.3.2
|
||||||
|
provider: ^6.1.5+1
|
||||||
|
metadata_fetch: ^0.4.2
|
||||||
|
url_launcher: ^6.3.2
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
|
intl: any
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -23,5 +29,5 @@ dev_dependencies:
|
|||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
generate: true
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
Reference in New Issue
Block a user