1
0
mirror of https://github.com/akaessens/NoFbEventScraper synced 2025-06-05 23:29:13 +02:00

11 Commits

Author SHA1 Message Date
268a6bc650 prepare v0.3.0 2020-08-28 21:37:09 +02:00
f9a711300e use activity names as titles 2020-08-28 21:24:23 +02:00
793a9e3549 prevent nullpointer exception on tests 2020-08-28 21:13:21 +02:00
ec62cb6347 switch to webview display in help and about
add icons to overflow menu
add settings with scrape url prefix selection
rewrite help and about
2020-08-28 20:47:52 +02:00
023b7f951a use rfc time format instead of local 2020-08-28 17:52:36 +02:00
4e319d3807 add javadoc 2020-08-28 17:32:39 +02:00
41eac31dba remove unneeded try catch error handling 2020-08-28 16:59:11 +02:00
6c0c2d23fe make fbevent properties immutable 2020-08-28 16:19:08 +02:00
16d390094e much refactoring:
-move event formatting logic to event class
-disable editing of event output, it's available in the calendar app
-replace string datetimes with ZonedDateZime
-move uri checking logic to scraper
-update exception handling and error messages
-reformatting and renaming
-fix messy xml layouts
-update tests
-add comments
2020-08-28 16:14:03 +02:00
05f3ba9a33 use regex find instead of replace for url matching 2020-08-26 18:47:44 +02:00
57b00e2e57 add maps intent
not gmaps exclusive
closes #9
2020-08-26 17:57:51 +02:00
30 changed files with 761 additions and 745 deletions

View File

@ -8,8 +8,8 @@ android {
applicationId "com.akdev.nofbeventscraper"
minSdkVersion 26
targetSdkVersion 29
versionCode 5
versionName "0.2.2"
versionCode 6
versionName "0.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@ -38,8 +38,9 @@ dependencies {
implementation 'org.jsoup:jsoup:1.13.1'
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'androidx.preference:preference:1.1.1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

View File

@ -1,110 +0,0 @@
package com.akdev.nofbeventscraper;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class MainActivityUnitTest {
@Test
public void TestSubdomainUrl() {
Instrumentation mInstrumentation = getInstrumentation();
// We register our interest in the activity
Instrumentation.ActivityMonitor monitor = mInstrumentation.addMonitor(MainActivity.class.getName(), null, false);
// We launch it
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setClassName(mInstrumentation.getTargetContext(), MainActivity.class.getName());
mInstrumentation.startActivitySync(intent);
MainActivity mainActivity = (MainActivity) getInstrumentation().waitForMonitor(monitor);
// We register our interest in the next activity from the sequence in this use case
mInstrumentation.removeMonitor(monitor);
final String exp = "https://m.facebook.com/events/261145401687844";
String url = "https://www.facebook.com/events/261145401687844";
String act = mainActivity.checkURI(url);
assertEquals(exp, act);
url = "https://de-de.facebook.com/events/261145401687844";
act = mainActivity.checkURI(url);
assertEquals(exp, act);
url = "https://m.facebook.com/events/261145401687844";
act = mainActivity.checkURI(url);
assertEquals(exp, act);
url = "https://www.facebook.com/events/261145401687844/?active_tab=discussion";
act = mainActivity.checkURI(url);
assertEquals(exp, act);
url = "https://www.facebook.com/events/261145401687844?reflink_something";
act = mainActivity.checkURI(url);
assertEquals(exp, act);
}
@Test
public void TestTimeToEpoch() {
Instrumentation mInstrumentation = getInstrumentation();
// We register our interest in the activity
Instrumentation.ActivityMonitor monitor = mInstrumentation.addMonitor(MainActivity.class.getName(), null, false);
// We launch it
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setClassName(mInstrumentation.getTargetContext(), MainActivity.class.getName());
mInstrumentation.startActivitySync(intent);
MainActivity mainActivity = (MainActivity) getInstrumentation().waitForMonitor(monitor);
// We register our interest in the next activity from the sequence in this use case
mInstrumentation.removeMonitor(monitor);
String in = "2020-07-29T12:00:00+00:00";
Long exp = new Long(1596024000);
exp = exp* 1000;
Long act = mainActivity.convertTimeToEpoch(in);
assertEquals(exp, act);
in = "2020-07-29T12:00:00+02:00";
exp = new Long(1596016800);
exp = exp* 1000;
act = mainActivity.convertTimeToEpoch(in);
assertEquals(exp, act);
in = "1970-01-01T00:00:00+00:00";
exp = new Long(0);
exp = exp* 1000;
act = mainActivity.convertTimeToEpoch(in);
assertEquals(exp, act);
in = "1970-01-01T02:00:00+02:00";
exp = new Long(0);
exp = exp* 1000;
act = mainActivity.convertTimeToEpoch(in);
assertEquals(exp, act);
}
}

View File

@ -5,13 +5,19 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class ScraperUnitTest {
@Test
public void TestLocation() {
public void testLocation() {
FbScraper scraper = new FbScraper(null, "");
@ -32,37 +38,50 @@ public class ScraperUnitTest {
}
@Test
public void TestTimezone() {
public void testTimezone() {
FbScraper scraper = new FbScraper(null, "");
String exp = "2020-10-23T05:00:00+02:00";
String exp = "2020-10-23T05:00+02:00";
String in = "2020-10-23T05:00:00+0200";
String act = scraper.fixTimezone(in);
String act = scraper.toZonedDateTime(in).toString();
assertEquals(exp, act);
exp = "";
exp = null;
in = "";
act = scraper.fixTimezone(in);
assertEquals(exp, act);
ZonedDateTime act2 = scraper.toZonedDateTime(in);
assertEquals(exp, act2);
}
@Test
public void TestLinks() {
public void testDescriptionLinks() {
FbScraper scraper = new FbScraper(null, "");
String in = "foo @[152580919265:274:MagentaMusik 360] bar";
String exp = "foo MagentaMusik 360 [m.facebook.com/152580919265] bar";
String act = scraper.fixLinks(in);
String act = scraper.fixDescriptionLinks(in);
assertEquals(exp, act);
in = "foo @[152580919265:274:MagentaMusik 360] bar @[666666666666:274:NoOfTheBeast]";
exp = "foo MagentaMusik 360 [m.facebook.com/152580919265] bar NoOfTheBeast [m.facebook.com/666666666666]";
act = scraper.fixLinks(in);
act = scraper.fixDescriptionLinks(in);
assertEquals(exp, act);
}
@Test
public void testURI() throws MalformedURLException, URISyntaxException {
FbScraper scraper = new FbScraper(null, "");
String in = "https://www.facebook.com/events/1234324522341432?refsomething";
String exp = "https://m.facebook.com/events/1234324522341432";
String act = scraper.fixURI(in);
assertEquals(exp, act);
in = "https://de-de.facebook.com/events/1234324522341432/?active_tab=discussion";
act = scraper.fixURI(in);
assertEquals(exp, act);
}
}

View File

@ -11,8 +11,25 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".AboutActivity"></activity>
<activity android:name=".HelpActivity"></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"
android:label="@string/action_help" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity"/>
</activity>
<activity android:name=".AboutActivity"
android:label="@string/action_about" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity"/>
</activity>
<activity
android:name=".MainActivity"
android:label="@string/app_name"

View File

@ -0,0 +1,9 @@
<!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>

View File

@ -0,0 +1,14 @@
<!doctype html>
<h3>What links can be used with this app?</h3>
<p>All facebook subdomains are supported, whether mobile (m.facebook.com) or language-specific (de-de.facebook.com). The link must contain an event ID.</p>
<h3>How to use this application?</h3>
<ul>
<li><strong>Paste button</strong>: paste a copied link from the clipboard into the URL bar.</li>
<li><strong>Share to</strong>: Android's built-in share-function, e.g. from a browser.</li>
<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>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&amp;hl=de">Etar Calendar</a> because it is Open Source.</p>

View File

@ -1,5 +1,6 @@
package com.akdev.nofbeventscraper;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
@ -7,6 +8,7 @@ import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.webkit.WebView;
import android.widget.ImageView;
public class AboutActivity extends AppCompatActivity {
@ -15,19 +17,14 @@ public class AboutActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
this.getSupportActionBar().hide();
ActionBar action_bar = getSupportActionBar();
if (action_bar != null) {
action_bar.setDisplayHomeAsUpEnabled(true);
}
ImageView img = (ImageView)findViewById(R.id.paypal_image);
WebView webview_about = findViewById(R.id.webview_about);
img.setOnClickListener(new View.OnClickListener(){
public void onClick(View v){
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setData(Uri.parse("https://www.paypal.me/andreaskaessens"));
startActivity(intent);
}
});
webview_about.loadUrl("file:////android_asset/about.html");
}

View File

@ -1,14 +1,27 @@
package com.akdev.nofbeventscraper;
public class FbEvent {
public String name;
public String start_date;
public String end_date;
public String description;
public String location;
public String image_url;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
public FbEvent (String name, String start_date, String end_date, String description, String location, String image_url) {
/**
* Objects of this class store immutable information about
* a single event that was created by the FbScraper.
*/
public class FbEvent {
public final String url;
public final String name;
public final ZonedDateTime start_date;
public final ZonedDateTime end_date;
public final String description;
public final String location;
public final String image_url;
public FbEvent(String url, String name, ZonedDateTime start_date, ZonedDateTime end_date,
String description, String location, String image_url) {
this.url = url;
this.name = name;
this.start_date = start_date;
this.end_date = end_date;
@ -16,4 +29,34 @@ public class FbEvent {
this.location = location;
this.image_url = image_url;
}
/**
* Converts datetime to epoch.
*
* @param zoned_date_time ZonedDateTime object
* @return Event begin time in milliseconds from the epoch for calendar intent or null
*/
static Long dateTimeToEpoch(ZonedDateTime zoned_date_time) {
try {
return zoned_date_time.toEpochSecond() * 1000;
} catch (Exception e) {
return null;
}
}
/**
* Returns a RFC formatted String representation of a ZonedDateTime
*
* @param zoned_date_time
* @return RFC-1123 formatted String of zoned_date_time or empty String
*/
static String dateTimeToString(ZonedDateTime zoned_date_time) {
try {
return DateTimeFormatter
.RFC_1123_DATE_TIME
.format(zoned_date_time);
} catch (Exception e) {
return "";
}
}
}

View File

@ -1,80 +1,163 @@
package com.akdev.nofbeventscraper;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.text.Editable;
import android.text.SpannableStringBuilder;
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.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> {
private String url;
private String error;
private MainActivity main;
private String input_url;
private WeakReference<MainActivity> main; // no context leak with WeakReference
private FbEvent event;
FbScraper(MainActivity main, String url) {
this.url = url;
/**
* Constructor with WeakReference to the main activity, to update it's text fields.
*
* @param main WeakReference of main activity to prevent context leak
* @param input_url Input url to scrape from
*/
FbScraper(WeakReference<MainActivity> main, String input_url) {
this.main = main;
this.input_url = input_url;
}
/**
* Strips the facebook event link of the input 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 {
// check for url format
new URL(url).toURI();
String regex = "(facebook.com/events/[0-9]*)(/\\?event_time_id=[0-9]*)?";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(url);
if (matcher.find()) {
String url_prefix = "https://m.";
if (main != null){
SharedPreferences shared_prefs = PreferenceManager.getDefaultSharedPreferences(main.get());
url_prefix = shared_prefs.getString("url_preference", url_prefix);
}
// rewrite url to m.facebook and dismiss any query strings or referrals
String ret = url_prefix + matcher.group(1);
if (matcher.group(2) != null) {
ret += matcher.group(2);
}
return ret;
} else {
throw new URISyntaxException(url, "Does not contain event.");
}
}
/**
* 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 name = "";
String location_name = "";
try {
JSONObject reader = new JSONObject(location_json);
name = reader.getString("name");
location_name = reader.getString("name");
JSONObject address = reader.getJSONObject("address");
String type = address.getString("@type");
if (type.equals("PostalAddress"))
{
if (type.equals("PostalAddress")) {
String postal_code = address.getString("postalCode");
String address_locality = address.getString("addressLocality");
String address_country = address.getString("addressCountry");
String street_address = address.getString("streetAddress");
// included in locality
//String address_country = address.getString("addressCountry");
return name + ", " + street_address + ", " + postal_code + " " + address_locality;
return location_name + ", "
+ street_address + ", "
+ postal_code + " "
+ address_locality;
} else {
return location_name;
}
else
{
return name;
}
} catch (JSONException e) {
e.printStackTrace();
return name;
return location_name;
}
}
protected String fixTimezone(String time_in) {
/**
* 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
*/
protected ZonedDateTime toZonedDateTime(String time_in) {
try {
// time in is missing a : in the timezone offset
Editable editable = new SpannableStringBuilder(time_in);
String time_str = editable.insert(22, ":").toString();
return editable.insert(22, ":").toString();
// parse e.g. 2011-12-03T10:15:30+01:00
return ZonedDateTime.parse(time_str, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
} catch (Exception e) {
e.printStackTrace();
return "";
return null;
}
}
protected String fixLinks(String description_in) {
/**
* 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:MagentaMusik 360] -> m.facebook.com/152580919265
return description_in.replaceAll("@\\[([0-9]{10,}):[0-9]{3}:([^\\]]*)\\]",
/* @[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) {
@ -83,63 +166,61 @@ public class FbScraper extends AsyncTask<Void, Void, Void> {
}
}
/**
* 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) {
} 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) {
Document document = null;
try {
document = Jsoup.connect(url).userAgent("Mozilla").get();
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();
try {
String json = document.select("script[type = application/ld+json]").first().data();
JSONObject reader = new JSONObject(json);
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")
);
String event_name = readFromJson(reader, "name");
String event_start = fixTimezone(readFromJson(reader, "startDate"));
String event_end = fixTimezone(readFromJson(reader, "endDate"));
String event_description = fixLinks(readFromJson(reader, "description"));
String location = fixLocation(readFromJson(reader, "location"));
String image_url = "";
try {
image_url = readFromJson(reader, "image"); // get from json
// get from event header
image_url = document.getElementsByClass("scaledImageFitWidth").first().attr("src");
} catch (Exception e) {
e.printStackTrace();
this.error = "Error: no image found";
}
if (event_name == null) {
this.event = null;
throw new Exception();
} else {
this.event = new FbEvent(event_name, event_start, event_end, event_description, location, image_url);
//this.event = new FbEvent("", "", "", "", "", "");
}
} catch (Exception e) {
e.printStackTrace();
this.error = "Error: Scraping event data failed";
}
} catch (Exception e) {
} catch (URISyntaxException | MalformedURLException e) {
e.printStackTrace();
this.error = "Error: URL not available";
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.";
}
return null;
}
@ -148,15 +229,21 @@ public class FbScraper extends AsyncTask<Void, Void, Void> {
super.onPreExecute();
}
/**
* 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 (this.event != null) {
this.main.update(event);
}
else {
main.error(error);
this.main.clear(false);
if (main != null) {
if (this.event != null) {
main.get().update(event);
} else {
main.get().error(error);
main.get().clear(false);
}
}
}
}

View File

@ -1,7 +1,9 @@
package com.akdev.nofbeventscraper;
import android.os.Bundle;
import android.webkit.WebView;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
public class HelpActivity extends AppCompatActivity {
@ -10,6 +12,14 @@ public class HelpActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_help);
this.getSupportActionBar().hide();
ActionBar action_bar = getSupportActionBar();
if (action_bar != null) {
action_bar.setDisplayHomeAsUpEnabled(true);
}
WebView webview_help = findViewById(R.id.webview_help);
webview_help.loadUrl("file:////android_asset/help.html");
}
}

View File

@ -5,12 +5,6 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
import com.google.android.material.textfield.TextInputEditText;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.provider.CalendarContract;
import android.view.KeyEvent;
import android.view.Menu;
@ -19,30 +13,41 @@ import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.menu.MenuBuilder;
import androidx.appcompat.widget.Toolbar;
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.textfield.TextInputLayout;
import com.squareup.picasso.Picasso;
import java.net.URL;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.lang.ref.WeakReference;
import java.util.Objects;
import static com.akdev.nofbeventscraper.FbEvent.dateTimeToEpoch;
public class MainActivity extends AppCompatActivity {
private Button paste_button;
private Button ok_button;
protected Button ok_button;
protected Button paste_button;
private TextInputEditText field_uri_input;
private TextInputEditText field_event_name;
private TextInputEditText field_event_start;
private TextInputEditText field_event_end;
private TextInputEditText field_event_location;
private TextInputEditText field_event_description;
private ImageView toolbar_image_view;
private CollapsingToolbarLayout toolbar_layout;
private TextInputLayout input_layout;
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;
private FbScraper scraper;
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;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -54,41 +59,52 @@ public class MainActivity extends AppCompatActivity {
ok_button = (Button) findViewById(R.id.ok_button);
paste_button = (Button) findViewById(R.id.paste_button);
field_uri_input = (TextInputEditText) findViewById(R.id.field_uri_input);
input_layout = (TextInputLayout) findViewById(R.id.textInputLayout);
field_event_name = (TextInputEditText) findViewById(R.id.field_event_name);
field_event_start = (TextInputEditText) findViewById(R.id.field_event_start);
field_event_end = (TextInputEditText) findViewById(R.id.field_event_end);
field_event_location = (TextInputEditText) findViewById(R.id.field_event_location);
field_event_description = (TextInputEditText) findViewById(R.id.field_event_description);
toolbar_image_view = (ImageView) findViewById(R.id.image_view);
toolbar_layout = (CollapsingToolbarLayout) findViewById(R.id.toolbar_layout);
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);
/*
* Default view settings
*/
ok_button.setEnabled(false);
layout_event_location.setEndIconVisible(false);
image_view_toolbar.setImageResource(R.drawable.ic_banner_foreground);
toolbar_image_view.setImageResource(R.drawable.ic_banner_foreground);
/*
* Display title only when toolbar is collapsed
*/
AppBarLayout app_bar_layout = (AppBarLayout) findViewById(R.id.app_bar);
app_bar_layout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
boolean isShow = true;
int scrollRange = -1;
boolean show = true;
int scroll_range = -1;
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
if (scrollRange == -1) {
scrollRange = appBarLayout.getTotalScrollRange();
public void onOffsetChanged(AppBarLayout app_bar_layout, int vertical_offset) {
if (scroll_range == -1) {
scroll_range = app_bar_layout.getTotalScrollRange();
}
if (scrollRange + verticalOffset == 0) {
toolbar_layout.setTitle(getString(R.string.app_name));
isShow = true;
} else if(isShow) {
toolbar_layout.setTitle(" ");
isShow = false;
if (scroll_range + vertical_offset == 0) {
layout_toolbar.setTitle(getString(R.string.app_name));
show = true;
} else if (show) {
layout_toolbar.setTitle(" ");
show = false;
}
}
});
/*
* Paste button: get last entry from clipboard
*/
paste_button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
@ -98,9 +114,8 @@ public class MainActivity extends AppCompatActivity {
String str = clipboard.getPrimaryClip().getItemAt(0).getText().toString();
clear(true);
field_uri_input.setText(str);
}
catch (NullPointerException e) {
edit_text_uri_input.setText(str);
} catch (NullPointerException e) {
e.printStackTrace();
error("Error: Clipboard empty");
}
@ -108,54 +123,65 @@ public class MainActivity extends AppCompatActivity {
}
});
input_layout.setEndIconOnClickListener(new View.OnClickListener() {
/*
* Clear button: delete all text in all fields
*/
View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View view) {
clear(true);
}
});
input_layout.setErrorIconOnClickListener(new View.OnClickListener() {
};
layout_uri_input.setEndIconOnClickListener(listener);
layout_uri_input.setErrorIconOnClickListener(listener);
/*
* Maps button: launch maps intent
*/
layout_event_location.setEndIconOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
clear(true);
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);
}
}
});
/*
* Add to calendar button: launch calendar application
*/
ok_button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
Long start_epoch = convertTimeToEpoch(field_event_start.getText().toString());
Long end_epoch = convertTimeToEpoch(field_event_end.getText().toString());
String name = parseField(field_event_name);
Long start_epoch = dateTimeToEpoch(event.start_date);
Long end_epoch = dateTimeToEpoch(event.end_date);
String location = parseField(field_event_location);
String description = parseField(field_event_description);
String uri = parseField(field_uri_input);
Intent intent = new Intent(Intent.ACTION_EDIT);
intent.setType("vnd.android.cursor.item/event");
intent.putExtra(CalendarContract.Events.TITLE, 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, location);
intent.putExtra(CalendarContract.Events.DESCRIPTION, uri + "\n\n" + description);
startActivity(intent);
}
catch (Exception e )
{
e.printStackTrace();
}
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);
}
});
field_uri_input.setOnKeyListener(new View.OnKeyListener() {
public boolean onKey(View view, int keyCode, KeyEvent keyevent) {
//If the keyevent is a key-down event on the "enter" button
if ((keyevent.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
/*
* Enter button in uri input: start scraping
*/
edit_text_uri_input.setOnKeyListener(new View.OnKeyListener() {
public boolean onKey(View view, int keycode, KeyEvent keyevent) {
//If the key event is a key-down event on the "enter" button
if ((keyevent.getAction() == KeyEvent.ACTION_DOWN) && (keycode == KeyEvent.KEYCODE_ENTER)) {
startScraping();
return true;
}
@ -163,164 +189,130 @@ public class MainActivity extends AppCompatActivity {
}
});
//get data from deep link
/*
* 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
field_uri_input.setText(data.toString());
edit_text_uri_input.setText(data.toString());
startScraping();
}
else if (shared_text != null) {
} else if (shared_text != null) {
//share to nofb
field_uri_input.setText(shared_text);
edit_text_uri_input.setText(shared_text);
startScraping();
}
}
private String parseField(TextInputEditText field) {
try {
return field.getText().toString();
}
catch (Exception e) {
return null;
}
}
Long convertTimeToEpoch(String time_str) {
try {
ZonedDateTime datetime = ZonedDateTime.parse(time_str, DateTimeFormatter.ISO_DATE_TIME);
return datetime.toEpochSecond() * 1000;
} catch (Exception e)
{
e.printStackTrace();
}
return null;
}
String checkURI(String str)
{
try {
// check for a valid uri
new URL(str).toURI();
if (str.matches(".*(facebook.com/events/[0-9]*).*")) {
return str.replaceAll(".*(facebook.com/events/[0-9]*).*",
"https://m.$1");
}
else {
error("Error: Invalid URL");
clear(false);
return "";
}
}
catch (Exception e) {
e.printStackTrace();
error("Error: Invalid URL");
clear(false);
return "";
}
}
/**
* launch the FbScraper asynchronous task with the current text in the input text field.
*/
public void startScraping() {
error(null);
try {
String str = checkURI(field_uri_input.getText().toString());
if (!str.equals(""))
{
field_uri_input.setText(str);
scraper = new FbScraper(this, field_uri_input.getText().toString());
scraper.execute();
}
}
catch (Exception e) {
e.printStackTrace();
error("Error: Invalid URL");
}
String url = Objects.requireNonNull(edit_text_uri_input.getText()).toString();
scraper = new FbScraper(new WeakReference<>(this), url);
scraper.execute();
}
public void error(String str) {
//Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
input_layout.setError(str);
layout_uri_input.setError(str);
}
public void clear(boolean clearUri) {
if (clearUri) {
field_uri_input.setText("");
}
field_event_name.setText("");
field_event_name.setError(null);
field_event_start.setText("");
field_event_start.setError(null);
field_event_end.setText("");
field_event_end.setError(null);
field_event_location.setText("");
field_event_location.setError(null);
field_event_description.setText("");
field_event_description.setError(null);
toolbar_image_view.setImageDrawable(null);
if (scraper!=null)
{
/**
* 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("");
layout_uri_input.setError(null);
}
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;
}
ok_button.setEnabled(false);
toolbar_image_view.setImageResource(R.drawable.ic_banner_foreground);
}
public void update(FbEvent event) {
field_event_name.setText(event.name);
input_layout.setError(null);
if (event.name.equals(""))
{
field_event_name.setError("no event name detected");
}
field_event_start.setText(event.start_date);
if (event.start_date.equals(""))
{
field_event_start.setError("no event start date detected");
}
field_event_end.setText(event.end_date);
if (event.end_date.equals(""))
{
field_event_end.setError("no event end date detected");
}
field_event_location.setText(event.location);
if (event.location.equals(""))
{
field_event_location.setError("no event location detected");
}
field_event_description.setText(event.description);
if (event.description.equals(""))
{
field_event_description.setError("no event description detected");
}
try {
Picasso.get().load(event.image_url).into(toolbar_image_view);
} catch (Exception e)
{
} 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.
*
* @param scraped_event the event information that was scraped by FbScraper
*/
public void update(FbEvent scraped_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 (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);
}
@ -329,6 +321,12 @@ public class MainActivity extends AppCompatActivity {
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);
if(menu instanceof MenuBuilder){
MenuBuilder m = (MenuBuilder) menu;
//noinspection RestrictedApi
m.setOptionalIconsVisible(true);
}
return true;
}
@ -348,6 +346,10 @@ public class MainActivity extends AppCompatActivity {
startActivity(new Intent(this, HelpActivity.class));
return true;
}
if (id == R.id.action_settings) {
startActivity(new Intent(this, SettingsActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}

View File

@ -0,0 +1,31 @@
package com.akdev.nofbeventscraper;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceFragmentCompat;
public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.settings_activity);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings, new SettingsFragment())
.commit();
ActionBar action_bar = getSupportActionBar();
if (action_bar != null) {
action_bar.setDisplayHomeAsUpEnabled(true);
}
}
public static class SettingsFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.root_preferences, rootKey);
}
}
}

View File

@ -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="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>

View File

@ -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="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"
android:fillColor="#000000"/>
</vector>

View File

@ -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="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"
android:fillColor="#000000"/>
</vector>

View File

@ -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="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"
android:fillColor="#000000"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,97 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<androidx.constraintlayout.widget.ConstraintLayout
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"
android:fitsSystemWindows="true"
tools:context=".AboutActivity">
<ScrollView
<WebView
android:id="@+id/webview_about"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_margin="16dp">
<TextView
android:id="@+id/description_heading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/description_heading"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:id="@+id/description_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/description_text"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<TextView
android:id="@+id/open_source_heading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:text="@string/open_source_heading"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:id="@+id/open_source_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:text="@string/open_source_text"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<TextView
android:id="@+id/issues_heading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:text="@string/issues_heading"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:id="@+id/issues_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:text="@string/issues_text"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<TextView
android:id="@+id/donate_heading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:text="@string/donate_heading"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:id="@+id/changelog_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:text="@string/donate_text"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<ImageView
android:id="@+id/paypal_image"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:contentDescription="PayPal Donate"
android:scaleType="fitStart"
android:src="@drawable/paypal" />
</LinearLayout>
</ScrollView>
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,76 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<androidx.constraintlayout.widget.ConstraintLayout
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">
<ScrollView
<WebView
android:id="@+id/webview_help"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_margin="16dp">
<TextView
android:id="@+id/faq_heading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/faq_heading"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
<TextView
android:id="@+id/faq_0q"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:layout_marginBottom="4dp"
android:text="@string/faq_0q"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<TextView
android:id="@+id/faq_0a"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/faq_0a"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
<TextView
android:id="@+id/faq_1q"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp"
android:text="@string/faq_1q"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<TextView
android:id="@+id/faq_1a"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/faq_1a"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
<TextView
android:id="@+id/faq_2q"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp"
android:text="@string/faq_2q"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<TextView
android:id="@+id/faq_2a"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/faq_2a"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
</LinearLayout>
</ScrollView>
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -3,6 +3,7 @@
<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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
@ -16,7 +17,7 @@
android:theme="@style/AppTheme.AppBarOverlay">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:id="@+id/layout_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
@ -63,7 +64,7 @@
<include layout="@layout/content_main" />
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottomButtonLayout"
android:id="@+id/bottom_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"

View File

@ -1,59 +1,50 @@
<?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"
android:id="@+id/nested_scroll_view" >
tools:showIn="@layout/activity_main">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraint_layout"
<LinearLayout
android:id="@+id/linear_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="32dp"
android:layout_marginBottom="64dp"
<LinearLayout
android:id="@+id/linearLayout4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:divider="@drawable/divider"
android:orientation="vertical"
android:showDividers="middle">
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:id="@+id/layout_uri_input"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="16dp"
app:endIconCheckable="false"
app:endIconDrawable="@drawable/ic_backspace_black"
app:endIconMode="custom"
app:errorIconDrawable="@drawable/ic_backspace_black"
app:endIconMode="custom"
app:helperText="@string/add_link_helper"
app:helperTextEnabled="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearLayout4">
app:helperTextEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/field_uri_input"
android:id="@+id/edit_text_uri_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:clickable="true"
android:cursorVisible="true"
android:hint="@string/add_link_hint"
android:inputType="textNoSuggestions"
@ -61,123 +52,94 @@
android:textColorLink="@color/material_on_background_emphasis_high_type" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:id="@+id/scrollView"
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toTopOf="@+id/spacer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout">
android:layout_height="match_parent">
<LinearLayout
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_text_event_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@drawable/divider"
android:orientation="vertical"
android:showDividers="middle">
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.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/field_event_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:cursorVisible="true"
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.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.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/field_event_start"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:cursorVisible="true"
android:inputType="textNoSuggestions"
android:hint="Event start"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<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
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/field_event_end"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:cursorVisible="true"
android:inputType="textNoSuggestions"
android:hint="Event end"
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:endIconDrawable="@drawable/ic_add_location"
app:endIconMode="custom">
<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_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.TextInputEditText
android:id="@+id/field_event_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="map"
android:clickable="true"
android:cursorVisible="true"
android:inputType="textNoSuggestions"
android:hint="Event location"
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.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>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/field_event_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:clickable="true"
android:cursorVisible="true"
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>
</LinearLayout>
</LinearLayout>
<Space
android:id="@+id/spacer"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="@id/constraint_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -0,0 +1,9 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/settings"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -4,12 +4,17 @@
tools:context="com.akdev.nofbeventscraper.MainActivity">
<item
android:id="@+id/action_about"
android:icon="@android:drawable/ic_menu_info_details"
android:icon="@drawable/ic_info"
android:orderInCategory="100"
android:title="@string/action_about" />
<item
android:id="@+id/action_help"
android:icon="@android:drawable/ic_menu_info_details"
android:icon="@drawable/ic_help"
android:orderInCategory="100"
android:title="@string/action_help" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings"
android:orderInCategory="100"
android:title="@string/action_settings" />
</menu>

View File

@ -0,0 +1,13 @@
<resources>
<!-- Reply Preference -->
<string-array name="url_to_scrape">
<item>m.facebook.com</item>
<item>www.facebook.com</item>
</string-array>
<string-array name="url_prefix">
<item>https://m.</item>
<item>https://www.</item>
</string-array>
</resources>

View File

@ -2,34 +2,18 @@
<string name="app_name">NoFb Event Scraper</string>
<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>
<string name="description_heading">Description</string>
<string name="description_text">This application was developed to be used without a facebook account.
\nTherefore 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.
<!-- Preference Titles -->
<string name="scraper_header">Scraper</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>
<string name="open_source_heading"> Open Source </string>
<string name="open_source_text"> The source code for this application can be found at https://github.com/akaessens/NoFbEventScraper .</string>
<string name="issues_heading" > Report a bug</string>
<string name="issues_text" >If you encounter a bug please report it to me anonymously at https://gitreports.com/issue/akaessens/NoFbEventScraper or directly at Github with the link above.</string>
<string name="faq_heading"> FAQ </string>
<string name="faq_0q"> What links can be used with this app? </string>
<string name="faq_0a"> All facebook subdomains are supported, whether mobile (m.facebook.com) or language-specific (de-de.facebook.com). The link must contain an event ID.</string>
<string name="faq_1q"> How to use this application? </string>
<string name="faq_1a"> You can use the paste button to paste a previously copied link into the URL bar.
Alternatively you can use the "share" function e.g. from the Android browser.
A third option is to click on a facebook link and use the "open with"-dialog. </string>
<string name="faq_2q"> Why does event X not work? </string>
<string name="faq_2a"> Current limitations are events in the past or events with multiple instances. If you find some other event that does not work, please let me know.</string>
<string name="donate_heading"> Donations </string>
<string name="donate_text">Click on the PayPal image to send me a coffee if you like :) </string>
</resources>

View File

@ -0,0 +1,21 @@
<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">
<ListPreference
app:defaultValue="m.facebook.com"
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"/>
</PreferenceCategory>
</PreferenceScreen>

View File

@ -0,0 +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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 484 KiB