Compare commits
90 Commits
Author | SHA1 | Date |
---|---|---|
akaessens | ec681e45e1 | |
akaessens | a38bf1dcc1 | |
akaessens | 9d56911c04 | |
akaessens | 2ef6a35862 | |
akaessens | 43458af11c | |
akaessens | 2a67c74f57 | |
akaessens | 8f8d0d07a8 | |
akaessens | 7fdfd38cdc | |
akaessens | b4d37fbc3f | |
akaessens | 2b035b6975 | |
akaessens | 32da94c275 | |
akaessens | 866889db27 | |
akaessens | 6248e79021 | |
akaessens | e8893fd712 | |
akaessens | 6c00e63d1f | |
akaessens | b37fa14d96 | |
akaessens | c6e25fdcfb | |
akaessens | 1ec5c5ea41 | |
akaessens | e051e66188 | |
akaessens | 42882b7aa8 | |
akaessens | 62742fd1aa | |
akaessens | 3299001d9b | |
Diego | ad9bf21b68 | |
akaessens | 0a20102678 | |
akaessens | 85f420e15d | |
akaessens | c119a163c0 | |
akaessens | 08c1040679 | |
akaessens | 2efaafa38b | |
akaessens | 262c1c4377 | |
akaessens | 26021d540c | |
akaessens | 37627a43d0 | |
akaessens | b94fa6be60 | |
akaessens | 23f431f535 | |
akaessens | 404b8e1086 | |
akaessens | f067076752 | |
akaessens | 748cf3c074 | |
akaessens | 2479cd9c72 | |
akaessens | 9e81e3d74a | |
akaessens | ce790763fd | |
akaessens | b4b57f68b8 | |
akaessens | fea21a4c82 | |
akaessens | 3ba50f3df7 | |
akaessens | f579094cec | |
akaessens | 43913ccd21 | |
akaessens | e549ca7676 | |
akaessens | 8335ffeada | |
akaessens | a8baabdb5f | |
akaessens | 85f68f35ac | |
akaessens | d3ade66878 | |
akaessens | 2e1b20051a | |
akaessens | 5e7d9bc4dc | |
akaessens | 626128b5dc | |
akaessens | af504084fe | |
akaessens | a30756a873 | |
akaessens | 2750ad86e8 | |
akaessens | 6977fad449 | |
akaessens | 63de4b4a4f | |
akaessens | 0506b5dfa0 | |
akaessens | 551a3c21a9 | |
akaessens | 3c5876f6bc | |
akaessens | 3c73ea2d77 | |
akaessens | fecf4ae4e8 | |
akaessens | 85e0d1ff24 | |
akaessens | 56300d78a3 | |
akaessens | 85e9c15f5e | |
akaessens | 926c2c612a | |
akaessens | c43911fc77 | |
akaessens | 3cbfc9b9c1 | |
akaessens | 71169dae76 | |
akaessens | 5b412733b4 | |
akaessens | a3f2990c61 | |
akaessens | 1c112f3e4e | |
akaessens | 2faf8aa003 | |
akaessens | 643ac62788 | |
akaessens | d5e12c4d88 | |
akaessens | 320d1787aa | |
akaessens | c7a4d9b027 | |
akaessens | bb1e7579d8 | |
akaessens | ba2ec36666 | |
akaessens | 81fd1f3ebb | |
akaessens | 8b5263db63 | |
akaessens | 76f56434e8 | |
akaessens | 9540b252a5 | |
akaessens | 7a44e467f0 | |
akaessens | 1570742462 | |
akaessens | 0950da98e1 | |
akaessens | 15ef35ba6c | |
akaessens | 55e7ad44d8 | |
akaessens | 2d3d2a2ab6 | |
akaessens | dd36c1fb00 |
|
@ -2,9 +2,7 @@ name: Android CI
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -13,9 +11,9 @@ jobs:
|
|||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 1.8
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
name: changelog-generate
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
run-script:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: '0' # get tags
|
||||
ref: 'master'
|
||||
|
||||
- name: run-script
|
||||
shell: bash
|
||||
run: $GITHUB_WORKSPACE/create_changelog.sh
|
||||
|
||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: Update changelog
|
|
@ -0,0 +1,64 @@
|
|||
# Changelog
|
||||
## v0.5.0 (15)
|
||||
- parallelize multiple event scraping
|
||||
- add scraping history
|
||||
- allow entering page name without URL format
|
||||
- updated dependencies
|
||||
## v0.4.4 (14)
|
||||
- Fix Android 11 intents (to open Calendar or Maps)
|
||||
## v0.4.3 (13)
|
||||
- Add spanish translation thanks to @sguinetti
|
||||
- update dependencies
|
||||
## v0.4.2 (12)
|
||||
- Fix scraping not working when cookies need to be accepted
|
||||
- Android 11 ready
|
||||
## v0.4.1 (11)
|
||||
- Fix events not displaying correctly after activity resume
|
||||
- add share action on each event
|
||||
- add URL shortener redirection for fb.me
|
||||
## v0.4.0 (10)
|
||||
- Support pages with upcoming events *beta*
|
||||
- Display events in a scrollable card-based view
|
||||
- Improve intent handling
|
||||
- Add history for scraped events
|
||||
- Tap image preview to open fullscreen
|
||||
- Scrape name and image even if no event data found
|
||||
## v0.3.3 (9)
|
||||
- Update about section with download and changelog information.
|
||||
- Improve high-res preview scraping.
|
||||
- Fix potential crash on calendar intent.
|
||||
## v0.3.2 (8)
|
||||
- Add german translation.
|
||||
- Decrease MinSDK to 23 (Android 6.0).
|
||||
- Replace Date implementation.
|
||||
- Enable Dark Mode for WebViews.
|
||||
- Add high-res preview for www prefix.
|
||||
## v0.3.1 (7)
|
||||
- Fix invalid default uri.
|
||||
- Update about section.
|
||||
## v0.3.0 (6)
|
||||
- Rework help and about.
|
||||
- Add settings with URL prefix selector.
|
||||
- Add Maps button for location.
|
||||
- Disable text field inputs - all fields can be edited in the calendar app.
|
||||
- Change date/time format to RFC standard.
|
||||
- Minor interface changes.
|
||||
- Code cleanup and refactoring.
|
||||
- Add code documentation.
|
||||
## v0.2.2 (5)
|
||||
- Fix for event links with querystrings.
|
||||
- New icon and banner as default toolbar image.
|
||||
## v0.2.1 (4)
|
||||
- Fixes for new design.
|
||||
- New button layout.
|
||||
- Improve input error display.
|
||||
## v0.2.0 (3)
|
||||
- Fix for timezone calculation.
|
||||
- Material design Interface, will be improved further.
|
||||
- Display the event picture in the toolbar.
|
||||
## v0.1.1 (2)
|
||||
- Added help and reports section.
|
||||
- Fix for some locations not scraped correctly.
|
||||
- Resolve fb-internal links.
|
||||
## v0.1.0 (1)
|
||||
- Initial release.
|
40
README.md
|
@ -1,18 +1,42 @@
|
|||
![Android CI](https://github.com/akaessens/NoFbEventScraper/workflows/Android%20CI/badge.svg)
|
||||
[![Android CI](https://github.com/akaessens/NoFbEventScraper/workflows/Android%20CI/badge.svg)](https://github.com/akaessens/NoFbEventScraper/actions)
|
||||
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://github.com/akaessens/NoFbEventScraper/blob/master/LICENSE)
|
||||
[![Release](https://img.shields.io/github/release/akaessens/nofbeventscraper.svg?logo=github)](https://github.com/akaessens/NoFbEventScraper/releases/latest)
|
||||
[![F-Droid](https://img.shields.io/f-droid/v/com.akdev.nofbeventscraper.svg)](https://f-droid.org/en/packages/com.akdev.nofbeventscraper)
|
||||
|
||||
# NoFb Event Scraper
|
||||
|
||||
This application was developed to be used without a facebook account.
|
||||
<img src="https://github.com/akaessens/NoFbEventScraper/raw/master/fastlane/metadata/android/en-US/images/featureGraphic.png" alt="Feature Graphic" width="max-width">
|
||||
|
||||
Therefore it does not use the facebook API.
|
||||
Instead it opens the facebook event URI and downloads the website source code.
|
||||
# Description
|
||||
|
||||
This application was developed to be used without a Facebook account.
|
||||
|
||||
Therefore it does not use the Facebook API.
|
||||
Instead it opens the Facebook event URI and downloads the website source code.
|
||||
This source contains the information which is used to create a calendar entry.
|
||||
|
||||
# Download
|
||||
Currently this application is in the submitting process to FDroid. see also [#2](/../../issues/2).
|
||||
|
||||
Until it is published at FDroid, use [release v0.2.2](https://github.com/akaessens/NoFbEventScraper/releases/download/v0.2.2/app-release.apk).
|
||||
<a href="https://f-droid.org/en/packages/com.akdev.nofbeventscraper">
|
||||
<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="75">
|
||||
</a>
|
||||
|
||||
# Changelog
|
||||
|
||||
[CHANGELOG](CHANGELOG.md)
|
||||
|
||||
# Building
|
||||
|
||||
This Android app is written in Java and is using the Gradle build system. To compile it, i recommend using Android Studio.
|
||||
```
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
# Screenshots
|
||||
|
||||
<img src="https://github.com/akaessens/NoFbEventScraper/raw/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" alt="Screenshot 1" width="200"> <img src="https://github.com/akaessens/NoFbEventScraper/raw/master/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" alt="Screenshot 2" width="200"> <img src="https://github.com/akaessens/NoFbEventScraper/raw/master/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" alt="Screenshot 3" width="200"> <img src="https://github.com/akaessens/NoFbEventScraper/raw/master/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" alt="Screenshot 4" width="200">
|
||||
<img src="https://github.com/akaessens/NoFbEventScraper/raw/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" alt="Screenshot 1" width="150"> <img src="https://github.com/akaessens/NoFbEventScraper/raw/master/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" alt="Screenshot 2" width="150"> <img src="https://github.com/akaessens/NoFbEventScraper/raw/master/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" alt="Screenshot 3" width="150"> <img src="https://github.com/akaessens/NoFbEventScraper/raw/master/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" alt="Screenshot 4" width="150"> <img
|
||||
src="https://github.com/akaessens/NoFbEventScraper/raw/master/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png" alt="Screenshot 5" width="150">
|
||||
|
||||
# Donations
|
||||
I develop this application in my free time. If you like it, you can donate at <a href="https://www.paypal.me/andreaskaessens">PayPal</a>.
|
||||
|
||||
<a title="PayPal" href="https://www.paypal.me/andreaskaessens"><img src="https://raw.githubusercontent.com/stefan-niedermann/paypal-donate-button/master/paypal-donate-button.png" height="75" /></a>
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion "29.0.3"
|
||||
compileSdkVersion 30
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.akdev.nofbeventscraper"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 29
|
||||
versionCode 6
|
||||
versionName "0.3.0"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
versionCode 15
|
||||
versionName "0.5.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
@ -24,23 +23,33 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
||||
implementation 'androidx.navigation:navigation-fragment:2.3.0'
|
||||
implementation 'androidx.navigation:navigation-ui:2.3.0'
|
||||
|
||||
// jsoup HTML parser library @ https://jsoup.org/
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
|
||||
implementation 'com.squareup.picasso:picasso:2.71828'
|
||||
// androidx
|
||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.navigation:navigation-fragment:2.3.5'
|
||||
implementation 'androidx.navigation:navigation-ui:2.3.5'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation "androidx.webkit:webkit:1.4.0"
|
||||
|
||||
// JSON save/restore shared preference
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
|
||||
// Theme
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
|
||||
// Scraping
|
||||
implementation 'org.jsoup:jsoup:1.14.1'
|
||||
|
||||
// Image loading and transforming
|
||||
implementation 'com.squareup.picasso:picasso:2.71828'
|
||||
|
||||
// animations and transformations
|
||||
implementation 'jp.wasabeef:picasso-transformations:2.4.0'
|
||||
implementation 'jp.wasabeef:recyclerview-animators:4.0.2'
|
||||
// tests
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
}
|
||||
|
|
|
@ -42,15 +42,16 @@ public class ScraperUnitTest {
|
|||
|
||||
FbScraper scraper = new FbScraper(null, "");
|
||||
|
||||
String exp = "2020-10-23T05:00+02:00";
|
||||
String in = "2020-10-23T05:00:00+0200";
|
||||
String act = scraper.toZonedDateTime(in).toString();
|
||||
String in = "2020-01-01T12:00:00+0100";
|
||||
String exp = "Mi., 01 Jan. 2020 12:00 MEZ";
|
||||
|
||||
String act = FbEvent.dateTimeToString(scraper.parseToDate(in));
|
||||
assertEquals(exp, act);
|
||||
|
||||
|
||||
exp = null;
|
||||
exp = "";
|
||||
in = "";
|
||||
ZonedDateTime act2 = scraper.toZonedDateTime(in);
|
||||
String act2 = FbEvent.dateTimeToString(scraper.parseToDate(in));
|
||||
assertEquals(exp, act2);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,20 +11,49 @@
|
|||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity android:name=".IntentReceiver">
|
||||
<!-- Accepts open with -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:host="*.facebook.com"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="facebook.com"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="*.fb.me"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="fb.me"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
|
||||
<activity android:name=".SettingsActivity"
|
||||
<!-- Accepts share intents -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:label="@string/action_settings">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
<activity android:name=".HelpActivity"
|
||||
<activity
|
||||
android:name=".HelpActivity"
|
||||
android:label="@string/action_help">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
<activity android:name=".AboutActivity"
|
||||
<activity
|
||||
android:name=".AboutActivity"
|
||||
android:label="@string/action_about">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
|
@ -33,37 +62,15 @@
|
|||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.NoActionBar">
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:windowSoftInputMode="stateVisible|adjustPan">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<!-- Accepts URIs that begin with "https://*.facebook.com/events” -->
|
||||
<data
|
||||
android:host="*.facebook.com"
|
||||
android:pathPrefix="/events"
|
||||
android:scheme="https" />
|
||||
<!-- Accepts URIs that begin with "https://facebook.com/events” -->
|
||||
<data
|
||||
android:host="facebook.com"
|
||||
android:pathPrefix="/events"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Accepts share intents” -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<!doctype html>
|
||||
<h3>Description</h3>
|
||||
<p>This application was developed to be used without a facebook account. Therefore it does not use the facebook API. Instead it opens the facebook event URI and downloads the website source code. This source contains the information which is used to create a calendar entry.</p>
|
||||
<h3>Open Source</h3>
|
||||
<p>The source code for this application is available at <a href=" https://github.com/akaessens/NoFbEventScraper">GitHub</a>.<strong><br /></strong></p>
|
||||
<p>If you encounter an issue, please report it to me anonymously at the <a href="https://gitreports.com/issue/akaessens/NoFbEventScraper">Bugtracker</a> or directly at <a href="https://github.com/akaessens/NoFbEventScraper/issues">GitHub</a>.</p>
|
||||
<h3>Donations</h3>
|
||||
<p>I develop this application in my free time. If you like it, you can donate at <a href="https://www.paypal.me/andreaskaessens">PayPal</a>.</p>
|
||||
<p><a title="PayPal" href="https://www.paypal.me/andreaskaessens"><img src="https://www.paypalobjects.com/webstatic/de_DE/i/de-pp-logo-100px.png" /></a></p>
|
|
@ -1,15 +1,13 @@
|
|||
package com.akdev.nofbeventscraper;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.webkit.WebView;
|
||||
import android.widget.ImageView;
|
||||
import androidx.webkit.WebSettingsCompat;
|
||||
import androidx.webkit.WebViewFeature;
|
||||
|
||||
public class AboutActivity extends AppCompatActivity {
|
||||
|
||||
|
@ -24,7 +22,14 @@ public class AboutActivity extends AppCompatActivity {
|
|||
|
||||
WebView webview_about = findViewById(R.id.webview_about);
|
||||
|
||||
webview_about.loadUrl("file:////android_asset/about.html");
|
||||
int night_mode_flags = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
if (night_mode_flags == Configuration.UI_MODE_NIGHT_YES) {
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
|
||||
WebSettingsCompat.setForceDark(webview_about.getSettings(),
|
||||
WebSettingsCompat.FORCE_DARK_ON);
|
||||
}
|
||||
}
|
||||
webview_about.loadUrl("file:///android_res/raw/about.html");
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package com.akdev.nofbeventscraper;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.jsoup.Connection;
|
||||
import org.jsoup.HttpStatusException;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class DocumentReceiver {
|
||||
|
||||
public static org.jsoup.nodes.Document getDocument(String url) throws HttpStatusException, IOException {
|
||||
|
||||
org.jsoup.nodes.Document document;
|
||||
|
||||
// use default android user agent
|
||||
String user_agent = "Mozilla/5.0 (X11; Linux x86_64)";
|
||||
|
||||
Log.d("scraperLog", "DocumentReceiver: " + url);
|
||||
|
||||
Connection connection = Jsoup.connect(url).userAgent(user_agent).followRedirects(true);
|
||||
|
||||
Connection.Response response = connection.execute();
|
||||
|
||||
document = response.parse();
|
||||
|
||||
Log.d("scraperLog", "Document title: " + document.title());
|
||||
|
||||
try {
|
||||
// accept cookies needed?
|
||||
Element form = document.select("form[method=post]").first();
|
||||
String action = form.attr("action");
|
||||
|
||||
List<String> names = form.select("input").eachAttr("name");
|
||||
List<String> values = form.select("input").eachAttr("value");
|
||||
|
||||
Map<String, String> data = new HashMap<String, String>();
|
||||
|
||||
for (int i = 0; i < names.size(); i++) {
|
||||
data.put(names.get(i), values.get(i));
|
||||
}
|
||||
|
||||
document = connection.url("https://mbasic.facebook.com" + action)
|
||||
.cookies(response.cookies())
|
||||
.method(Connection.Method.POST)
|
||||
.data(data)
|
||||
.post();
|
||||
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
return document;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,276 @@
|
|||
package com.akdev.nofbeventscraper;
|
||||
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.provider.CalendarContract;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jp.wasabeef.picasso.transformations.CropCircleTransformation;
|
||||
|
||||
import static com.akdev.nofbeventscraper.FbEvent.dateTimeToEpoch;
|
||||
|
||||
|
||||
public class EventAdapter extends
|
||||
RecyclerView.Adapter<EventAdapter.ViewHolder> {
|
||||
|
||||
private List<FbEvent> events;
|
||||
|
||||
// Pass in the contact array into the constructor
|
||||
public EventAdapter(List<FbEvent> events) {
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int view_type) {
|
||||
final Context context = parent.getContext();
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
|
||||
// Inflate the custom layout
|
||||
View view = inflater.inflate(R.layout.item_event, parent, false);
|
||||
|
||||
|
||||
// Return a new holder instance
|
||||
final ViewHolder holder = new ViewHolder(view);
|
||||
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final EventAdapter.ViewHolder holder, int position) {
|
||||
// Get the data model based on position
|
||||
final FbEvent event = events.get(position);
|
||||
|
||||
|
||||
// Set item views based on your views and data model
|
||||
holder.text_view_event_name.setText(event.name);
|
||||
|
||||
/*
|
||||
* initialize all text views with event information
|
||||
* hide fields and image views if no information is available
|
||||
*/
|
||||
if (!event.location.equals("")) {
|
||||
holder.text_view_event_location.setText(event.location);
|
||||
} else {
|
||||
holder.text_view_event_location.setVisibility(View.GONE);
|
||||
holder.image_view_event_location.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (event.start_date != null) {
|
||||
String str = FbEvent.dateTimeToString(event.start_date);
|
||||
holder.text_view_event_start.setText(str);
|
||||
} else {
|
||||
holder.text_view_event_start.setVisibility(View.GONE);
|
||||
|
||||
if (event.end_date == null) {
|
||||
holder.image_view_event_time.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.end_date != null) {
|
||||
String str = FbEvent.dateTimeToString(event.end_date);
|
||||
holder.text_view_event_end.setText(str);
|
||||
} else {
|
||||
holder.text_view_event_end.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
|
||||
if (!event.description.equals("")) {
|
||||
holder.text_view_event_description.setText(event.description);
|
||||
} else {
|
||||
holder.text_view_event_description.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
try {
|
||||
Picasso.get()
|
||||
.load(event.image_url)
|
||||
.placeholder(R.mipmap.ic_launcher)
|
||||
.transform(new CropCircleTransformation())
|
||||
.into(holder.image_view_event_image);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
/*
|
||||
* Maps button: launch maps intent
|
||||
*/
|
||||
View.OnClickListener location_click_listener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
String map_search = "geo:0,0?q=" + event.location;
|
||||
|
||||
Uri intent_uri = Uri.parse(map_search);
|
||||
Intent map_intent = new Intent(Intent.ACTION_VIEW, intent_uri);
|
||||
|
||||
try {
|
||||
view.getContext().startActivity(map_intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast toast=Toast.makeText(view.getContext(),"no App installed", Toast.LENGTH_SHORT);
|
||||
toast.show();
|
||||
}
|
||||
}
|
||||
};
|
||||
holder.image_view_event_location.setOnClickListener(location_click_listener);
|
||||
holder.text_view_event_location.setOnClickListener(location_click_listener);
|
||||
|
||||
/*
|
||||
* Add to calendar button: launch calendar application with current event
|
||||
*/
|
||||
holder.button_add_to_calendar.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
// calendar event intent expects epoch time format
|
||||
Long start_epoch = dateTimeToEpoch(event.start_date);
|
||||
Long end_epoch = dateTimeToEpoch(event.end_date);
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_EDIT);
|
||||
intent.setType("vnd.android.cursor.item/event");
|
||||
intent.putExtra(CalendarContract.Events.TITLE, event.name);
|
||||
intent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, start_epoch);
|
||||
intent.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, end_epoch);
|
||||
intent.putExtra(CalendarContract.Events.EVENT_LOCATION, event.location);
|
||||
|
||||
// prepend url in description
|
||||
String desc = event.url + "\n\n" + event.description;
|
||||
intent.putExtra(CalendarContract.Events.DESCRIPTION, desc);
|
||||
|
||||
try {
|
||||
view.getContext().startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast toast=Toast.makeText(view.getContext(),"no App installed", Toast.LENGTH_SHORT);
|
||||
toast.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
/*
|
||||
* Expand and collapse description
|
||||
*/
|
||||
holder.text_view_event_description.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (holder.description_collapsed) {
|
||||
holder.description_collapsed = false;
|
||||
holder.text_view_event_description.setMaxLines(Integer.MAX_VALUE);
|
||||
} else {
|
||||
holder.description_collapsed = true;
|
||||
holder.text_view_event_description.setMaxLines(5);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Image preview click creates fullscreen dialog
|
||||
*/
|
||||
|
||||
View.OnClickListener listener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
|
||||
final Dialog dialog = new Dialog(view.getContext(), android.R.style.Theme_Translucent_NoTitleBar);
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
dialog.setContentView(R.layout.dialog_image);
|
||||
dialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
|
||||
|
||||
ImageView image = (ImageView) dialog.findViewById(R.id.image_view_event_image_fullscreen);
|
||||
image.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
Picasso.get()
|
||||
.load(event.image_url)
|
||||
.into(image, new com.squareup.picasso.Callback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
holder.image_view_event_image.setOnClickListener(listener);
|
||||
|
||||
holder.image_view_share.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Intent share_intent = new Intent(android.content.Intent.ACTION_SEND);
|
||||
share_intent.setType("text/plain");
|
||||
share_intent.putExtra(Intent.EXTRA_TEXT, event.url);
|
||||
|
||||
try {
|
||||
view.getContext().startActivity(share_intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast toast=Toast.makeText(view.getContext(),"no App installed", Toast.LENGTH_SHORT);
|
||||
toast.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Returns the total count of items in the list
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return events.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* access item view elements via holder class
|
||||
*/
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
protected TextView text_view_event_name;
|
||||
protected TextView text_view_event_start;
|
||||
protected TextView text_view_event_end;
|
||||
protected TextView text_view_event_location;
|
||||
protected TextView text_view_event_description;
|
||||
protected ImageView image_view_event_image;
|
||||
protected ImageView image_view_event_location;
|
||||
protected ImageView image_view_event_time;
|
||||
protected ImageView image_view_share;
|
||||
protected Button button_add_to_calendar;
|
||||
|
||||
protected boolean description_collapsed = true;
|
||||
|
||||
public ViewHolder(View item_view) {
|
||||
super(item_view);
|
||||
|
||||
text_view_event_name = item_view.findViewById(R.id.text_view_event_name);
|
||||
text_view_event_start = item_view.findViewById(R.id.text_view_event_start);
|
||||
text_view_event_end = item_view.findViewById(R.id.text_view_event_end);
|
||||
text_view_event_location = item_view.findViewById(R.id.text_view_event_location);
|
||||
text_view_event_description = item_view.findViewById(R.id.text_view_event_description);
|
||||
image_view_event_image = item_view.findViewById(R.id.image_view_event_image);
|
||||
image_view_event_location = item_view.findViewById(R.id.image_view_event_location);
|
||||
image_view_event_time = item_view.findViewById(R.id.image_view_event_time);
|
||||
image_view_share = item_view.findViewById(R.id.image_view_share);
|
||||
button_add_to_calendar = item_view.findViewById(R.id.button_add_to_calendar);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
package com.akdev.nofbeventscraper;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Objects of this class store immutable information about
|
||||
|
@ -12,14 +13,23 @@ public class FbEvent {
|
|||
|
||||
public final String url;
|
||||
public final String name;
|
||||
public final ZonedDateTime start_date;
|
||||
public final ZonedDateTime end_date;
|
||||
public final Date start_date;
|
||||
public final Date end_date;
|
||||
public final String description;
|
||||
public final String location;
|
||||
public final String image_url;
|
||||
|
||||
public FbEvent() {
|
||||
url = "";
|
||||
name= "";
|
||||
start_date = null;
|
||||
end_date = null;
|
||||
description = "";
|
||||
location = "";
|
||||
image_url = null;
|
||||
}
|
||||
|
||||
public FbEvent(String url, String name, ZonedDateTime start_date, ZonedDateTime end_date,
|
||||
public FbEvent(String url, String name, Date start_date, Date end_date,
|
||||
String description, String location, String image_url) {
|
||||
this.url = url;
|
||||
this.name = name;
|
||||
|
@ -30,31 +40,36 @@ public class FbEvent {
|
|||
this.image_url = image_url;
|
||||
}
|
||||
|
||||
public static ArrayList<FbEvent> createEventList() {
|
||||
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts datetime to epoch.
|
||||
*
|
||||
* @param zoned_date_time ZonedDateTime object
|
||||
* @param date Date object
|
||||
* @return Event begin time in milliseconds from the epoch for calendar intent or null
|
||||
*/
|
||||
static Long dateTimeToEpoch(ZonedDateTime zoned_date_time) {
|
||||
static Long dateTimeToEpoch(Date date) {
|
||||
try {
|
||||
return zoned_date_time.toEpochSecond() * 1000;
|
||||
return date.getTime();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a RFC formatted String representation of a ZonedDateTime
|
||||
* Returns a locally formatted String representation of a Date
|
||||
*
|
||||
* @param zoned_date_time
|
||||
* @return RFC-1123 formatted String of zoned_date_time or empty String
|
||||
* @param date
|
||||
* @return locally formatted String of date or empty String
|
||||
*/
|
||||
static String dateTimeToString(ZonedDateTime zoned_date_time) {
|
||||
static String dateTimeToString(Date date) {
|
||||
try {
|
||||
return DateTimeFormatter
|
||||
.RFC_1123_DATE_TIME
|
||||
.format(zoned_date_time);
|
||||
SimpleDateFormat formatter = new SimpleDateFormat("E, dd MMM yyyy HH:mm z",
|
||||
Locale.getDefault());
|
||||
return formatter.format(date);
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
package com.akdev.nofbeventscraper;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.jsoup.HttpStatusException;
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* This class can asynchronously scrape public facebook events
|
||||
* and gather the most important information. It is stored in a FbEvent object.
|
||||
*/
|
||||
public class FbEventScraper extends AsyncTask<Void, Void, Void> {
|
||||
|
||||
private FbScraper scraper;
|
||||
private int error;
|
||||
private String url;
|
||||
private FbEvent event;
|
||||
|
||||
/**
|
||||
* Constructor with reference to scraper to return results.
|
||||
*
|
||||
* @param scraper Reference to FbScraper
|
||||
* @param input_url Input url to scrape from
|
||||
*/
|
||||
FbEventScraper(FbScraper scraper, String input_url) {
|
||||
|
||||
this.scraper = scraper;
|
||||
this.url = input_url;
|
||||
this.error = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips the event location from the json string.
|
||||
* This can be a name only or a complete postal address.
|
||||
*
|
||||
* @param location_json JSON formatted string
|
||||
* @return String representation of the location.
|
||||
*/
|
||||
protected String fixLocation(String location_json) {
|
||||
|
||||
String location_name = "";
|
||||
|
||||
try {
|
||||
JSONObject reader = new JSONObject(location_json);
|
||||
|
||||
location_name = reader.getString("name");
|
||||
JSONObject address = reader.getJSONObject("address");
|
||||
|
||||
String type = address.getString("@type");
|
||||
|
||||
if (type.equals("PostalAddress")) {
|
||||
String postal_code = address.getString("postalCode");
|
||||
String address_locality = address.getString("addressLocality");
|
||||
String street_address = address.getString("streetAddress");
|
||||
// included in locality
|
||||
//String address_country = address.getString("addressCountry");
|
||||
|
||||
return location_name + ", "
|
||||
+ street_address + ", "
|
||||
+ postal_code + " "
|
||||
+ address_locality;
|
||||
} else {
|
||||
return location_name;
|
||||
}
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
return location_name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a time string from the facebook event into a Date
|
||||
*
|
||||
* @param time_in time string from the event
|
||||
* @return Date parsed from input or null
|
||||
*/
|
||||
protected Date parseToDate(String time_in) {
|
||||
|
||||
try {
|
||||
// parse e.g. 2011-12-03T10:15:30+0100
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault());
|
||||
|
||||
return sdf.parse(time_in);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all occurrences of a facebook internal links in
|
||||
* an event description into an actual URL.
|
||||
*
|
||||
* @param description_in description string from the event
|
||||
* @return corrected String with internal links resolved
|
||||
*/
|
||||
protected String fixDescriptionLinks(String description_in) {
|
||||
try {
|
||||
/* @[152580919265:274:SiteDescription]
|
||||
* to
|
||||
* SiteDescription [m.facebook.com/152580919265] */
|
||||
|
||||
return description_in.replaceAll("@\\[([0-9]{10,}):[0-9]{3}:([^]]*)]",
|
||||
"$2 [m.facebook.com/$1]");
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return description_in;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a single field from a JSONObject
|
||||
*
|
||||
* @param reader JSONObject to read from
|
||||
* @param field Which field to read
|
||||
* @return String of the value of the field or empty string
|
||||
*/
|
||||
private String readFromJson(JSONObject reader, String field) {
|
||||
try {
|
||||
return reader.getString(field);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Started by execute().
|
||||
* Gets the HTML doc from the input string and scrapes the event information from it.
|
||||
*
|
||||
* @param voids
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
|
||||
Log.d("scraperLog", "doInBackground: "+url);
|
||||
|
||||
try {
|
||||
Document document = DocumentReceiver.getDocument(url);
|
||||
if (document == null) {
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
String name = "", location = "", description = "", image_url = "";
|
||||
Date start_date = null, end_date = null;
|
||||
try {
|
||||
|
||||
String json = document
|
||||
.select("script[type = application/ld+json]")
|
||||
.first().data();
|
||||
|
||||
JSONObject reader = new JSONObject(json);
|
||||
|
||||
// get all fields from json event information
|
||||
name = readFromJson(reader, "name");
|
||||
start_date = parseToDate(readFromJson(reader, "startDate"));
|
||||
end_date = parseToDate(readFromJson(reader, "endDate"));
|
||||
description = fixDescriptionLinks(readFromJson(reader, "description"));
|
||||
location = fixLocation(readFromJson(reader, "location"));
|
||||
image_url = readFromJson(reader, "image");
|
||||
|
||||
// try to find a high-res image
|
||||
try {
|
||||
image_url = document.select("div[id=event_header_primary]")
|
||||
.select("img").first().attr("src");
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
} catch (JSONException | NullPointerException e) {
|
||||
// json event information mot found. get at least title and image
|
||||
name = document.title();
|
||||
description = scraper.main.get().getString(R.string.error_scraping);
|
||||
try {
|
||||
image_url = document.select("div[id*=event_header]")
|
||||
.select("img").first().attr("src");
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
this.event = new FbEvent(url, name, start_date, end_date, description, location, image_url);
|
||||
|
||||
} catch (HttpStatusException e) {
|
||||
this.error = R.string.error_url;
|
||||
}
|
||||
catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
this.error = R.string.error_connection;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
this.error = R.string.error_unknown;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
}
|
||||
|
||||
/**
|
||||
* When scraping is finished, the scraper callback will receive the event.
|
||||
*
|
||||
* @param aVoid
|
||||
*/
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
super.onPostExecute(aVoid);
|
||||
|
||||
this.scraper.scrapeEventResultCallback(this.event, this.error);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package com.akdev.nofbeventscraper;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.jsoup.HttpStatusException;
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* This class can asynchronously scrape public facebook pages for event ids
|
||||
* It returns a String list of event urls
|
||||
*/
|
||||
public class FbPageScraper extends AsyncTask<Void, Void, Void> {
|
||||
|
||||
private FbScraper scraper;
|
||||
private int error;
|
||||
private String url;
|
||||
private List<String> event_links = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Constructor with reference to scraper to return results.
|
||||
*
|
||||
* @param scraper Reference to FbScraper
|
||||
* @param page_url Input url to scrape from
|
||||
*/
|
||||
FbPageScraper(FbScraper scraper, String page_url) {
|
||||
|
||||
this.scraper = scraper;
|
||||
this.url = page_url;
|
||||
this.error = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Started by execute().
|
||||
* Gets the HTML doc from the input string and scrapes the event links from it.
|
||||
*
|
||||
* @param voids
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
|
||||
|
||||
do {
|
||||
try {
|
||||
// use default android user agent
|
||||
|
||||
Document document = DocumentReceiver.getDocument(url);
|
||||
|
||||
if (document == null) {
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
/*
|
||||
* get all event id's from current url and add to the list
|
||||
*/
|
||||
String regex = "(/events/[0-9]*)(/\\?event_time_id=[0-9]*)?";
|
||||
|
||||
List<String> event_links_href = document
|
||||
.getElementsByAttributeValueMatching("href", Pattern.compile(regex))
|
||||
.eachAttr("href");
|
||||
|
||||
for (String event_id : event_links_href) {
|
||||
this.event_links.add("https://mbasic.facebook.com" + event_id);
|
||||
}
|
||||
|
||||
/*
|
||||
* check if more events should be scraped
|
||||
*/
|
||||
SharedPreferences shared_prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(scraper.main.get());
|
||||
|
||||
int max = shared_prefs.getInt("page_event_max", 5);
|
||||
|
||||
if (event_links.size() < max) {
|
||||
// find next page
|
||||
try {
|
||||
String next_url = document
|
||||
.getElementsByAttributeValueMatching("href", "has_more=1")
|
||||
.first().attr("href");
|
||||
|
||||
this.url = "https://mbasic.facebook.com" + next_url;
|
||||
} catch (NullPointerException e) {
|
||||
url = null;
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
url = null;
|
||||
event_links = event_links.subList(0, max);
|
||||
}
|
||||
} catch (HttpStatusException e) {
|
||||
this.error = R.string.error_url;
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
this.error = R.string.error_connection;
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
this.error = R.string.error_unknown;
|
||||
return null;
|
||||
}
|
||||
} while (url != null);
|
||||
|
||||
if (this.event_links.size() == 0) {
|
||||
this.error = R.string.error_no_events;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
}
|
||||
|
||||
/**
|
||||
* When scraping is finished, the scraper callback will receive the link list.
|
||||
*
|
||||
* @param aVoid
|
||||
*/
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
super.onPostExecute(aVoid);
|
||||
|
||||
this.scraper.scrapePageResultCallback(this.event_links, this.error);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.akdev.nofbeventscraper;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
public class FbRedirectionResolver extends AsyncTask<Void, Void, Void> {
|
||||
|
||||
private String input_url;
|
||||
private FbScraper scraper;
|
||||
private String redirected_url;
|
||||
|
||||
public FbRedirectionResolver (FbScraper scraper, String input_url) {
|
||||
this.input_url = input_url;
|
||||
this.scraper = scraper;
|
||||
}
|
||||
|
||||
protected Void doInBackground(Void... voids) {
|
||||
try {
|
||||
HttpURLConnection con = (HttpURLConnection) new URL(input_url).openConnection();
|
||||
|
||||
con.setInstanceFollowRedirects(false);
|
||||
|
||||
con.connect();
|
||||
|
||||
redirected_url = con.getHeaderField("Location");
|
||||
|
||||
} catch (Exception e) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
super.onPostExecute(aVoid);
|
||||
|
||||
scraper.redirectionResultCallback(redirected_url);
|
||||
}
|
||||
}
|
|
@ -2,39 +2,30 @@ package com.akdev.nofbeventscraper;
|
|||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.text.Editable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.net.URLConnection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* This class can asynchronously scrape public facebook events
|
||||
* and gather the most important information. It is stored in a FbEvent object.
|
||||
*/
|
||||
public class FbScraper extends AsyncTask<Void, Void, Void> {
|
||||
public class FbScraper {
|
||||
|
||||
private String error;
|
||||
protected List<AsyncTask> tasks;
|
||||
protected WeakReference<MainActivity> main; // no context leak with WeakReference
|
||||
url_type_enum url_type = url_type_enum.EVENT;
|
||||
private String input_url;
|
||||
private WeakReference<MainActivity> main; // no context leak with WeakReference
|
||||
private FbEvent event;
|
||||
|
||||
/**
|
||||
* Constructor with WeakReference to the main activity, to update it's text fields.
|
||||
* Constructor with WeakReference to the main activity, to add events.
|
||||
*
|
||||
* @param main WeakReference of main activity to prevent context leak
|
||||
* @param input_url Input url to scrape from
|
||||
|
@ -42,17 +33,72 @@ public class FbScraper extends AsyncTask<Void, Void, Void> {
|
|||
FbScraper(WeakReference<MainActivity> main, String input_url) {
|
||||
this.main = main;
|
||||
this.input_url = input_url;
|
||||
this.tasks = new ArrayList<>();
|
||||
}
|
||||
|
||||
protected String getShortened(String url) throws IOException, URISyntaxException {
|
||||
// check for url format
|
||||
new URL(url).toURI();
|
||||
|
||||
String regex = "(fb.me/)(e/)?([^/?]*)|(facebook.com/event_invite/[a-zA-Z0-9]*)";
|
||||
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(url);
|
||||
|
||||
if (matcher.find()) {
|
||||
//only mbasic does have event ids displayed in HTML
|
||||
String url_prefix = "https://mbasic.";
|
||||
|
||||
// create URL
|
||||
return url_prefix + matcher.group();
|
||||
|
||||
} else {
|
||||
throw new URISyntaxException(url, "Does not contain page.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips the facebook event link of the input url.
|
||||
* Checks if valid URL,
|
||||
* strips the facebook page id from the input link and create an URL that can be scraped from.
|
||||
*
|
||||
* @param url input URL
|
||||
* @return new mbasic url that can be scraped for event id's
|
||||
* @throws URISyntaxException if page not found
|
||||
* @throws MalformedURLException
|
||||
*/
|
||||
protected String getPageUrl(String url) throws URISyntaxException, MalformedURLException {
|
||||
|
||||
// check for url format
|
||||
new URL(url).toURI();
|
||||
|
||||
String regex = "(facebook.com/)(pg/)?([^/?]*)";
|
||||
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(url);
|
||||
|
||||
if (matcher.find()) {
|
||||
//only mbasic does have event ids displayed in HTML
|
||||
String url_prefix = "https://mbasic.facebook.com/";
|
||||
String url_suffix = "?v=events";
|
||||
|
||||
// create URL
|
||||
return url_prefix + matcher.group(3) + url_suffix;
|
||||
|
||||
} else {
|
||||
throw new URISyntaxException(url, "Does not contain page.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips the facebook event link from the input event url.
|
||||
*
|
||||
* @param url input url
|
||||
* @return facebook event url String if one was found
|
||||
* @throws URISyntaxException if event not found
|
||||
* @throws MalformedURLException
|
||||
*/
|
||||
protected String fixURI(String url) throws URISyntaxException, MalformedURLException {
|
||||
protected String getEventUrl(String url) throws URISyntaxException, MalformedURLException {
|
||||
|
||||
// check for url format
|
||||
new URL(url).toURI();
|
||||
|
@ -73,6 +119,7 @@ public class FbScraper extends AsyncTask<Void, Void, Void> {
|
|||
// rewrite url to m.facebook and dismiss any query strings or referrals
|
||||
String ret = url_prefix + matcher.group(1);
|
||||
if (matcher.group(2) != null) {
|
||||
// add event time identifier
|
||||
ret += matcher.group(2);
|
||||
}
|
||||
return ret;
|
||||
|
@ -83,168 +130,157 @@ public class FbScraper extends AsyncTask<Void, Void, Void> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Strips the event location from the json string.
|
||||
* This can be a name only or a complete postal address.
|
||||
* @param location_json JSON formatted string
|
||||
* @return String representation of the location.
|
||||
* cancel vestigial async tasks
|
||||
*/
|
||||
protected String fixLocation(String location_json) {
|
||||
|
||||
String location_name = "";
|
||||
void killAllTasks() {
|
||||
|
||||
if (!tasks.isEmpty()) {
|
||||
for (AsyncTask task : tasks) {
|
||||
try {
|
||||
JSONObject reader = new JSONObject(location_json);
|
||||
task.cancel(true);
|
||||
task = null;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
location_name = reader.getString("name");
|
||||
JSONObject address = reader.getJSONObject("address");
|
||||
/**
|
||||
* start an EventScraper async task and add to tasks list
|
||||
*
|
||||
* @param event_url
|
||||
*/
|
||||
void scrapeEvent(String event_url) {
|
||||
FbEventScraper scraper = new FbEventScraper(this, event_url);
|
||||
|
||||
String type = address.getString("@type");
|
||||
Log.d("scraperLog", "scrapeEvent: "+event_url);
|
||||
tasks.add(scraper);
|
||||
scraper.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
if (type.equals("PostalAddress")) {
|
||||
String postal_code = address.getString("postalCode");
|
||||
String address_locality = address.getString("addressLocality");
|
||||
String street_address = address.getString("streetAddress");
|
||||
// included in locality
|
||||
//String address_country = address.getString("addressCountry");
|
||||
/**
|
||||
* Callback for finished EventSCraper async task
|
||||
*
|
||||
* @param event Contains event information if scraping successful
|
||||
* @param error resId for error message
|
||||
*/
|
||||
void scrapeEventResultCallback(FbEvent event, int error) {
|
||||
|
||||
return location_name + ", "
|
||||
+ street_address + ", "
|
||||
+ postal_code + " "
|
||||
+ address_locality;
|
||||
if (event != null) {
|
||||
Log.d("scraperLog", "scrapeEventResultCallback: "+event.url);
|
||||
main.get().addEvent(event);
|
||||
main.get().input_helper(main.get().getString(R.string.done), false);
|
||||
} else if (url_type == url_type_enum.EVENT) {
|
||||
main.get().input_helper(main.get().getString(error), true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* start a page scraper and add to list of tasks
|
||||
*
|
||||
* @param page_url
|
||||
*/
|
||||
void scrapePage(String page_url) {
|
||||
FbPageScraper scraper = new FbPageScraper(this, page_url);
|
||||
|
||||
Log.d("scraperLog", "scrapePage: "+page_url);
|
||||
|
||||
tasks.add(scraper);
|
||||
scraper.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for page scraper async task
|
||||
*
|
||||
* @param event_urls List of event urls scraped from the event
|
||||
* @param error resId of error message if task list is empty
|
||||
*/
|
||||
protected void scrapePageResultCallback(List<String> event_urls, int error) {
|
||||
|
||||
if (event_urls.size() > 0) {
|
||||
Log.d("scraperLog", "scrapePageResultCallback: "+event_urls.toString());
|
||||
for (String event_url : event_urls) {
|
||||
try {
|
||||
String url = getEventUrl(event_url);
|
||||
Log.d("scraperLog", "scrapePageResultCallback: "+url);
|
||||
scrapeEvent(url);
|
||||
} catch (URISyntaxException | MalformedURLException e) {
|
||||
// ignore this event
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return location_name;
|
||||
main.get().input_helper(main.get().getString(error), true);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
return location_name;
|
||||
protected void redirectUrl (String url) {
|
||||
FbRedirectionResolver resolver = new FbRedirectionResolver(this, url);
|
||||
|
||||
Log.d("scraperLog", "redirectUrl: "+url);
|
||||
|
||||
resolver.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
protected void redirectionResultCallback(String url) {
|
||||
this.input_url = url;
|
||||
|
||||
Log.d("scraperLog", "redirectUrlCb: "+url);
|
||||
|
||||
// now try again with expanded url
|
||||
this.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a time string from the facebook event.
|
||||
* Corrects format to ISO date time and parse into ZonedDateTime
|
||||
*
|
||||
* @param time_in time string from the event
|
||||
* @return ZonedDateTime parsed from input or null
|
||||
* Start scraping input url
|
||||
*/
|
||||
protected ZonedDateTime toZonedDateTime(String time_in) {
|
||||
void run() {
|
||||
|
||||
// check if shortened url
|
||||
try {
|
||||
// time in is missing a : in the timezone offset
|
||||
Editable editable = new SpannableStringBuilder(time_in);
|
||||
String time_str = editable.insert(22, ":").toString();
|
||||
String shortened = getShortened(input_url);
|
||||
url_type = url_type_enum.SHORT;
|
||||
redirectUrl(shortened);
|
||||
|
||||
// parse e.g. 2011-12-03T10:15:30+01:00
|
||||
return ZonedDateTime.parse(time_str, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
||||
return;
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
url_type = url_type_enum.INVALID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all occurrences of a facebook internal links in
|
||||
* an event description into an actual URL.
|
||||
*
|
||||
* @param description_in description string from the event
|
||||
* @return corrected String with internal links resolved
|
||||
*/
|
||||
protected String fixDescriptionLinks(String description_in) {
|
||||
// check if input url is an event
|
||||
try {
|
||||
/* @[152580919265:274:SiteDescription]
|
||||
* to
|
||||
* SiteDescription [m.facebook.com/152580919265] */
|
||||
String event_url = getEventUrl(input_url);
|
||||
url_type = url_type_enum.EVENT;
|
||||
scrapeEvent(event_url);
|
||||
|
||||
return description_in.replaceAll("@\\[([0-9]{10,}):[0-9]{3}:([^]]*)]",
|
||||
"$2 [m.facebook.com/$1]");
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return description_in;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a single field from a JSONObject
|
||||
* @param reader JSONObject to read from
|
||||
* @param field Which field to read
|
||||
* @return String of the value of the field or empty string
|
||||
*/
|
||||
private String readFromJson(JSONObject reader, String field) {
|
||||
try {
|
||||
return reader.getString(field);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Started by scraper.execute().
|
||||
* Gets the HTML doc from the input string and scrapes the event information from it.
|
||||
* @param voids
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
|
||||
try {
|
||||
String url = fixURI(input_url);
|
||||
// useragent needed with Jsoup > 1.12
|
||||
Document document = Jsoup.connect(url).userAgent("Mozilla").get();
|
||||
String json = document
|
||||
.select("script[type = application/ld+json]")
|
||||
.first().data();
|
||||
|
||||
JSONObject reader = new JSONObject(json);
|
||||
|
||||
event = new FbEvent(
|
||||
url,
|
||||
readFromJson(reader, "name"),
|
||||
toZonedDateTime(readFromJson(reader, "startDate")),
|
||||
toZonedDateTime(readFromJson(reader, "endDate")),
|
||||
fixDescriptionLinks(readFromJson(reader, "description")),
|
||||
fixLocation(readFromJson(reader, "location")),
|
||||
readFromJson(reader, "image")
|
||||
);
|
||||
return;
|
||||
|
||||
} catch (URISyntaxException | MalformedURLException e) {
|
||||
e.printStackTrace();
|
||||
this.error = "Error: URL invalid.";
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
this.error = "Error: Scraping event data failed";
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
this.error = "Error: Unable to connect.";
|
||||
url_type = url_type_enum.INVALID;
|
||||
}
|
||||
// check if input url is a page
|
||||
try {
|
||||
String page_url = getPageUrl(input_url);
|
||||
url_type = url_type_enum.PAGE;
|
||||
scrapePage(page_url);
|
||||
|
||||
return null;
|
||||
}
|
||||
return;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
} catch (URISyntaxException | MalformedURLException e) {
|
||||
url_type = url_type_enum.INVALID;
|
||||
}
|
||||
// check if only page name without prefix
|
||||
try {
|
||||
String page_url = getPageUrl("https://mbasic.facebook.com/"+input_url);
|
||||
url_type = url_type_enum.PAGE;
|
||||
scrapePage(page_url);
|
||||
|
||||
/**
|
||||
* When scraping is finished, main activity will be updated.
|
||||
* If an error occurred, main activity is given an error string.
|
||||
* @param aVoid
|
||||
*/
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
super.onPostExecute(aVoid);
|
||||
|
||||
if (main != null) {
|
||||
if (this.event != null) {
|
||||
main.get().update(event);
|
||||
} else {
|
||||
main.get().error(error);
|
||||
main.get().clear(false);
|
||||
}
|
||||
}
|
||||
} catch (URISyntaxException | MalformedURLException e) {
|
||||
url_type = url_type_enum.INVALID;
|
||||
main.get().input_helper(main.get().getString(R.string.error_url), true);
|
||||
}
|
||||
}
|
||||
|
||||
// enum for storing url type in this class
|
||||
enum url_type_enum {SHORT, EVENT, PAGE, INVALID}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
package com.akdev.nofbeventscraper;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.webkit.WebSettingsCompat;
|
||||
import androidx.webkit.WebViewFeature;
|
||||
|
||||
public class HelpActivity extends AppCompatActivity {
|
||||
|
||||
|
@ -19,7 +22,17 @@ public class HelpActivity extends AppCompatActivity {
|
|||
|
||||
WebView webview_help = findViewById(R.id.webview_help);
|
||||
|
||||
webview_help.loadUrl("file:////android_asset/help.html");
|
||||
int night_mode_flags = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
if (night_mode_flags == Configuration.UI_MODE_NIGHT_YES) {
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
|
||||
WebSettingsCompat.setForceDark(webview_help.getSettings(),
|
||||
WebSettingsCompat.FORCE_DARK_ON);
|
||||
}
|
||||
}
|
||||
|
||||
webview_help.loadUrl("file:////android_res/raw/help.html");
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package com.akdev.nofbeventscraper;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
public class IntentReceiver extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
/*
|
||||
* Get data from intent: if launched by other application
|
||||
* via "share to" or "open with"
|
||||
*/
|
||||
Intent intent = getIntent();
|
||||
setResult(RESULT_OK, intent);
|
||||
|
||||
String data = intent.getDataString();
|
||||
String extra = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
|
||||
String input = (data != null) ? data : extra;
|
||||
|
||||
Intent main = new Intent(this, MainActivity.class);
|
||||
main.putExtra("InputLink", input);
|
||||
|
||||
this.startActivity(main);
|
||||
|
||||
finish();
|
||||
}
|
||||
}
|
|
@ -1,91 +1,186 @@
|
|||
package com.akdev.nofbeventscraper;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.provider.CalendarContract;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.menu.MenuBuilder;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.squareup.picasso.Picasso;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static com.akdev.nofbeventscraper.FbEvent.dateTimeToEpoch;
|
||||
import jp.wasabeef.recyclerview.animators.FadeInAnimator;
|
||||
|
||||
import static com.akdev.nofbeventscraper.FbEvent.createEventList;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
protected Button ok_button;
|
||||
protected Button paste_button;
|
||||
|
||||
protected TextInputEditText edit_text_uri_input;
|
||||
protected TextInputEditText edit_text_event_name;
|
||||
protected TextInputEditText edit_text_event_start;
|
||||
protected TextInputEditText edit_text_event_end;
|
||||
protected TextInputEditText edit_text_event_location;
|
||||
protected TextInputEditText edit_text_event_description;
|
||||
|
||||
protected ExtendedFloatingActionButton paste_button;
|
||||
protected AutoCompleteTextView edit_text_uri_input;
|
||||
protected TextInputLayout layout_uri_input;
|
||||
protected TextInputLayout layout_event_location;
|
||||
|
||||
protected ImageView image_view_toolbar;
|
||||
protected CollapsingToolbarLayout layout_toolbar;
|
||||
|
||||
protected FbScraper scraper;
|
||||
protected FbEvent event;
|
||||
protected List<FbEvent> events;
|
||||
EventAdapter adapter;
|
||||
LinearLayoutManager linear_layout_manager;
|
||||
|
||||
List<String> history;
|
||||
ArrayAdapter<String> history_adapter;
|
||||
|
||||
|
||||
private List<String> getHistory() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
|
||||
Gson gson = new Gson();
|
||||
String json = prefs.getString("history", "");
|
||||
|
||||
Type history_type = new TypeToken<List<String>>() {
|
||||
}.getType();
|
||||
List<String> list = gson.fromJson(json, history_type);
|
||||
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
private List<FbEvent> getSavedEvents() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
|
||||
Gson gson = new Gson();
|
||||
String json = prefs.getString("events", "");
|
||||
|
||||
Type event_list_type = new TypeToken<List<FbEvent>>() {
|
||||
}.getType();
|
||||
List<FbEvent> list = gson.fromJson(json, event_list_type);
|
||||
|
||||
if (list == null) {
|
||||
list = createEventList();
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/*
|
||||
* On resume from other activities, e.g. settings
|
||||
*/
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
/*
|
||||
* Clear events after saved events deleted from settings
|
||||
*/
|
||||
if (getSavedEvents().isEmpty()) {
|
||||
events.clear();
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
if (getHistory().isEmpty()) {
|
||||
history.clear();
|
||||
history_adapter.clear();
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/*
|
||||
* Intent from IntentReceiver - read only once
|
||||
*/
|
||||
Intent intent = getIntent();
|
||||
String data = intent.getStringExtra("InputLink");
|
||||
|
||||
if (data != null) {
|
||||
intent.removeExtra("InputLink");
|
||||
edit_text_uri_input.setText(data);
|
||||
startScraping();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save events list to SharedPreferences as JSON
|
||||
*/
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
SharedPreferences.Editor prefs_edit = prefs.edit();
|
||||
Gson gson = new Gson();
|
||||
String json = gson.toJson(events);
|
||||
prefs_edit.putString("events", json);
|
||||
|
||||
json = gson.toJson(history);
|
||||
prefs_edit.putString("history", json);
|
||||
prefs_edit.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
edit_text_uri_input = findViewById(R.id.edit_text_uri_input);
|
||||
layout_uri_input = findViewById(R.id.layout_uri_input);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ok_button = (Button) findViewById(R.id.ok_button);
|
||||
paste_button = (Button) findViewById(R.id.paste_button);
|
||||
|
||||
edit_text_uri_input = (TextInputEditText) findViewById(R.id.edit_text_uri_input);
|
||||
edit_text_event_name = (TextInputEditText) findViewById(R.id.edit_text_event_name);
|
||||
edit_text_event_start = (TextInputEditText) findViewById(R.id.edit_text_event_start);
|
||||
edit_text_event_end = (TextInputEditText) findViewById(R.id.edit_text_event_end);
|
||||
edit_text_event_location = (TextInputEditText) findViewById(R.id.edit_text_event_location);
|
||||
edit_text_event_description = (TextInputEditText) findViewById(R.id.edit_text_event_description);
|
||||
|
||||
layout_uri_input = (TextInputLayout) findViewById(R.id.layout_uri_input);
|
||||
layout_event_location = (TextInputLayout) findViewById(R.id.layout_event_location);
|
||||
layout_toolbar = (CollapsingToolbarLayout) findViewById(R.id.layout_toolbar);
|
||||
image_view_toolbar = (ImageView) findViewById(R.id.image_view);
|
||||
|
||||
paste_button = findViewById(R.id.paste_button);
|
||||
|
||||
/*
|
||||
* Default view settings
|
||||
* initialize recycler view with saved list of events
|
||||
* scroll horizontal with snapping
|
||||
*/
|
||||
ok_button.setEnabled(false);
|
||||
layout_event_location.setEndIconVisible(false);
|
||||
image_view_toolbar.setImageResource(R.drawable.ic_banner_foreground);
|
||||
RecyclerView recycler_view = findViewById(R.id.recycler_view);
|
||||
this.events = getSavedEvents();
|
||||
adapter = new EventAdapter(events);
|
||||
recycler_view.setAdapter(adapter);
|
||||
linear_layout_manager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
|
||||
recycler_view.setLayoutManager(linear_layout_manager);
|
||||
|
||||
// restore history
|
||||
this.history = getHistory();
|
||||
history_adapter = new ArrayAdapter<>(this, android.R.layout.simple_expandable_list_item_1, history);
|
||||
|
||||
recycler_view.setItemAnimator(new FadeInAnimator());
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Display title only when toolbar is collapsed
|
||||
*/
|
||||
AppBarLayout app_bar_layout = (AppBarLayout) findViewById(R.id.app_bar);
|
||||
AppBarLayout app_bar_layout = findViewById(R.id.app_bar);
|
||||
app_bar_layout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
|
||||
boolean show = true;
|
||||
int scroll_range = -1;
|
||||
CollapsingToolbarLayout layout_toolbar = findViewById(R.id.layout_toolbar);
|
||||
|
||||
@Override
|
||||
public void onOffsetChanged(AppBarLayout app_bar_layout, int vertical_offset) {
|
||||
|
@ -113,67 +208,41 @@ public class MainActivity extends AppCompatActivity {
|
|||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
String str = clipboard.getPrimaryClip().getItemAt(0).getText().toString();
|
||||
|
||||
clear(true);
|
||||
edit_text_uri_input.setText(str);
|
||||
} catch (NullPointerException e) {
|
||||
e.printStackTrace();
|
||||
error("Error: Clipboard empty");
|
||||
}
|
||||
|
||||
startScraping();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
input_helper(getString(R.string.error_clipboard_empty), true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Clear button: delete all text in all fields
|
||||
* Error in input: clear input on click
|
||||
*/
|
||||
View.OnClickListener listener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
clear(true);
|
||||
input_helper(getString(R.string.helper_add_link), true);
|
||||
edit_text_uri_input.setText(null);
|
||||
if (scraper != null) {
|
||||
scraper.killAllTasks();
|
||||
}
|
||||
input_helper(getString(R.string.helper_add_link), false);
|
||||
}
|
||||
};
|
||||
layout_uri_input.setEndIconOnClickListener(listener);
|
||||
layout_uri_input.setErrorIconOnClickListener(listener);
|
||||
layout_uri_input.setEndIconOnClickListener(listener);
|
||||
edit_text_uri_input.setAdapter(history_adapter);
|
||||
|
||||
/*
|
||||
* Maps button: launch maps intent
|
||||
*/
|
||||
layout_event_location.setEndIconOnClickListener(new View.OnClickListener() {
|
||||
layout_uri_input.setStartIconOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
String map_search = "geo:0,0?q=" + edit_text_event_location.getText();
|
||||
|
||||
Uri intent_uri = Uri.parse(map_search);
|
||||
Intent map_intent = new Intent(Intent.ACTION_VIEW, intent_uri);
|
||||
if (map_intent.resolveActivity(getPackageManager()) != null) {
|
||||
startActivity(map_intent);
|
||||
}
|
||||
edit_text_uri_input.showDropDown();
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Add to calendar button: launch calendar application
|
||||
*/
|
||||
ok_button.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
|
||||
Long start_epoch = dateTimeToEpoch(event.start_date);
|
||||
Long end_epoch = dateTimeToEpoch(event.end_date);
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_EDIT);
|
||||
intent.setType("vnd.android.cursor.item/event");
|
||||
intent.putExtra(CalendarContract.Events.TITLE, event.name);
|
||||
intent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, start_epoch);
|
||||
intent.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, end_epoch);
|
||||
intent.putExtra(CalendarContract.Events.EVENT_LOCATION, event.location);
|
||||
|
||||
// prepend url in description
|
||||
String desc = event.url + "\n\n" + event.description;
|
||||
intent.putExtra(CalendarContract.Events.DESCRIPTION, desc);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Enter button in uri input: start scraping
|
||||
|
@ -183,6 +252,13 @@ public class MainActivity extends AppCompatActivity {
|
|||
//If the key event is a key-down event on the "enter" button
|
||||
if ((keyevent.getAction() == KeyEvent.ACTION_DOWN) && (keycode == KeyEvent.KEYCODE_ENTER)) {
|
||||
startScraping();
|
||||
|
||||
// do not focus next view, just release it
|
||||
edit_text_uri_input.clearFocus();
|
||||
|
||||
// close soft keyboard
|
||||
InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -190,154 +266,85 @@ public class MainActivity extends AppCompatActivity {
|
|||
});
|
||||
|
||||
|
||||
/*
|
||||
* Get data from intent: if launched by other application
|
||||
* via "share to" or "open with"
|
||||
*/
|
||||
Intent intent = getIntent();
|
||||
Uri data = intent.getData();
|
||||
String shared_text = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
|
||||
if (data != null) {
|
||||
// opening external fb link
|
||||
edit_text_uri_input.setText(data.toString());
|
||||
startScraping();
|
||||
} else if (shared_text != null) {
|
||||
//share to nofb
|
||||
edit_text_uri_input.setText(shared_text);
|
||||
startScraping();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* launch the FbScraper asynchronous task with the current text in the input text field.
|
||||
* launch the FbScraper with the current text in the input text field.
|
||||
*/
|
||||
public void startScraping() {
|
||||
|
||||
error(null);
|
||||
input_helper(null, false);
|
||||
|
||||
String url = Objects.requireNonNull(edit_text_uri_input.getText()).toString();
|
||||
|
||||
scraper = new FbScraper(new WeakReference<>(this), url);
|
||||
scraper.execute();
|
||||
|
||||
scraper.run();
|
||||
|
||||
history_adapter.insert(url, 0);
|
||||
history.add(0, url);
|
||||
}
|
||||
|
||||
public void error(String str) {
|
||||
/**
|
||||
* manage Helper text on uri_input
|
||||
*
|
||||
* @param str What should be displayed
|
||||
* @param error True if should be displayed as error
|
||||
*/
|
||||
public void input_helper(String str, boolean error) {
|
||||
|
||||
if (str == null) {
|
||||
str = " ";
|
||||
} // keep spacing
|
||||
|
||||
if (error) {
|
||||
layout_uri_input.setError(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all event text field strings and errors and also the input field depending if wanted.
|
||||
* Loads the default banner into the toolbar image view and disables unneeded buttons.
|
||||
*
|
||||
* @param clear_uri Choose whether to clear the input uri field, too
|
||||
*/
|
||||
public void clear(boolean clear_uri) {
|
||||
|
||||
if (clear_uri) {
|
||||
edit_text_uri_input.setText("");
|
||||
} else {
|
||||
layout_uri_input.setError(null);
|
||||
layout_uri_input.setHelperText(str);
|
||||
}
|
||||
edit_text_event_name.setText("");
|
||||
edit_text_event_start.setText("");
|
||||
edit_text_event_end.setText("");
|
||||
edit_text_event_location.setText("");
|
||||
edit_text_event_description.setText("");
|
||||
|
||||
edit_text_event_name.setError(null);
|
||||
edit_text_event_start.setError(null);
|
||||
edit_text_event_end.setError(null);
|
||||
edit_text_event_location.setError(null);
|
||||
edit_text_event_description.setError(null);
|
||||
|
||||
try {
|
||||
scraper.cancel(true);
|
||||
scraper = null;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
ok_button.setEnabled(false);
|
||||
layout_event_location.setEndIconVisible(false);
|
||||
image_view_toolbar.setImageResource(R.drawable.ic_banner_foreground);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the text fields with the event information provided.
|
||||
* If something is missing, the corresponding test field will show an error.
|
||||
* Adds new events to the start of the events list.
|
||||
*
|
||||
* @param scraped_event the event information that was scraped by FbScraper
|
||||
* @param new_event the event that was scraped by FbScraper
|
||||
*/
|
||||
public void update(FbEvent scraped_event) {
|
||||
public void addEvent(FbEvent new_event) {
|
||||
|
||||
this.event = scraped_event;
|
||||
|
||||
edit_text_uri_input.setText(event.url);
|
||||
|
||||
if (event.name.equals("")) {
|
||||
edit_text_event_name.setError("no event name detected");
|
||||
} else {
|
||||
edit_text_event_name.setText(event.name);
|
||||
if (new_event != null) {
|
||||
this.events.add(0, new_event);
|
||||
this.adapter.notifyItemInserted(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.start_date == null) {
|
||||
edit_text_event_start.setError("no event start date detected");
|
||||
} else {
|
||||
String str = FbEvent.dateTimeToString(event.start_date);
|
||||
edit_text_event_start.setText(str);
|
||||
}
|
||||
|
||||
if (event.end_date == null) {
|
||||
edit_text_event_end.setError("no event end date detected");
|
||||
} else {
|
||||
String str = FbEvent.dateTimeToString(event.end_date);
|
||||
edit_text_event_end.setText(str);
|
||||
}
|
||||
|
||||
if (event.location.equals("")) {
|
||||
edit_text_event_location.setError("no event location detected");
|
||||
} else {
|
||||
edit_text_event_location.setText(event.location);
|
||||
layout_event_location.setEndIconVisible(true);
|
||||
}
|
||||
|
||||
if (event.description.equals("")) {
|
||||
edit_text_event_description.setError("no event description detected");
|
||||
} else {
|
||||
edit_text_event_description.setText(event.description);
|
||||
}
|
||||
|
||||
Picasso.get()
|
||||
.load(event.image_url)
|
||||
.placeholder(R.drawable.ic_banner_foreground)
|
||||
.into(image_view_toolbar);
|
||||
|
||||
ok_button.setEnabled(true);
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||
|
||||
/*
|
||||
* Display icons, restricted API, maybe find other solution?
|
||||
*/
|
||||
if (menu instanceof MenuBuilder) {
|
||||
MenuBuilder m = (MenuBuilder) menu;
|
||||
//noinspection RestrictedApi
|
||||
m.setOptionalIconsVisible(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch menu item to new activity
|
||||
*
|
||||
* @param item
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
// Handle action bar item clicks here. The action bar will
|
||||
// automatically handle clicks on the Home/Up button, so long
|
||||
// as you specify a parent activity in AndroidManifest.xml.
|
||||
|
||||
int id = item.getItemId();
|
||||
|
||||
//noinspection SimplifiableIfStatement
|
||||
if (id == R.id.action_about) {
|
||||
startActivity(new Intent(this, AboutActivity.class));
|
||||
return true;
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
package com.akdev.nofbeventscraper;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
|
@ -26,6 +31,40 @@ public class SettingsActivity extends AppCompatActivity {
|
|||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
setPreferencesFromResource(R.xml.root_preferences, rootKey);
|
||||
|
||||
/*
|
||||
* reset events click action: delete saved events and display snackbar
|
||||
*/
|
||||
Preference button = findPreference("event_reset");
|
||||
if (button != null) {
|
||||
button.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
|
||||
final SharedPreferences prefs = preference.getSharedPreferences();
|
||||
|
||||
final String events = prefs.getString("events", "");
|
||||
prefs.edit().remove("events").apply();
|
||||
|
||||
final String history = prefs.getString("history", "");
|
||||
prefs.edit().remove("history").apply();
|
||||
|
||||
Snackbar.make(getActivity().findViewById(android.R.id.content),
|
||||
getString(R.string.preferences_event_snackbar), Snackbar.LENGTH_SHORT)
|
||||
.setAction(R.string.undo, new View.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
prefs.edit().putString("events", events).apply();
|
||||
prefs.edit().putString("history", history).apply();
|
||||
}
|
||||
}).show();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<size
|
||||
android:height="8dp"
|
||||
android:width="0dp"/>
|
||||
</shape>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,2C8.14,2 5,5.14 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.86 -3.14,-7 -7,-7zM16,10h-3v3h-2v-3L8,10L8,8h3L11,5h2v3h3v2z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
|
@ -1,63 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="120dp"
|
||||
android:height="120dp"
|
||||
android:viewportWidth="120"
|
||||
android:viewportHeight="120">
|
||||
<group android:scaleX="0.078457944"
|
||||
android:scaleY="0.078457944"
|
||||
android:translateX="34.815"
|
||||
android:translateY="34.815">
|
||||
<path
|
||||
android:pathData="M0.5,0.5h640v640h-640z"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M0.5,120.5h640v410h-640z"
|
||||
android:fillColor="#297da6"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M6.5,40.5L634.5,40.5A6,6 0,0 1,640.5 46.5L640.5,134.5A6,6 0,0 1,634.5 140.5L6.5,140.5A6,6 0,0 1,0.5 134.5L0.5,46.5A6,6 0,0 1,6.5 40.5z"
|
||||
android:fillColor="#74b42b"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M519.5,0.5L561.5,0.5A9,9 0,0 1,570.5 9.5L570.5,101.5A9,9 0,0 1,561.5 110.5L519.5,110.5A9,9 0,0 1,510.5 101.5L510.5,9.5A9,9 0,0 1,519.5 0.5z"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M399.5,0.5L441.5,0.5A9,9 0,0 1,450.5 9.5L450.5,101.5A9,9 0,0 1,441.5 110.5L399.5,110.5A9,9 0,0 1,390.5 101.5L390.5,9.5A9,9 0,0 1,399.5 0.5z"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M199.5,0.5L241.5,0.5A9,9 0,0 1,250.5 9.5L250.5,101.5A9,9 0,0 1,241.5 110.5L199.5,110.5A9,9 0,0 1,190.5 101.5L190.5,9.5A9,9 0,0 1,199.5 0.5z"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M79.5,0.5L121.5,0.5A9,9 0,0 1,130.5 9.5L130.5,101.5A9,9 0,0 1,121.5 110.5L79.5,110.5A9,9 0,0 1,70.5 101.5L70.5,9.5A9,9 0,0 1,79.5 0.5z"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M526.5,10.5L554.5,10.5A6,6 0,0 1,560.5 16.5L560.5,94.5A6,6 0,0 1,554.5 100.5L526.5,100.5A6,6 0,0 1,520.5 94.5L520.5,16.5A6,6 0,0 1,526.5 10.5z"
|
||||
android:fillColor="#42a3d2"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M406.5,10.5L434.5,10.5A6,6 0,0 1,440.5 16.5L440.5,94.5A6,6 0,0 1,434.5 100.5L406.5,100.5A6,6 0,0 1,400.5 94.5L400.5,16.5A6,6 0,0 1,406.5 10.5z"
|
||||
android:fillColor="#42a3d2"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M86.5,10.5L114.5,10.5A6,6 0,0 1,120.5 16.5L120.5,94.5A6,6 0,0 1,114.5 100.5L86.5,100.5A6,6 0,0 1,80.5 94.5L80.5,16.5A6,6 0,0 1,86.5 10.5z"
|
||||
android:fillColor="#42a3d2"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M206.5,10.5L234.5,10.5A6,6 0,0 1,240.5 16.5L240.5,94.5A6,6 0,0 1,234.5 100.5L206.5,100.5A6,6 0,0 1,200.5 94.5L200.5,16.5A6,6 0,0 1,206.5 10.5z"
|
||||
android:fillColor="#42a3d2"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M9.5,490.5L631.5,490.5A9,9 0,0 1,640.5 499.5L640.5,631.5A9,9 0,0 1,631.5 640.5L9.5,640.5A9,9 0,0 1,0.5 631.5L0.5,499.5A9,9 0,0 1,9.5 490.5z"
|
||||
android:fillColor="#297da6"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M155.5,295.5L281.94,295.5L320.5,174.5L359.06,295.5L485.5,295.5L384.76,376.53L424.71,504.5L320.5,425.3L216.29,504.5L256.24,376.53Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.25,2.52 0.77,-1.28 -3.52,-2.09L13.5,8z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M20.5,3l-0.16,0.03L15,5.1 9,3 3.36,4.9c-0.21,0.07 -0.36,0.25 -0.36,0.48V20.5c0,0.28 0.22,0.5 0.5,0.5l0.16,-0.03L9,18.9l6,2.1 5.64,-1.9c0.21,-0.07 0.36,-0.25 0.36,-0.48V3.5c0,-0.28 -0.22,-0.5 -0.5,-0.5zM15,19l-6,-2.11V5l6,2.11V19z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
|
@ -1,15 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".AboutActivity">
|
||||
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview_about"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</RelativeLayout>
|
|
@ -1,15 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".HelpActivity">
|
||||
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview_help"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</RelativeLayout>
|
|
@ -1,9 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/coordinator_layout"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
|
@ -21,22 +20,20 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/colorPrimary"
|
||||
android:fitsSystemWindows="true"
|
||||
app:contentScrim="?attr/colorPrimary"
|
||||
app:expandedTitleGravity="center"
|
||||
app:contentScrim="?attr/colorPrimary"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed"
|
||||
app:title=" "
|
||||
app:toolbarId="@+id/toolbar">
|
||||
|
||||
<!-- android:src="@drawable/ic_banner_foreground"-->
|
||||
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/image_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_collapseMode="parallax" />
|
||||
android:src="@drawable/ic_banner_foreground"
|
||||
app:layout_collapseMode="pin" />
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
|
@ -48,7 +45,6 @@
|
|||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
|
@ -59,35 +55,10 @@
|
|||
app:layout_anchor="@id/app_bar"
|
||||
app:layout_anchorGravity="bottom|end"
|
||||
app:icon="@drawable/ic_content_paste"
|
||||
android:text="@string/paste_button"/>
|
||||
|
||||
android:text="@android:string/paste"
|
||||
android:tooltipText="@string/tooltip_paste"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
<include layout="@layout/content_main" />
|
||||
|
||||
<com.google.android.material.bottomappbar.BottomAppBar
|
||||
android:id="@+id/bottom_appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_margin="0dp"
|
||||
app:contentInsetStart="0dp">
|
||||
|
||||
|
||||
<Button
|
||||
android:id="@+id/ok_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="12dp"
|
||||
android:text="@string/add_to_calendar"
|
||||
android:textColor="#FFFFFF"
|
||||
app:cornerRadius="24dp"
|
||||
app:icon="@drawable/ic_event_available"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="16dp"
|
||||
app:iconTint="#FFFFFF" />
|
||||
|
||||
</com.google.android.material.bottomappbar.BottomAppBar>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -1,145 +1,52 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/nested_scroll_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="false"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:context=".MainActivity"
|
||||
tools:showIn="@layout/activity_main">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginBottom="64dp"
|
||||
|
||||
android:divider="@drawable/divider"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:showDividers="middle">
|
||||
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_uri_input"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
app:endIconCheckable="false"
|
||||
app:endIconDrawable="@drawable/ic_backspace_black"
|
||||
app:endIconMode="clear_text"
|
||||
app:errorIconDrawable="@drawable/ic_backspace_black"
|
||||
app:endIconMode="custom"
|
||||
app:helperText="@string/helper_add_link"
|
||||
app:helperTextEnabled="true"
|
||||
app:startIconDrawable="@drawable/ic_history_black">
|
||||
|
||||
app:helperText="@string/add_link_helper"
|
||||
app:helperTextEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
android:id="@+id/edit_text_uri_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="web"
|
||||
android:cursorVisible="true"
|
||||
android:hint="@string/add_link_hint"
|
||||
android:hint="@string/hint_add_link"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:singleLine="true"
|
||||
android:textColorLink="@color/material_on_background_emphasis_high_type" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_text_event_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="false"
|
||||
android:cursorVisible="false"
|
||||
android:hint="Event name"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:singleLine="true"
|
||||
android:textColorLink="@color/material_on_background_emphasis_high_type" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_text_event_start"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="false"
|
||||
android:cursorVisible="false"
|
||||
android:hint="Event start"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:singleLine="true" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_text_event_end"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="false"
|
||||
android:cursorVisible="false"
|
||||
android:hint="Event end"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:singleLine="true" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/layout_event_location"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:endIconDrawable="@drawable/ic_add_location"
|
||||
app:endIconMode="custom">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_text_event_location"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="map"
|
||||
android:focusable="false"
|
||||
android:cursorVisible="false"
|
||||
android:hint="Event location"
|
||||
android:inputType="textNoSuggestions"
|
||||
android:singleLine="true" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/edit_text_event_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="web"
|
||||
android:focusable="false"
|
||||
android:cursorVisible="false"
|
||||
android:hint="Event description"
|
||||
android:inputType="textNoSuggestions|textMultiLine"
|
||||
android:singleLine="false"
|
||||
android:textColorLink="@color/material_on_background_emphasis_high_type" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
android:nestedScrollingEnabled="false" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
|
||||
android:id="@+id/image_view_event_image_fullscreen"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
</LinearLayout>
|
|
@ -0,0 +1,156 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_view_event_image"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="thumbnail"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_view_event_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
android:text="@string/event_placeholder"
|
||||
android:textAppearance="?attr/textAppearanceHeadline6" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_view_event_location"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_map"
|
||||
app:tint="@color/material_on_surface_emphasis_high_type" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_view_event_location"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/event_placeholder"
|
||||
android:textAppearance="?attr/textAppearanceBody1" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_view_event_time"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clickable="false"
|
||||
android:src="@drawable/ic_schedule"
|
||||
app:tint="@color/material_on_surface_emphasis_high_type" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_view_event_start"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/event_placeholder"
|
||||
android:textAppearance="?attr/textAppearanceBody2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_view_event_end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/event_placeholder"
|
||||
android:textAppearance="?attr/textAppearanceBody2" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_view_event_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="5"
|
||||
android:text="@string/event_placeholder"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_add_to_calendar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/button_add"
|
||||
android:textColor="@android:color/white"
|
||||
app:icon="@drawable/ic_event_available"
|
||||
app:iconGravity="textStart"
|
||||
app:iconTint="@android:color/white" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_view_share"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_share"
|
||||
app:tint="@color/material_on_surface_emphasis_high_type" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_channel_background"/>
|
||||
<foreground android:drawable="@drawable/ic_channel_foreground"/>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<h3>Open Source</h3>
|
||||
<p>Der Quellcode für diese Anwendung ist bei <a href=" https://github.com/akaessens/NoFbEventScraper">GitHub</a> verfügbar.<strong><br /></strong></p>
|
||||
<p>Wenn Sie auf ein Problem stoßen, melden Sie es mir bitte anonym über den <a href="https://gitreports.com/issue/akaessens/NoFbEventScraper">Bugtracker</a> oder direkt bei <a href="https://github.com/akaessens/NoFbEventScraper/issues">GitHub</a>.</p>
|
||||
<h3>Updates</h3>
|
||||
<p>Diese App ist bei <a href="https://f-droid.org/de/packages/com.akdev.nofbeventscraper">F-Droid</a> zum Download verfügbar. Das Änderungsprotokoll ist auf <a href="https://github.com/akaessens/NoFbEventScraper/blob/master/CHANGELOG.md">GitHub</a> verfügbar.</p>
|
||||
<p><a href="https://f-droid.org/en/packages/com.akdev.nofbeventscraper"> <img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="75" /> </a></p>
|
||||
<h3>Rechtliches</h3>
|
||||
<p>Diese Anwendung ist für das Speichern einzelner, öffentlich zugänglicher Veranstaltungen in einem persönlichen Kalender vorgesehen. Verwenden Sie sie nicht zur automatischen Datenerfassung und halten Sie sich an die <a href="http://www.facebook.com/apps/site_scraping_tos_terms.php">Bedingungen für die automatische Datenerfassung</a> von Facebook.</p>
|
||||
<h3>Spenden</h3>
|
||||
<p>Ich entwickle diese Anwendung in meiner Freizeit. Wenn sie Ihnen gefällt, können Sie bei <a href="https://www.paypal.me/andreaskaessens">PayPal</a> spenden.</p>
|
||||
<p><a title="PayPal" href="https://www.paypal.me/andreaskaessens"><img src="https://raw.githubusercontent.com/stefan-niedermann/paypal-donate-button/master/paypal-donate-button.png" height="75" /></a></p>
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<h3>Welche Links können mit dieser App verwendet werden?</h3>
|
||||
<p>Alle Facebook-Subdomains werden unterstützt, ob mobil (m.facebook.com) oder sprachspezifisch (de-de.facebook.com). Der Link muss eine Ereignis-ID enthalten.<br />Wie kann diese Anwendung verwendet werden?</p>
|
||||
<h3>Wie kann diese Anwendung verwendet werden?</h3>
|
||||
<ul>
|
||||
<li><strong>Einfügen-Knopf</strong>: Fügen Sie einen kopierten Link aus der Zwischenablage in die URL-Leiste ein.</li>
|
||||
<li><strong>Teilen mit</strong>: Eingebaute Android-Freigabefunktion, z.B. von einem Browser aus.</li>
|
||||
<li><strong>Öffnen mit</strong>: Die in Android eingebaute öffnen-mit-Funktion, z.B. beim Klicken auf einen Link in einem Messenger.</li>
|
||||
</ul>
|
||||
<h3>Warum funktioniert die Veranstaltung X nicht?</h3>
|
||||
<p>Diese Anwendung greift auf öffentlich zugängliche Ereignisinformationen zu. Wenn die Veranstaltung z.B. den Ort ohne Login nicht anbietet, ist dies in dieser Anwendung nicht verfügbar. Auch bieten einige Veranstaltungen die Informationen einfach nicht in einem maschinenlesbaren Format an. Veranstaltungen mit mehreren Instanzen sind problematisch, da sie beim Scraping von m.facebook.com nicht das korrekte Start- und Enddatum angeben.</p>
|
||||
<p>Wenn Sie Probleme mit einem bestimmten Ereignis haben, lassen Sie es mich bitte über den <a href="https://gitreports.com/issue/akaessens/NoFbEventScraper">anonymen Bugtracker</a> oder auf der <a href="https://github.com/akaessens/NoFbEventScraper/issues/">GitHub Problemseite</a>.</p>
|
||||
<h3>Ist diese App kompatibel mit meinem Kalender?</h3>
|
||||
<p>Ja. Diese Anwendung verwendet anwendungsunabhängige Kalenderfunktionen, wodurch sie mit jeder Kalenderanwendung kompatibel ist. Ich empfehle jedoch den <a href="https://play.google.com/store/apps/details?id=ws.xsoh.etar">Etar Kalender</a>, da er Open Source ist. </p>
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<h3>Código abierto</h3>.
|
||||
<p>El código fuente de esta aplicación está disponible en <a href=" https://github.com/akaessens/NoFbEventScraper">GitHub</a>.<strong><br /></strong></p>
|
||||
<p>Si encuentras un problema, por favor, infórmame de forma anónima a través del <a href="https://gitreports.com/issue/akaessens/NoFbEventScraper">bug tracker</a> o directamente en <a href="https://github.com/akaessens/NoFbEventScraper/issues">GitHub</a>.</p>
|
||||
<h3>Actualizaciones</h3>.
|
||||
<p>Esta aplicación está disponible para su descarga en <a href="https://f-droid.org/de/packages/com.akdev.nofbeventscraper">F-Droid</a>. El registro de cambios está disponible en <a href="https://github.com/akaessens/NoFbEventScraper/blob/master/CHANGELOG.md">GitHub</a>.</p>.
|
||||
<p><a href="https://f-droid.org/en/packages/com.akdev.nofbeventscraper"> <img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="75" /> </a></p>
|
||||
<h3>Aviso legal</h3>.
|
||||
<p>Esta aplicación está pensada para almacenar eventos individuales y públicos en un calendario personal. No lo utilices para la recopilación automática de datos y respeta las <a href="http://www.facebook.com/apps/site_scraping_tos_terms.php">Condiciones de recopilación automática de datos</a> de Facebook.
|
||||
<h3>Donación</h3>
|
||||
<p>Estoy desarrollando esta aplicación en mi tiempo libre. Si te gusta, puedes donar en <a href="https://www.paypal.me/andreaskaessens">PayPal</a>.</p>
|
||||
<p><a title="PayPal" href="https://www.paypal.me/andreaskaessens"><img src="https://raw.githubusercontent.com/stefan-niedermann/paypal-donate-button/master/paypal-donate-button.png" height="75" /></a></p>
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<h3>¿Qué enlaces se pueden utilizar con esta aplicación?</h3>
|
||||
<p>Se admiten todos los subdominios de facebook, ya sean móviles (m.facebook.com) o específicos del idioma (de-de.facebook.com). El enlace debe contener un ID de evento.</p>
|
||||
<h3>¿Cómo utilizar esta aplicación?</h3>
|
||||
<ul>
|
||||
<li><strong>Botón pegar</strong>: Basta con pegar un enlace copiado desde el portapapeles en la barra de URL.</li>
|
||||
<li><strong>Compartir vía</strong>: La función de compartir incorporada en Android, por ejemplo, desde un navegador.</li>
|
||||
<li><strong>Abrir con</strong>: La función integrada de Android de abrir con, por ejemplo, al hacer clic desde un servicio de mensajería.</li>
|
||||
</ul>
|
||||
<h3>¿Por qué no funciona el evento X?</h3>
|
||||
<p>Esta app se basa en la información del evento que está disponible públicamente. Si el evento no ofrece, por ejemplo, la ubicación sin necesidad de iniciar sesión, no se encontrará disponible en esta aplicación. Además, algunos eventos simplemente no proporcionan la información en un formato legible por la máquina. Los eventos con múltiples instancias son problemáticos porque no proporcionan la fecha correcta de inicio y fin cuando se extraen de m.facebook.com.</p>
|
||||
<p>Si encuentras problemas con un evento específico, por favor, házmelo saber a través de <a href="https://gitreports.com/issue/akaessens/NoFbEventScraper">bugtracker anónimo</a> o en la <a href="https://github.com/akaessens/NoFbEventScraper/issues/">página de incidentes de GitHub</a>.</p>
|
||||
<h3>¿Esta app se integra con mi aplicación de calendario?</h3>
|
||||
<p>Sí. Esta aplicación hace uso de funciones de calendario independientes de la aplicación, lo que la hace compatible con cualquier app de calendario. Sin embargo, en lo personal recomiendo <a href="https://play.google.com/store/apps/details?id=ws.xsoh.etar">Calendario de Etar</a> porque es Open Source.</p>
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<h3>Open Source</h3>
|
||||
<p>The source code for this application is available at <a href=" https://github.com/akaessens/NoFbEventScraper">GitHub</a>.<strong><br /></strong></p>
|
||||
<p>If you encounter an issue, please report it to me anonymously at the <a href="https://gitreports.com/issue/akaessens/NoFbEventScraper">Bugtracker</a> or directly at <a href="https://github.com/akaessens/NoFbEventScraper/issues">GitHub</a>.</p>
|
||||
<h3>Updates</h3>
|
||||
<p>This application is available for download at <a href="https://f-droid.org/de/packages/com.akdev.nofbeventscraper">F-Droid</a>. The changelog is at <a href="https://github.com/akaessens/NoFbEventScraper/blob/master/CHANGELOG.md">GitHub</a>.</p>
|
||||
<p><a href="https://f-droid.org/en/packages/com.akdev.nofbeventscraper"> <img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="75" /> </a></p>
|
||||
<h3>Legal</h3>
|
||||
<p>This application is intended for saving single, publicly available events into a personal calendar. Do not use it to automatically collect data and comply with Facebook's <a href="http://www.facebook.com/apps/site_scraping_tos_terms.php">Automated Data Collection Terms</a>.</p>
|
||||
<h3>Donations</h3>
|
||||
<p>I develop this application in my free time. If you like it, you can donate at <a href="https://www.paypal.me/andreaskaessens">PayPal</a>.</p>
|
||||
<p><a title="PayPal" href="https://www.paypal.me/andreaskaessens"><img src="https://raw.githubusercontent.com/stefan-niedermann/paypal-donate-button/master/paypal-donate-button.png" height="75" /></a></p>
|
|
@ -8,7 +8,7 @@
|
|||
<li><strong>Open with</strong>: Android's built-in open-with-function, e.g. when clicking on a link in a messenger.</li>
|
||||
</ul>
|
||||
<h3>Why does event X not work?</h3>
|
||||
<p>This app relies on event information that is publicy available. If the event does not offer for example the location without a login, it will not be available in this application. Events with multiple instances are problematic because they do not provide the correct start and end date when scraping from m.facebook.com.</p>
|
||||
<p>This app relies on event information that is publicly available. If the event does not offer for example the location without a login, it will not be available in this application. Also, some events just do not provide the information in a machine readable format. Events with multiple instances are problematic because they do not provide the correct start and end date when scraping from m.facebook.com.</p>
|
||||
<p>If you encounter issues with a specific event, please let me know via the <a href="https://gitreports.com/issue/akaessens/NoFbEventScraper">anonymous bugtracker</a> or at the <a href="https://github.com/akaessens/NoFbEventScraper/issues/">GitHub issue page</a>.</p>
|
||||
<h3>Is this compatible with my calendar app?</h3>
|
||||
<p>Yes. This application makes use of application independent calendar functions, which makes it compatible to every calendar app. However, i recommend <a href="https://play.google.com/store/apps/details?id=ws.xsoh.etar&hl=de">Etar Calendar</a> because it is Open Source.</p>
|
||||
<p>Yes. This application makes use of application independent calendar functions, which makes it compatible to every calendar app. However, i recommend <a href="https://play.google.com/store/apps/details?id=ws.xsoh.etar">Etar Calendar</a> because it is Open Source.</p>
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">NoFb Event Scraper</string>
|
||||
<string name="action_about">Über</string>
|
||||
<string name="action_help">Hilfe</string>
|
||||
<string name="action_settings">Einstellungen</string>
|
||||
<string name="hint_add_link">Veranstaltungslink</string>
|
||||
<string name="helper_add_link">Facebook-Link zur Veranstaltung einfügen</string>
|
||||
<string name="button_add">Zum Kalender hinzufügen</string>
|
||||
<string name="tooltip_paste">Einfügen von Inhalten aus der Zwischenablage in das URL-Eingabefeld</string>
|
||||
<string name="preferences_url_setting">Welcher URL-Präfix ist zu verwenden?</string>
|
||||
<string name="preferences_url_setting_summary">mbasic is am schnellsten, www lädt eine hochauflösende Vorschau und touch dient als zusätzliches Backup</string>
|
||||
<string name="error_clipboard_empty">Fehler: Zwischenablage leer</string>
|
||||
<string name="error_scraping">Fehler: Veranstaltungsdaten nicht gefunden</string>
|
||||
<string name="error_url">Fehler: URL ungültig</string>
|
||||
<string name="error_connection">Fehler: Keine Verbindung möglich</string>
|
||||
<string name="error_unknown">Fehler: Unbekannter Fehler</string>
|
||||
<string name="preferences_events_header">Veranstaltungen</string>
|
||||
<string name="preferences_event_setting">Veranstaltungsliste löschen</string>
|
||||
<string name="preferences_event_snackbar">"Veranstaltungen gelöscht "</string>
|
||||
<string name="done">Fertig</string>
|
||||
<string name="undo">Rückgängig</string>
|
||||
<string name="preferences_page_event_max_summary">Maximale Anzahl Events, die von einer einzelnen Seite geladen werden sollen.</string>
|
||||
<string name="preferences_page_event_max">Veranstaltungslimit für Seiten</string>
|
||||
<string name="error_no_events">Fehler: keine bevorstehenden Veranstaltungen</string>
|
||||
</resources>
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">NoFb Event Scraper</string>
|
||||
<string name="action_about">Acerca de</string>
|
||||
<string name="action_help">Ayuda</string>
|
||||
<string name="action_settings">Ajustes</string>
|
||||
<string name="hint_add_link">Enlace del evento</string>
|
||||
<string name="helper_add_link">Pegue el enlace del evento en Facebook</string>
|
||||
<string name="button_add">Añadir al calendario</string>
|
||||
<string name="tooltip_paste">Pegue el contenido del portapapeles en el cuadro de entrada de la URL</string>
|
||||
<string name="preferences_url_setting">Prefijo de URL que debe utilizarse</string>
|
||||
<string name="preferences_url_setting_summary">mbasic es el más rápida, www carga la vista previa de la imagen en alta resolución, touch es una herramienta de respaldo</string>
|
||||
<string name="error_clipboard_empty">Error: Portapapeles vacío</string>
|
||||
<string name="error_scraping">Error: No se han encontrado los datos del evento</string>
|
||||
<string name="error_url">Error: URL no válida</string>
|
||||
<string name="error_connection">Error: No es posible la conexión</string>
|
||||
<string name="error_unknown">Error: Error desconocido</string>
|
||||
<string name="preferences_events_header">Eventos</string>
|
||||
<string name="preferences_event_setting">Limpiar lista de eventos</string>
|
||||
<string name="preferences_event_snackbar">"Lista de eventos limpiada "</string>
|
||||
<string name="done">Aceptar</string>
|
||||
<string name="undo">Deshacer</string>
|
||||
<string name="preferences_page_event_max_summary">Número máximo de eventos a cargar en una sola página.</string>
|
||||
<string name="preferences_page_event_max">Límite de eventos por página</string>
|
||||
<string name="error_no_events">Error: no hay próximos eventos</string>
|
||||
</resources>
|
|
@ -1,12 +1,14 @@
|
|||
<resources>
|
||||
<!-- Reply Preference -->
|
||||
<string-array name="url_to_scrape">
|
||||
<item>m.facebook.com</item>
|
||||
<item>mbasic.facebook.com</item>
|
||||
<item>touch.facebook.com</item>
|
||||
<item>www.facebook.com</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="url_prefix">
|
||||
<item>https://m.</item>
|
||||
<item>https://mbasic.</item>
|
||||
<item>https://touch.</item>
|
||||
<item>https://www.</item>
|
||||
</string-array>
|
||||
|
||||
|
|
|
@ -1,19 +1,42 @@
|
|||
<resources>
|
||||
<string name="app_name">NoFb Event Scraper</string>
|
||||
|
||||
<!-- Action names -->
|
||||
<string name="action_about">About</string>
|
||||
<string name="action_help">Help</string>
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="add_link_hint">Event link</string>
|
||||
<string name="add_link_helper">Paste facebook link to the event.</string>
|
||||
<string name="add_to_calendar">Add to calendar</string>
|
||||
<string name="paste_button">Paste from clipboard</string>
|
||||
|
||||
<!-- Preference Titles -->
|
||||
<string name="scraper_header">Scraper</string>
|
||||
<!-- Input hints -->
|
||||
<string name="hint_add_link">Event link</string>
|
||||
<string name="helper_add_link">Paste Facebook link to the event</string>
|
||||
<string name="button_add">Add to calendar</string>
|
||||
<string name="tooltip_paste">Paste content from clipboard into the URL input box</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_clipboard_empty">Error: clipboard empty</string>
|
||||
<string name="error_scraping">Error: Scraping event data failed</string>
|
||||
<string name="error_url">Error: URL invalid</string>
|
||||
<string name="error_connection">Error: Unable to connect</string>
|
||||
<string name="error_unknown">Error: Unknown Error</string>
|
||||
<string name="error_no_events">Error: No upcoming events</string>
|
||||
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="preferences_scraper_header" translatable="false">Scraper</string>
|
||||
<string name="preferences_url_setting">Which URL prefix to use</string>
|
||||
<string name="preferences_url_setting_summary">"mbasic is the fastest, www loads high-res image preview, touch is an additional backup."</string>
|
||||
|
||||
<string name="preferences_events_header">Events</string>
|
||||
<string name="preferences_event_setting">Clear event list</string>
|
||||
<string name="preferences_event_snackbar">Events list cleared</string>
|
||||
|
||||
<string name="preferences_page_event_max_summary">Maximum amount of events scraped from a single page link.</string>
|
||||
<string name="preferences_page_event_max">Page event limit</string>
|
||||
|
||||
<!-- others -->
|
||||
<string name="event_placeholder" translatable="false">Placeholder</string>
|
||||
<string name="done">Done</string>
|
||||
<string name="undo">Undo</string>
|
||||
|
||||
<string name="url_setting">Which URL to scrape</string>
|
||||
<string name="url_setting_summary">
|
||||
Using m.facebook.com is more stable and faster. Using www.facebook.com gives high-res images and works better with mutiple instance events but will eventually break when facebook disables the classic design.
|
||||
</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
</style>
|
||||
|
||||
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar" />
|
||||
|
||||
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.MaterialComponents.Light" />
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -1,21 +1,36 @@
|
|||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<PreferenceCategory app:title="@string/scraper_header">
|
||||
<PreferenceCategory app:title="@string/preferences_scraper_header">
|
||||
|
||||
|
||||
<ListPreference
|
||||
app:defaultValue="m.facebook.com"
|
||||
android:summary="@string/preferences_url_setting_summary"
|
||||
app:defaultValue="https://mbasic."
|
||||
app:entries="@array/url_to_scrape"
|
||||
app:entryValues="@array/url_prefix"
|
||||
app:key="url_preference"
|
||||
app:title="@string/url_setting"
|
||||
android:summary="@string/url_setting_summary"/>
|
||||
app:title="@string/preferences_url_setting" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory app:title="@string/preferences_events_header">
|
||||
|
||||
|
||||
<SeekBarPreference
|
||||
android:defaultValue="5"
|
||||
app:showSeekBarValue="true"
|
||||
app:min="1"
|
||||
android:max="100"
|
||||
android:summary="@string/preferences_page_event_max_summary"
|
||||
android:key="page_event_max"
|
||||
android:title="@string/preferences_page_event_max" />
|
||||
|
||||
<Preference
|
||||
android:key="event_reset"
|
||||
android:title="@string/preferences_event_setting" />
|
||||
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
|
||||
|
||||
</PreferenceScreen>
|
|
@ -4,11 +4,10 @@ buildscript {
|
|||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
@ -18,8 +17,7 @@ buildscript {
|
|||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
|
||||
changelog_location="fastlane/metadata/android/en-US/changelogs"
|
||||
count_txt=$(ls -r $changelog_location/*.txt | wc -l)
|
||||
count_tag=$(git tag | wc -l)
|
||||
|
||||
if (( count_txt != count_tag )); then
|
||||
echo "Tag count and txt-file count not matching."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "# Changelog" > CHANGELOG.md
|
||||
|
||||
for i in $(seq "$count_txt" -1 1)
|
||||
do
|
||||
tag=$(git tag | sed "$i!d")
|
||||
echo "## $tag ($i)" >> CHANGELOG.md
|
||||
|
||||
awk 1 $changelog_location/"$i".txt >> CHANGELOG.md
|
||||
|
||||
done
|
|
@ -0,0 +1,12 @@
|
|||
Der Zweck dieser Anwendung ist es, Zugang zu Facebook-Veranstaltungen ohne ein Konto zu erhalten.
|
||||
Daher verwendet sie nicht die Facebook-API.
|
||||
Stattdessen öffnet sie die URI der Facebook-Veranstaltung und lädt den HTML-Code der Website herunter.
|
||||
Dieser Code sollte die Veranstaltungsinformationen in Form von strukturierten Daten enthalten.
|
||||
Die Daten werden extrahiert und zur Erstellung von Android-Terminen verwendet.
|
||||
|
||||
Features:
|
||||
* Verwendet keine Facebook-API
|
||||
* Unterstützt "öffnen-mit" und "teilen mit".
|
||||
* Unabhängig von regionalen Subdomain-URLs von Facebook
|
||||
* Speichert den Verlauf der eingegebenen Veranstaltungen
|
||||
* Unterstützt bevorstehende Veranstaltungen von Seiten
|
|
@ -0,0 +1 @@
|
|||
Facebook-Veranstaltungen zum Kalender hinzufügen
|
|
@ -1 +1 @@
|
|||
Initial release.
|
||||
- Initial release.
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
- Support pages with upcoming events *beta*
|
||||
- Display events in a scrollable card-based view
|
||||
- Improve intent handling
|
||||
- Add history for scraped events
|
||||
- Tap image preview to open fullscreen
|
||||
- Scrape name and image even if no event data found
|
|
@ -0,0 +1,3 @@
|
|||
- Fix events not displaying correctly after activity resume
|
||||
- add share action on each event
|
||||
- add URL shortener redirection for fb.me
|
|
@ -0,0 +1,2 @@
|
|||
- Fix scraping not working when cookies need to be accepted
|
||||
- Android 11 ready
|
|
@ -0,0 +1,2 @@
|
|||
- Add spanish translation thanks to @sguinetti
|
||||
- update dependencies
|
|
@ -0,0 +1 @@
|
|||
- Fix Android 11 intents (to open Calendar or Maps)
|
|
@ -0,0 +1,4 @@
|
|||
- parallelize multiple event scraping
|
||||
- add scraping history
|
||||
- allow entering page name without URL format
|
||||
- updated dependencies
|
|
@ -1,3 +1,3 @@
|
|||
Added help and reports section.
|
||||
Fix for some locations not scraped correctly.
|
||||
Resolve fb-internal links.
|
||||
- Added help and reports section.
|
||||
- Fix for some locations not scraped correctly.
|
||||
- Resolve fb-internal links.
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
Fix for timezone calculation.
|
||||
Material design Interface, will be improved further.
|
||||
Display the event picture in the toolbar.
|
||||
- Fix for timezone calculation.
|
||||
- Material design Interface, will be improved further.
|
||||
- Display the event picture in the toolbar.
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
Fixes for new design.
|
||||
New button layout.
|
||||
Improve input error display.
|
||||
- Fixes for new design.
|
||||
- New button layout.
|
||||
- Improve input error display.
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
Fix for event links with querystrings.
|
||||
New icon and banner as default toolbar image.
|
||||
- Fix for event links with querystrings.
|
||||
- New icon and banner as default toolbar image.
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
Rework help and about.
|
||||
Add settings with URL prefix selector.
|
||||
Add Maps button for location.
|
||||
Disable text field inputs - all fields can be edited in the calendar app.
|
||||
Change date/time format to RFC standard.
|
||||
Minor interface changes.
|
||||
Code cleanup and refactoring.
|
||||
Add code documentation.
|
||||
- Rework help and about.
|
||||
- Add settings with URL prefix selector.
|
||||
- Add Maps button for location.
|
||||
- Disable text field inputs - all fields can be edited in the calendar app.
|
||||
- Change date/time format to RFC standard.
|
||||
- Minor interface changes.
|
||||
- Code cleanup and refactoring.
|
||||
- Add code documentation.
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
- Fix invalid default uri.
|
||||
- Update about section.
|
|
@ -0,0 +1,5 @@
|
|||
- Add german translation.
|
||||
- Decrease MinSDK to 23 (Android 6.0).
|
||||
- Replace Date implementation.
|
||||
- Enable Dark Mode for WebViews.
|
||||
- Add high-res preview for www prefix.
|
|
@ -0,0 +1,3 @@
|
|||
- Update about section with download and changelog information.
|
||||
- Improve high-res preview scraping.
|
||||
- Fix potential crash on calendar intent.
|
|
@ -1,4 +1,12 @@
|
|||
This application was developed to be used without a facebook account.
|
||||
Therefore it does not use the facebook API.
|
||||
Instead it opens the facebook event URI and downloads the website source code.
|
||||
This source contains the information which is used to create a calendar entry.
|
||||
The purpose of this application is to get access to Facebook events without an account.
|
||||
Therefore it does not use the Facebook API.
|
||||
Instead it opens the Facebook event URI and downloads the website HTML code.
|
||||
This source should contain the event information in form of structured data.
|
||||
That data is extracted and used to create Android events.
|
||||
|
||||
Features:
|
||||
* Does not use Facebook API
|
||||
* Supports "open-with" and "share-to"
|
||||
* Independent from Facebook regional sub-domain URLs
|
||||
* Saves history of scraped events
|
||||
* Handles upcoming events from pages
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 304 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 290 KiB |
Before Width: | Height: | Size: 484 KiB After Width: | Height: | Size: 321 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 85 KiB |
After Width: | Height: | Size: 185 KiB |
|
@ -1 +1 @@
|
|||
This app scrapes facebook event links and adds the event to your calendar.
|
||||
Import Facebook-Events to the calendar
|
|
@ -0,0 +1,12 @@
|
|||
El propósito de esta aplicación es obtener acceso a los eventos de Facebook sin necesidad de una cuenta.
|
||||
Por lo tanto, no recurre a la API de Facebook.
|
||||
Como alternativa, abre el URI del evento de Facebook y descarga el código HTML del sitio web.
|
||||
Esta fuente debe contener la información del evento en forma de datos estructurados.
|
||||
Esos datos al extraerse se crean eventos en Android.
|
||||
|
||||
Características:
|
||||
* No recurre a la API de Facebook
|
||||
* Soporta "abrir con" y "compartir via"
|
||||
* Funciona con todas las URLs de los subdominios regionales de Facebook
|
||||
* Guarda el historial de eventos extraídos
|
||||
* Maneja los próximos eventos de las páginas
|
|
@ -0,0 +1 @@
|
|||
Importa los eventos de Facebook al calendario
|
|
@ -3,5 +3,5 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
|
||||
distributionSha256Sum=23e7d37e9bb4f8dabb8a3ea7fdee9dd0428b9b1a71d298aefd65b11dccea220f
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
|
||||
distributionSha256Sum=bf8b869948901d422e9bb7d1fa61da6a6e19411baa7ad6ee929073df85d6365d
|
||||
|
|