Compare commits
59 Commits
f64ed77b75
...
v0.1.24
| 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 | |||
| 2d93d1a9d7 | |||
| a4d760a970 | |||
| d0feca1ba8 | |||
| 12459bb4cb | |||
| 4970836c60 | |||
| 90539932ec | |||
| 69c00cca3e | |||
| 603060fe58 | |||
| ef9a369df1 | |||
| 651e52485c | |||
| fbec875334 | |||
| 56e277593d | |||
| 0502d82509 |
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,28 +1,6 @@
|
|||||||
# This file configures the analyzer, which statically analyzes Dart code to
|
|
||||||
# check for errors, warnings, and lints.
|
|
||||||
#
|
|
||||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
|
||||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
|
||||||
# invoked from the command line by running `flutter analyze`.
|
|
||||||
|
|
||||||
# The following line activates a set of recommended lints for Flutter apps,
|
|
||||||
# packages, and plugins designed to encourage good coding practices.
|
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
# The lint rules applied to this project can be customized in the
|
|
||||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
|
||||||
# included above or to enable additional rules. A list of all available lints
|
|
||||||
# and their documentation is published at https://dart.dev/lints.
|
|
||||||
#
|
|
||||||
# Instead of disabling a lint rule for the entire project in the
|
|
||||||
# section below, it can also be suppressed for a single line of code
|
|
||||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
|
||||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
|
||||||
# producing the lint.
|
|
||||||
rules:
|
rules:
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
prefer_single_quotes: true
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
prefer_relative_imports: true
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
|
||||||
# https://dart.dev/guides/language/analysis-options
|
|
||||||
|
|||||||
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"
|
||||||
@@ -25,20 +26,10 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND"/>
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
|
||||||
<data android:mimeType="text/*"/>
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<data android:mimeType="text/plain" />
|
||||||
<data android:scheme="https"
|
|
||||||
android:host="www.maps.google.com" />
|
|
||||||
<data android:scheme="https"
|
|
||||||
android:host="www.maps.app.goog.le" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
@@ -56,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>
|
||||||
|
|||||||
@@ -1,5 +1,47 @@
|
|||||||
package com.example.maps_bookmarks
|
package com.example.maps_bookmarks
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
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.plugin.common.MethodChannel
|
||||||
|
import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin
|
||||||
|
|
||||||
class MainActivity : FlutterActivity()
|
class MainActivity: FlutterActivity() {
|
||||||
|
private var sharedText: String? = null
|
||||||
|
private val CHANNEL = "app.channel.shared.data"
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
handleIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
setIntent(intent)
|
||||||
|
handleIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleIntent(intent: Intent) {
|
||||||
|
val action = intent.action
|
||||||
|
val type = intent.type
|
||||||
|
|
||||||
|
if (Intent.ACTION_SEND == action && "text/plain" == type) {
|
||||||
|
sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
flutterEngine.plugins.add(SharedPreferencesPlugin())
|
||||||
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
|
||||||
|
.setMethodCallHandler { call, result ->
|
||||||
|
if (call.method == "getSharedText") {
|
||||||
|
result.success(sharedText)
|
||||||
|
sharedText = null
|
||||||
|
} else {
|
||||||
|
result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
|
||||||
|
|||||||
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:
|
||||||
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,19 +1,89 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.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/share_intent_service.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
void main() {
|
void main() async {
|
||||||
runApp(const MyApp());
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await Storage.initialize();
|
||||||
|
runApp(
|
||||||
|
MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider(create: (_) => SharedLinkProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => SearchProvider()),
|
||||||
|
],
|
||||||
|
child: const MapsBookmarks(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MapsBookmarks extends StatefulWidget {
|
||||||
const MyApp({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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This widget is the root of your application.
|
|
||||||
@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,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: [Locale('en'), Locale('de')],
|
||||||
|
navigatorKey: _navigatorKey,
|
||||||
|
theme: lightTheme,
|
||||||
|
darkTheme: darkTheme,
|
||||||
|
initialRoute: CollectionsListPage.routeName,
|
||||||
|
routes: {
|
||||||
|
CollectionsListPage.routeName: (context) => const CollectionsListPage(),
|
||||||
|
CollectionPage.routeName: (context) => const CollectionPage(),
|
||||||
|
SearchPage.routeName: (context) => const SearchPage(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
lib/model/bookmark.dart
Normal file
45
lib/model/bookmark.dart
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
class Bookmark {
|
||||||
|
Bookmark({
|
||||||
|
required this.collectionId,
|
||||||
|
required this.name,
|
||||||
|
required this.link,
|
||||||
|
required this.description,
|
||||||
|
int? createdAt,
|
||||||
|
}) : createdAt = createdAt ?? DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
factory Bookmark.fromJson(Map<String, dynamic> json) => Bookmark(
|
||||||
|
collectionId: json['collectionId'] as int,
|
||||||
|
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 name;
|
||||||
|
String description;
|
||||||
|
int createdAt;
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
lib/model/collection.dart
Normal file
34
lib/model/collection.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
class Collection {
|
||||||
|
Collection({required this.name, int? createdAt})
|
||||||
|
: createdAt = createdAt ?? DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
factory Collection.fromJson(Map<String, dynamic> json) => Collection(
|
||||||
|
name: json['name'] as String,
|
||||||
|
createdAt: json['createdAt'] as int,
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
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()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
166
lib/service/storage.dart
Normal file
166
lib/service/storage.dart
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
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 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 {
|
||||||
|
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 }
|
||||||
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
60
lib/widgets/create_bookmark_collection_dialog.dart
Normal file
60
lib/widgets/create_bookmark_collection_dialog.dart
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter/material.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 {
|
||||||
|
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
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final nameController = TextEditingController();
|
||||||
|
|
||||||
|
if (selectedCollection != null) {
|
||||||
|
nameController.text = selectedCollection!.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: EditDialogTitle(
|
||||||
|
dialogType: DialogType.collection,
|
||||||
|
onDeletePressed: onDeletePressed,
|
||||||
|
),
|
||||||
|
content: 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)!.collectionName,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
EditDialogActions(
|
||||||
|
onSavePressed: () {
|
||||||
|
final bookmark =
|
||||||
|
selectedCollection?.copyWith(name: nameController.text) ??
|
||||||
|
Collection(name: nameController.text);
|
||||||
|
onSavePressed(bookmark);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
300
pubspec.lock
300
pubspec.lock
@@ -1,6 +1,14 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
android_intent_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: android_intent_plus
|
||||||
|
sha256: "14a9f94c5825a528e8c38ee89a33dbeba947efbbf76f066c174f4f3ae4f48feb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -41,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:
|
||||||
@@ -57,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
|
||||||
@@ -66,15 +98,57 @@ 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_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
|
||||||
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"
|
||||||
|
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:
|
||||||
@@ -103,10 +177,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:
|
||||||
@@ -127,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:
|
||||||
@@ -139,6 +229,38 @@ 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:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: platform
|
||||||
|
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.6"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -147,14 +269,70 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
receive_sharing_intent:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: receive_sharing_intent
|
name: provider
|
||||||
sha256: ec76056e4d258ad708e76d85591d933678625318e411564dcb9059048ca3a593
|
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.1"
|
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 +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:
|
||||||
@@ -204,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:
|
||||||
@@ -224,6 +482,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"
|
||||||
|
|||||||
15
pubspec.yaml
15
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
|
||||||
@@ -13,14 +13,21 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
receive_sharing_intent: ^1.8.1
|
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
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
|
intl: any
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
flutter_lints: ^5.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