Compare commits
10 commits
01182a0eee
...
9961a31936
| Author | SHA1 | Date | |
|---|---|---|---|
| 9961a31936 | |||
| 5e4560e89e | |||
| 3490cd6ed9 | |||
|
|
f810a0f645 | ||
|
|
5a277dd869 | ||
|
|
0df2b02ad5 | ||
|
|
4ed7f5d54a | ||
|
|
66586f83b9 | ||
|
|
5ab4301aca | ||
|
|
56a420bb7a |
5
.gitignore
vendored
|
|
@ -6,3 +6,8 @@
|
||||||
/captures
|
/captures
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.idea/*
|
.idea/*
|
||||||
|
|
||||||
|
|
||||||
|
# Added by cargo
|
||||||
|
|
||||||
|
/target
|
||||||
|
|
|
||||||
2068
Cargo.lock
generated
Normal file
11
Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "next-companion"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gtk = { package = "gtk4", version = "0.10" }
|
||||||
|
adw = { package = "libadwaita", version = "0.8", features = ["v1_6"] }
|
||||||
|
reqwest = { version = "0.12", features = ["blocking", "rustls-tls"], default-features = false }
|
||||||
|
serde_json = "1"
|
||||||
|
dirs = "5"
|
||||||
1
app/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
||||||
/build
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion 28
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.example.hochi.nextcompanion"
|
|
||||||
minSdkVersion 15
|
|
||||||
targetSdkVersion 28
|
|
||||||
versionCode 8
|
|
||||||
versionName "0.1.7.1"
|
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
|
||||||
}
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
||||||
implementation 'com.android.support:appcompat-v7:28.0.0'
|
|
||||||
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
|
||||||
implementation 'com.android.support:design:28.0.0'
|
|
||||||
testImplementation 'junit:junit:4.12'
|
|
||||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
|
||||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
|
||||||
}
|
|
||||||
21
app/proguard-rules.pro
vendored
|
|
@ -1,21 +0,0 @@
|
||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package com.example.hochi.nextcompanion;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.support.test.InstrumentationRegistry;
|
|
||||||
import android.support.test.runner.AndroidJUnit4;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instrumented test, which will execute on an Android device.
|
|
||||||
*
|
|
||||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class ExampleInstrumentedTest {
|
|
||||||
@Test
|
|
||||||
public void useAppContext() {
|
|
||||||
// Context of the app under test.
|
|
||||||
Context appContext = InstrumentationRegistry.getTargetContext();
|
|
||||||
|
|
||||||
assertEquals("com.example.hochi.nextcompanion", appContext.getPackageName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="com.example.hochi.nextcompanion">
|
|
||||||
|
|
||||||
<!-- To auto-complete the email text field in the login form with the user's emails -->
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/AppTheme">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/AppTheme.NoActionBar">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".LoginActivity"
|
|
||||||
android:label="@string/title_activity_login" />
|
|
||||||
<activity android:name=".RentActivity"
|
|
||||||
android:label="@string/title_activity_rent">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value=".MainActivity" />
|
|
||||||
</activity>
|
|
||||||
<activity android:name=".ReturnActivity"
|
|
||||||
android:label="@string/title_activity_return">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
|
||||||
android:value=".MainActivity" />
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package com.example.hochi.nextcompanion;
|
|
||||||
|
|
||||||
interface AsyncTaskCallbacks<T> {
|
|
||||||
void onTaskComplete(T response);
|
|
||||||
}
|
|
||||||
|
|
@ -1,184 +0,0 @@
|
||||||
package com.example.hochi.nextcompanion;
|
|
||||||
|
|
||||||
import android.animation.Animator;
|
|
||||||
import android.animation.AnimatorListenerAdapter;
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.view.inputmethod.EditorInfo;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A login screen that offers login via phone number/pin.
|
|
||||||
*/
|
|
||||||
public class LoginActivity extends AppCompatActivity implements AsyncTaskCallbacks<String> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keep track of the login task to ensure we can cancel it if requested.
|
|
||||||
*/
|
|
||||||
private RequestHandler mAuthTask = null;
|
|
||||||
|
|
||||||
// UI references.
|
|
||||||
private TextView mPhoneView;
|
|
||||||
private EditText mPinView;
|
|
||||||
private View mProgressView;
|
|
||||||
private View mLoginFormView;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_login);
|
|
||||||
// Set up the login form.
|
|
||||||
mPhoneView = findViewById(R.id.phone);
|
|
||||||
|
|
||||||
mPinView = findViewById(R.id.pin);
|
|
||||||
mPinView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) {
|
|
||||||
if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) {
|
|
||||||
attemptLogin();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Button mPhoneSignInButton = findViewById(R.id.phone_sign_in_button);
|
|
||||||
mPhoneSignInButton.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
attemptLogin();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mLoginFormView = findViewById(R.id.login_form);
|
|
||||||
mProgressView = findViewById(R.id.login_progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to sign in or register the account specified by the login form.
|
|
||||||
* If there are form errors (invalid phone number, missing fields, etc.), the
|
|
||||||
* errors are presented and no actual login attempt is made.
|
|
||||||
*/
|
|
||||||
private void attemptLogin() {
|
|
||||||
if (mAuthTask != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset errors.
|
|
||||||
mPhoneView.setError(null);
|
|
||||||
mPinView.setError(null);
|
|
||||||
|
|
||||||
// Store values at the time of the login attempt.
|
|
||||||
String phone = mPhoneView.getText().toString();
|
|
||||||
String pin = mPinView.getText().toString();
|
|
||||||
String[] credentials = {
|
|
||||||
"apikey=", getString(R.string.apikey),
|
|
||||||
"mobile=", mPhoneView.getText().toString(),
|
|
||||||
"pin=", mPinView.getText().toString()
|
|
||||||
};
|
|
||||||
|
|
||||||
boolean cancel = false;
|
|
||||||
View focusView = null;
|
|
||||||
|
|
||||||
// Check for a valid pin, if the user entered one.
|
|
||||||
if (TextUtils.isEmpty(pin)) {
|
|
||||||
mPinView.setError(getString(R.string.error_field_required));
|
|
||||||
focusView = mPinView;
|
|
||||||
cancel = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for a valid phone address.
|
|
||||||
if (TextUtils.isEmpty(phone)) {
|
|
||||||
mPhoneView.setError(getString(R.string.error_field_required));
|
|
||||||
focusView = mPhoneView;
|
|
||||||
cancel = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancel) {
|
|
||||||
// There was an error; don't attempt login and focus the first
|
|
||||||
// form field with an error.
|
|
||||||
focusView.requestFocus();
|
|
||||||
} else {
|
|
||||||
// Show a progress spinner, and kick off a background task to
|
|
||||||
// perform the user login attempt.
|
|
||||||
showProgress(true);
|
|
||||||
mAuthTask = new RequestHandler(this, "POST",
|
|
||||||
"api/login.json", credentials);
|
|
||||||
mAuthTask.execute((Void) null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the progress UI and hides the login form.
|
|
||||||
*/
|
|
||||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
|
|
||||||
private void showProgress(final boolean show) {
|
|
||||||
// On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow
|
|
||||||
// for very easy animations. If available, use these APIs to fade-in
|
|
||||||
// the progress spinner.
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
|
|
||||||
int shortAnimTime = getResources().getInteger(android.R.integer.config_shortAnimTime);
|
|
||||||
|
|
||||||
mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE);
|
|
||||||
mLoginFormView.animate().setDuration(shortAnimTime).alpha(
|
|
||||||
show ? 0 : 1).setListener(new AnimatorListenerAdapter() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animation) {
|
|
||||||
mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mProgressView.setVisibility(show ? View.VISIBLE : View.GONE);
|
|
||||||
mProgressView.animate().setDuration(shortAnimTime).alpha(
|
|
||||||
show ? 1 : 0).setListener(new AnimatorListenerAdapter() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animation) {
|
|
||||||
mProgressView.setVisibility(show ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// The ViewPropertyAnimator APIs are not available, so simply show
|
|
||||||
// and hide the relevant UI components.
|
|
||||||
mProgressView.setVisibility(show ? View.VISIBLE : View.GONE);
|
|
||||||
mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTaskComplete(String response) {
|
|
||||||
//Callback called when RequestHandler finished request
|
|
||||||
if (!response.isEmpty()) {
|
|
||||||
try {
|
|
||||||
JSONObject jObject = new JSONObject(response);
|
|
||||||
JSONObject userObject = jObject.getJSONObject("user");
|
|
||||||
String loginkey = userObject.getString("loginkey");
|
|
||||||
SharedPreferences sharedPref = getSharedPreferences("persistence", MODE_PRIVATE);
|
|
||||||
SharedPreferences.Editor editor = sharedPref.edit();
|
|
||||||
editor.putString("loginKey", loginkey);
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
finish();
|
|
||||||
} else {
|
|
||||||
mPinView.setError(getString(R.string.error_incorrect_pin));
|
|
||||||
mPinView.requestFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
package com.example.hochi.nextcompanion;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.design.widget.FloatingActionButton;
|
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
import android.support.v7.widget.Toolbar;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity implements AsyncTaskCallbacks<String> {
|
|
||||||
private RequestHandler getBikesTask = null;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
//now this "every android activity" stuff
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_main);
|
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
final Context context = this;
|
|
||||||
|
|
||||||
//Floating Button
|
|
||||||
FloatingActionButton fab = findViewById(R.id.fab);
|
|
||||||
fab.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
Intent intent = new Intent(context, RentActivity.class);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
//pre-condition: Is there a login key?
|
|
||||||
SharedPreferences sharedPref = getSharedPreferences("persistence", MODE_PRIVATE);
|
|
||||||
String defaultValue = "nokey";
|
|
||||||
String loginKey = sharedPref.getString("loginKey", defaultValue);
|
|
||||||
//if not, go to LoginActivity
|
|
||||||
if (loginKey.equals("nokey")) {
|
|
||||||
Intent intent = new Intent(this, LoginActivity.class);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
reloadBikeList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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_logout) {
|
|
||||||
SharedPreferences sharedPref = getSharedPreferences("persistence", MODE_PRIVATE);
|
|
||||||
SharedPreferences.Editor editor = sharedPref.edit();
|
|
||||||
editor.remove("loginkey");
|
|
||||||
editor.apply();
|
|
||||||
Intent intent = new Intent(this, LoginActivity.class);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id == R.id.action_map) {
|
|
||||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.map_url)));
|
|
||||||
startActivity(browserIntent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void reloadBikeList() {
|
|
||||||
//get loginkey
|
|
||||||
SharedPreferences sharedPref = getSharedPreferences("persistence", MODE_PRIVATE);
|
|
||||||
String defaultValue = "nokey";
|
|
||||||
String loginKey = sharedPref.getString("loginKey", defaultValue);
|
|
||||||
|
|
||||||
String[] params = {
|
|
||||||
"apikey=", getString(R.string.apikey),
|
|
||||||
"loginkey=", loginKey
|
|
||||||
};
|
|
||||||
|
|
||||||
getBikesTask = new RequestHandler(this, "POST",
|
|
||||||
"api/getOpenRentals.json", params);
|
|
||||||
getBikesTask.execute((Void) null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTaskComplete(String response) {
|
|
||||||
//Callback called when RequestHandler finished request
|
|
||||||
final Context context = this;
|
|
||||||
if (!response.isEmpty()) {
|
|
||||||
final ArrayList<String> list = new ArrayList<>();
|
|
||||||
try {
|
|
||||||
JSONObject jObject = new JSONObject(response);
|
|
||||||
JSONArray bikesArray = jObject.getJSONArray("rentalCollection");
|
|
||||||
for (int i = 0; i < bikesArray.length(); i++) {
|
|
||||||
String entry;
|
|
||||||
JSONObject bike = bikesArray.getJSONObject(i);
|
|
||||||
entry = "Bike " + bike.getString("bike")
|
|
||||||
+ " with lock code " + bike.getString("code");
|
|
||||||
list.add(entry);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
//Create and fill list
|
|
||||||
final ListView listview = findViewById(R.id.listview);
|
|
||||||
final ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
|
||||||
android.R.layout.simple_list_item_1, list);
|
|
||||||
listview.setAdapter(adapter);
|
|
||||||
|
|
||||||
//Print indicator if empty
|
|
||||||
TextView tv = findViewById(R.id.noBikes);
|
|
||||||
if(list.isEmpty()) tv.setVisibility(View.VISIBLE);
|
|
||||||
else tv.setVisibility(View.INVISIBLE);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final JSONObject jObject = new JSONObject(response);
|
|
||||||
final JSONArray bikesArray = jObject.getJSONArray("rentalCollection");
|
|
||||||
listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onItemClick(AdapterView<?> parent, final View view, int position, long id) {
|
|
||||||
Intent intent = new Intent(context, ReturnActivity.class);
|
|
||||||
try {
|
|
||||||
JSONObject bike = bikesArray.getJSONObject(position);
|
|
||||||
String bID = bike.getString("bike");
|
|
||||||
String stID = bike.getString("start_place");
|
|
||||||
String lockE = bike.getString("electric_lock");
|
|
||||||
String[] bikeArray = {bID, stID, lockE};
|
|
||||||
intent.putExtra("bike", bikeArray);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
catch (JSONException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
} catch (JSONException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
//TODO: implement error handling
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
package com.example.hochi.nextcompanion;
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
public class RentActivity extends AppCompatActivity implements AsyncTaskCallbacks<String> {
|
|
||||||
private RequestHandler rentRequestTask = null;
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_rent);
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
|
||||||
Button mRentSubmitButton = findViewById(R.id.rent_submit_button);
|
|
||||||
mRentSubmitButton.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
rentRequest();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void rentRequest() {
|
|
||||||
//Prepare request to rent bike
|
|
||||||
TextView mBikeInput;
|
|
||||||
mBikeInput = findViewById(R.id.bike_id);
|
|
||||||
String bikeID = mBikeInput.getText().toString();
|
|
||||||
//get loginkey
|
|
||||||
SharedPreferences sharedPref = getSharedPreferences("persistence", MODE_PRIVATE);
|
|
||||||
String defaultValue = "nokey";
|
|
||||||
String loginKey = sharedPref.getString("loginKey", defaultValue);
|
|
||||||
|
|
||||||
String[] params = {
|
|
||||||
"apikey=", getString(R.string.apikey),
|
|
||||||
"loginkey=", loginKey,
|
|
||||||
"bike=", bikeID
|
|
||||||
};
|
|
||||||
|
|
||||||
rentRequestTask = new RequestHandler(this, "POST",
|
|
||||||
"api/rent.json", params);
|
|
||||||
rentRequestTask.execute((Void) null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTaskComplete(String response) {
|
|
||||||
//get back to main activity
|
|
||||||
//TODO: *any* response handling
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
package com.example.hochi.nextcompanion;
|
|
||||||
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.DataOutputStream;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
|
|
||||||
public class RequestHandler extends AsyncTask<Void, Void, String> {
|
|
||||||
|
|
||||||
private String mHTTPmethod;
|
|
||||||
private String mEndpoint;
|
|
||||||
private AsyncTaskCallbacks<String> callback;
|
|
||||||
private String[] mCredentials;
|
|
||||||
|
|
||||||
RequestHandler(AsyncTaskCallbacks<String> act, String HTTPmethod,
|
|
||||||
String endpoint, String[] credentials) {
|
|
||||||
mHTTPmethod = HTTPmethod;
|
|
||||||
mEndpoint = endpoint;
|
|
||||||
mCredentials = credentials;
|
|
||||||
callback = act;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String doInBackground(Void... params) {
|
|
||||||
StringBuilder response = new StringBuilder();
|
|
||||||
StringBuilder urlParameters = new StringBuilder();
|
|
||||||
int i=0;
|
|
||||||
while (i<mCredentials.length) {
|
|
||||||
urlParameters.append("&").append(mCredentials[i])
|
|
||||||
.append(URLEncoder.encode(mCredentials[i+1]));
|
|
||||||
i=i+2;
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpURLConnection connection = null;
|
|
||||||
try {
|
|
||||||
|
|
||||||
//Create connection
|
|
||||||
URL url = new URL("https://api.nextbike.net/" + mEndpoint);
|
|
||||||
connection = (HttpURLConnection) url.openConnection();
|
|
||||||
connection.setRequestMethod(mHTTPmethod);
|
|
||||||
if(mHTTPmethod.equals("POST")) {
|
|
||||||
connection.setRequestProperty("Content-Type",
|
|
||||||
"application/x-www-form-urlencoded");
|
|
||||||
|
|
||||||
connection.setRequestProperty("Content-Length", "" +
|
|
||||||
Integer.toString(urlParameters.toString().getBytes().length));
|
|
||||||
connection.setRequestProperty("Content-Language", "en-US");
|
|
||||||
|
|
||||||
connection.setUseCaches(false);
|
|
||||||
connection.setDoInput(true);
|
|
||||||
connection.setDoOutput(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Send request
|
|
||||||
DataOutputStream wr = new DataOutputStream (
|
|
||||||
connection.getOutputStream ());
|
|
||||||
wr.writeBytes (urlParameters.toString());
|
|
||||||
wr.flush ();
|
|
||||||
wr.close ();
|
|
||||||
|
|
||||||
//Get Response
|
|
||||||
InputStream is = connection.getInputStream();
|
|
||||||
BufferedReader rd = new BufferedReader(new InputStreamReader(is));
|
|
||||||
String line;
|
|
||||||
while((line = rd.readLine()) != null) {
|
|
||||||
response.append(line);
|
|
||||||
response.append('\r');
|
|
||||||
}
|
|
||||||
rd.close();
|
|
||||||
} catch (Exception e) {
|
|
||||||
|
|
||||||
e.printStackTrace();
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if(connection != null) {
|
|
||||||
connection.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(final String response) {
|
|
||||||
//TODO: reimplement progress or remove support for it
|
|
||||||
callback.onTaskComplete(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCancelled() {
|
|
||||||
//TODO: proper handling if needed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
package com.example.hochi.nextcompanion;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
public class ReturnActivity extends AppCompatActivity implements AsyncTaskCallbacks<String> {
|
|
||||||
private String[] bikeArray;
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_return);
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
|
||||||
|
|
||||||
|
|
||||||
Intent intent = getIntent();
|
|
||||||
bikeArray = intent.getStringArrayExtra("bike");
|
|
||||||
|
|
||||||
//if GPS and electric lock, show the instruction
|
|
||||||
TextView tv = findViewById(R.id.gps_info);
|
|
||||||
LinearLayout la = findViewById(R.id.return_form_container);
|
|
||||||
if(bikeArray[2].equals("true")) {
|
|
||||||
tv.setVisibility(View.VISIBLE);
|
|
||||||
la.setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
la.setVisibility(View.VISIBLE);
|
|
||||||
tv.setVisibility(View.INVISIBLE);
|
|
||||||
Button mReturnSubmitButton = findViewById(R.id.return_submit_button);
|
|
||||||
mReturnSubmitButton.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View view) {
|
|
||||||
returnRequest();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void returnRequest() {
|
|
||||||
TextView mStationInput;
|
|
||||||
mStationInput = findViewById(R.id.return_station_id);
|
|
||||||
String stationID = mStationInput.getText().toString();
|
|
||||||
//get loginkey
|
|
||||||
SharedPreferences sharedPref = getSharedPreferences("persistence", MODE_PRIVATE);
|
|
||||||
String defaultValue = "nokey";
|
|
||||||
String loginKey = sharedPref.getString("loginKey", defaultValue);
|
|
||||||
|
|
||||||
String[] params = {
|
|
||||||
"apikey=", getString(R.string.apikey),
|
|
||||||
"bike=", bikeArray[0],
|
|
||||||
"loginkey=", loginKey,
|
|
||||||
"station=", stationID,
|
|
||||||
"comment=", ""
|
|
||||||
};
|
|
||||||
RequestHandler returnRequestTask = new RequestHandler(this, "POST",
|
|
||||||
"api/return.json", params);
|
|
||||||
returnRequestTask.execute((Void) null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTaskComplete(String response) {
|
|
||||||
//get back to main activity
|
|
||||||
//TODO: *any* response handling
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#ffffff"
|
|
||||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout 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:gravity="center_horizontal"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
|
||||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
|
||||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
|
||||||
android:paddingTop="@dimen/activity_vertical_margin"
|
|
||||||
tools:context=".LoginActivity">
|
|
||||||
|
|
||||||
<!-- Login progress -->
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/login_progress"
|
|
||||||
style="?android:attr/progressBarStyleLarge"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<ScrollView
|
|
||||||
android:id="@+id/login_form"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/phone_login_form"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<android.support.design.widget.TextInputLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/phone"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/prompt_phone"
|
|
||||||
android:inputType="phone"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:singleLine="true" />
|
|
||||||
|
|
||||||
</android.support.design.widget.TextInputLayout>
|
|
||||||
|
|
||||||
<android.support.design.widget.TextInputLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/pin"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/prompt_pin"
|
|
||||||
android:imeActionId="6"
|
|
||||||
android:imeActionLabel="@string/action_sign_in_short"
|
|
||||||
android:imeOptions="actionUnspecified"
|
|
||||||
android:inputType="numberPassword"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:singleLine="true" />
|
|
||||||
|
|
||||||
</android.support.design.widget.TextInputLayout>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/phone_sign_in_button"
|
|
||||||
style="?android:textAppearanceSmall"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:text="@string/action_sign_in"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</ScrollView>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<android.support.design.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:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".MainActivity">
|
|
||||||
|
|
||||||
<android.support.design.widget.AppBarLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:theme="@style/AppTheme.AppBarOverlay">
|
|
||||||
|
|
||||||
<android.support.v7.widget.Toolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?attr/actionBarSize"
|
|
||||||
android:background="?attr/colorPrimary"
|
|
||||||
app:popupTheme="@style/AppTheme.PopupOverlay" />
|
|
||||||
|
|
||||||
</android.support.design.widget.AppBarLayout>
|
|
||||||
|
|
||||||
<include layout="@layout/content_main" />
|
|
||||||
|
|
||||||
<android.support.design.widget.FloatingActionButton
|
|
||||||
android:id="@+id/fab"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|end"
|
|
||||||
android:layout_margin="@dimen/fab_margin"
|
|
||||||
android:src="@drawable/ic_add_white_24dp" />
|
|
||||||
|
|
||||||
</android.support.design.widget.CoordinatorLayout>
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<android.support.constraint.ConstraintLayout 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"
|
|
||||||
android:gravity="center_horizontal"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
|
||||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
|
||||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
|
||||||
android:paddingTop="@dimen/activity_vertical_margin"
|
|
||||||
tools:context=".RentActivity">
|
|
||||||
<ScrollView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/rent_form"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<android.support.design.widget.TextInputLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/bike_id"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/prompt_bike_id"
|
|
||||||
android:inputType="number"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:singleLine="true" />
|
|
||||||
|
|
||||||
</android.support.design.widget.TextInputLayout>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/rent_submit_button"
|
|
||||||
style="?android:textAppearanceSmall"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:text="@string/action_rent_submit"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</ScrollView>
|
|
||||||
</android.support.constraint.ConstraintLayout>
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<android.support.constraint.ConstraintLayout 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"
|
|
||||||
android:gravity="center_horizontal"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
|
||||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
|
||||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
|
||||||
android:paddingTop="@dimen/activity_vertical_margin"
|
|
||||||
tools:context=".ReturnActivity">
|
|
||||||
<ScrollView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/return_form"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/return_form_container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
<android.support.design.widget.TextInputLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/return_station_id"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/prompt_return_station_id"
|
|
||||||
android:inputType="number"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:singleLine="true" />
|
|
||||||
|
|
||||||
</android.support.design.widget.TextInputLayout>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/return_submit_button"
|
|
||||||
style="?android:textAppearanceSmall"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:text="@string/action_return_submit"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/gps_info"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textSize="20sp"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:text="@string/indicator_electronic_lock" />
|
|
||||||
</LinearLayout>
|
|
||||||
</ScrollView>
|
|
||||||
</android.support.constraint.ConstraintLayout>
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<android.support.constraint.ConstraintLayout 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"
|
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
|
||||||
tools:context=".MainActivity"
|
|
||||||
tools:showIn="@layout/activity_main">
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/noBikes"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textSize="20sp"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:text="@string/indicator_no_bikes" />
|
|
||||||
|
|
||||||
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/listview"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
|
|
||||||
</android.support.constraint.ConstraintLayout>
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<menu 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"
|
|
||||||
tools:context="com.example.hochi.nextcompanion.MainActivity">
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_map"
|
|
||||||
android:orderInCategory="100"
|
|
||||||
android:title="@string/action_map"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_logout"
|
|
||||||
android:orderInCategory="100"
|
|
||||||
android:title="@string/action_logout"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
</menu>
|
|
||||||
|
|
@ -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_launcher_background"/>
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
|
@ -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_launcher_background"/>
|
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="colorPrimary">#3F51B5</color>
|
|
||||||
<color name="colorPrimaryDark">#303F9F</color>
|
|
||||||
<color name="colorAccent">#9c274f</color>
|
|
||||||
</resources>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<resources>
|
|
||||||
<dimen name="fab_margin">16dp</dimen>
|
|
||||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
|
||||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
|
||||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
|
||||||
</resources>
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#000000</color>
|
|
||||||
</resources>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<resources>
|
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="AppTheme" parent="Theme.AppCompat">
|
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBar">
|
|
||||||
<item name="windowActionBar">false</item>
|
|
||||||
<item name="windowNoTitle">true</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
|
|
||||||
|
|
||||||
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package com.example.hochi.nextcompanion;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
|
||||||
*
|
|
||||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
|
||||||
*/
|
|
||||||
public class ExampleUnitTest {
|
|
||||||
@Test
|
|
||||||
public void addition_isCorrect() {
|
|
||||||
assertEquals(4, 2 + 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1472
build-aux/cargo-sources.json
Normal file
5
build-aux/cargo-vendor-config.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
[source.crates-io]
|
||||||
|
replace-with = "vendored-sources"
|
||||||
|
|
||||||
|
[source.vendored-sources]
|
||||||
|
directory = "/run/build/next-companion/cargo-vendor"
|
||||||
55
build-aux/flatpak-cargo-generator.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate Flatpak source entries for Cargo dependencies from Cargo.lock.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 build-aux/flatpak-cargo-generator.py [Cargo.lock] \
|
||||||
|
> build-aux/cargo-sources.json
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
import tomli as tomllib # pip install tomli
|
||||||
|
except ImportError:
|
||||||
|
print("Error: requires Python 3.11+ or the 'tomli' package", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
CRATES_IO_DL = "https://static.crates.io/crates"
|
||||||
|
REGISTRY_SOURCE = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
lockfile = sys.argv[1] if len(sys.argv) > 1 else "Cargo.lock"
|
||||||
|
|
||||||
|
with open(lockfile, "rb") as f:
|
||||||
|
lock = tomllib.load(f)
|
||||||
|
|
||||||
|
sources = []
|
||||||
|
|
||||||
|
for pkg in lock.get("package", []):
|
||||||
|
name = pkg["name"]
|
||||||
|
version = pkg["version"]
|
||||||
|
source = pkg.get("source", "")
|
||||||
|
checksum = pkg.get("checksum")
|
||||||
|
|
||||||
|
# Only vendor packages from crates.io (they have a checksum)
|
||||||
|
if source == REGISTRY_SOURCE and checksum:
|
||||||
|
sources.append(
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gz",
|
||||||
|
"url": f"{CRATES_IO_DL}/{name}/{version}/download",
|
||||||
|
"sha256": checksum,
|
||||||
|
"dest": f"cargo-vendor/{name}-{version}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
print(json.dumps(sources, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
27
build.gradle
|
|
@ -1,27 +0,0 @@
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
jcenter()
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath 'com.android.tools.build:gradle:3.3.1'
|
|
||||||
|
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
|
||||||
// in the individual module build.gradle files
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
jcenter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task clean(type: Delete) {
|
|
||||||
delete rootProject.buildDir
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
62
flake.lock
generated
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1772963539,
|
||||||
|
"narHash": "sha256-9jVDGZnvCckTGdYT53d/EfznygLskyLQXYwJLKMPsZs=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "9dcb002ca1690658be4a04645215baea8b95f31d",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1744536153,
|
||||||
|
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1773115373,
|
||||||
|
"narHash": "sha256-bfK9FJFcQth6f3ydYggS5m0z2NRGF/PY6Y2XgZDJ6pg=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "1924b4672a2b8e4aee6e6652ec2e59a8d3c5648e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
51
flake.nix
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
description = "NextCompanion — a minimal GTK4 Nextbike client for Linux";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, rust-overlay, ... }:
|
||||||
|
let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
overlays = [ (import rust-overlay) ];
|
||||||
|
pkgs = import nixpkgs { inherit system overlays; };
|
||||||
|
|
||||||
|
runtimeDeps = with pkgs; [
|
||||||
|
gtk4
|
||||||
|
libadwaita
|
||||||
|
glib
|
||||||
|
];
|
||||||
|
buildDeps = with pkgs; [
|
||||||
|
pkg-config
|
||||||
|
rust-bin.stable.latest.default
|
||||||
|
];
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.${system}.default = pkgs.mkShell {
|
||||||
|
buildInputs = buildDeps ++ runtimeDeps;
|
||||||
|
shellHook = ''
|
||||||
|
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath runtimeDeps}:$LD_LIBRARY_PATH"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
packages.${system}.default = pkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = "next-companion";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = ./.;
|
||||||
|
cargoLock.lockFile = ./Cargo.lock;
|
||||||
|
nativeBuildInputs = buildDeps;
|
||||||
|
buildInputs = runtimeDeps;
|
||||||
|
postInstall = ''
|
||||||
|
install -Dm644 data/icons/org.nextbike.NextCompanion.png \
|
||||||
|
$out/share/icons/hicolor/512x512/apps/org.nextbike.NextCompanion.png
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
apps.${system}.default = {
|
||||||
|
type = "app";
|
||||||
|
program = "${self.packages.${system}.default}/bin/next-companion";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
# Project-wide Gradle settings.
|
|
||||||
# IDE (e.g. Android Studio) users:
|
|
||||||
# Gradle settings configured through the IDE *will override*
|
|
||||||
# any settings specified in this file.
|
|
||||||
# For more details on how to configure your build environment visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
|
||||||
org.gradle.jvmargs=-Xmx1536m
|
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
|
||||||
# org.gradle.parallel=true
|
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
6
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +0,0 @@
|
||||||
#Fri May 03 12:47:19 GMT 2019
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
|
|
||||||
172
gradlew
vendored
|
|
@ -1,172 +0,0 @@
|
||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
##
|
|
||||||
## Gradle start up script for UN*X
|
|
||||||
##
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
PRG="$0"
|
|
||||||
# Need this for relative symlinks.
|
|
||||||
while [ -h "$PRG" ] ; do
|
|
||||||
ls=`ls -ld "$PRG"`
|
|
||||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
|
||||||
if expr "$link" : '/.*' > /dev/null; then
|
|
||||||
PRG="$link"
|
|
||||||
else
|
|
||||||
PRG=`dirname "$PRG"`"/$link"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
SAVED="`pwd`"
|
|
||||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
|
||||||
APP_HOME="`pwd -P`"
|
|
||||||
cd "$SAVED" >/dev/null
|
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
|
||||||
APP_BASE_NAME=`basename "$0"`
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS=""
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD="maximum"
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "`uname`" in
|
|
||||||
CYGWIN* )
|
|
||||||
cygwin=true
|
|
||||||
;;
|
|
||||||
Darwin* )
|
|
||||||
darwin=true
|
|
||||||
;;
|
|
||||||
MINGW* )
|
|
||||||
msys=true
|
|
||||||
;;
|
|
||||||
NONSTOP* )
|
|
||||||
nonstop=true
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
|
||||||
else
|
|
||||||
JAVACMD="$JAVA_HOME/bin/java"
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD="java"
|
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
|
||||||
MAX_FD_LIMIT=`ulimit -H -n`
|
|
||||||
if [ $? -eq 0 ] ; then
|
|
||||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
|
||||||
MAX_FD="$MAX_FD_LIMIT"
|
|
||||||
fi
|
|
||||||
ulimit -n $MAX_FD
|
|
||||||
if [ $? -ne 0 ] ; then
|
|
||||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Darwin, add options to specify how the application appears in the dock
|
|
||||||
if $darwin; then
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Cygwin, switch paths to Windows format before running java
|
|
||||||
if $cygwin ; then
|
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
|
||||||
|
|
||||||
# We build the pattern for arguments to be converted via cygpath
|
|
||||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
|
||||||
SEP=""
|
|
||||||
for dir in $ROOTDIRSRAW ; do
|
|
||||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
|
||||||
SEP="|"
|
|
||||||
done
|
|
||||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
|
||||||
# Add a user-defined pattern to the cygpath arguments
|
|
||||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
|
||||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
|
||||||
fi
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
i=0
|
|
||||||
for arg in "$@" ; do
|
|
||||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
|
||||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
|
||||||
|
|
||||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
|
||||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
|
||||||
else
|
|
||||||
eval `echo args$i`="\"$arg\""
|
|
||||||
fi
|
|
||||||
i=$((i+1))
|
|
||||||
done
|
|
||||||
case $i in
|
|
||||||
(0) set -- ;;
|
|
||||||
(1) set -- "$args0" ;;
|
|
||||||
(2) set -- "$args0" "$args1" ;;
|
|
||||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
|
||||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
|
||||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
|
||||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
|
||||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
|
||||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
|
||||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Escape application args
|
|
||||||
save () {
|
|
||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
|
||||||
echo " "
|
|
||||||
}
|
|
||||||
APP_ARGS=$(save "$@")
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
|
||||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
|
||||||
|
|
||||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
|
||||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
||||||
84
gradlew.bat
vendored
|
|
@ -1,84 +0,0 @@
|
||||||
@if "%DEBUG%" == "" @echo off
|
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
|
||||||
@rem Gradle startup script for Windows
|
|
||||||
@rem
|
|
||||||
@rem ##########################################################################
|
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
|
||||||
set APP_BASE_NAME=%~n0
|
|
||||||
set APP_HOME=%DIRNAME%
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
set DEFAULT_JVM_OPTS=
|
|
||||||
|
|
||||||
@rem Find java.exe
|
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
|
||||||
if "%ERRORLEVEL%" == "0" goto init
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto init
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:init
|
|
||||||
@rem Get command-line arguments, handling Windows variants
|
|
||||||
|
|
||||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
|
||||||
|
|
||||||
:win9xME_args
|
|
||||||
@rem Slurp the command line arguments.
|
|
||||||
set CMD_LINE_ARGS=
|
|
||||||
set _SKIP=2
|
|
||||||
|
|
||||||
:win9xME_args_slurp
|
|
||||||
if "x%~1" == "x" goto execute
|
|
||||||
|
|
||||||
set CMD_LINE_ARGS=%*
|
|
||||||
|
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
|
||||||
|
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
|
||||||
exit /b 1
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
47
org.nextbike.NextCompanion.yml
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
app-id: org.nextbike.NextCompanion
|
||||||
|
runtime: org.gnome.Platform
|
||||||
|
runtime-version: '47'
|
||||||
|
sdk: org.gnome.Sdk
|
||||||
|
sdk-extensions:
|
||||||
|
- org.freedesktop.Sdk.Extension.rust-stable
|
||||||
|
command: next-companion
|
||||||
|
|
||||||
|
finish-args:
|
||||||
|
- --share=network # nextbike API calls
|
||||||
|
- --share=ipc
|
||||||
|
- --socket=wayland
|
||||||
|
- --socket=fallback-x11
|
||||||
|
- --device=dri # GPU acceleration
|
||||||
|
|
||||||
|
build-options:
|
||||||
|
append-path: /usr/lib/sdk/rust-stable/bin
|
||||||
|
env:
|
||||||
|
CARGO_HOME: /run/build/next-companion/cargo-home
|
||||||
|
RUST_BACKTRACE: '1'
|
||||||
|
arch:
|
||||||
|
aarch64:
|
||||||
|
env:
|
||||||
|
CARGO_BUILD_TARGET: aarch64-unknown-linux-gnu
|
||||||
|
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-unknown-linux-gnu-gcc
|
||||||
|
|
||||||
|
modules:
|
||||||
|
- name: next-companion
|
||||||
|
buildsystem: simple
|
||||||
|
build-commands:
|
||||||
|
- mkdir -p .cargo
|
||||||
|
- cp cargo-vendor-config/cargo-vendor-config.toml .cargo/config.toml
|
||||||
|
- cargo --offline build --release
|
||||||
|
- |
|
||||||
|
install -Dm755 \
|
||||||
|
"target/${CARGO_BUILD_TARGET:+${CARGO_BUILD_TARGET}/}release/next-companion" \
|
||||||
|
/app/bin/next-companion
|
||||||
|
- install -Dm644 data/icons/org.nextbike.NextCompanion.png
|
||||||
|
/app/share/icons/hicolor/512x512/apps/org.nextbike.NextCompanion.png
|
||||||
|
sources:
|
||||||
|
- type: dir
|
||||||
|
path: .
|
||||||
|
- type: file
|
||||||
|
path: build-aux/cargo-vendor-config.toml
|
||||||
|
dest: cargo-vendor-config
|
||||||
|
dest-filename: cargo-vendor-config.toml
|
||||||
|
- build-aux/cargo-sources.json
|
||||||
1
result
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/gan72x7dji7c6lwngwfx88my14azg9wn-next-companion-0.1.0
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
include ':app'
|
|
||||||
608
src/main.rs
Normal file
|
|
@ -0,0 +1,608 @@
|
||||||
|
use adw::prelude::*;
|
||||||
|
use adw::{Application, ApplicationWindow, BottomSheet, Clamp, HeaderBar, NavigationPage, NavigationView};
|
||||||
|
use gtk::{
|
||||||
|
Box, Button, Entry, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow, Spinner, Stack,
|
||||||
|
};
|
||||||
|
use gtk::gio;
|
||||||
|
use gtk::glib;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
const API_KEY: &str = "3IaBlP9OZw14dvES";
|
||||||
|
const BASE_URL: &str = "https://api.nextbike.net";
|
||||||
|
|
||||||
|
// ── Bike model ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Bike {
|
||||||
|
id: String,
|
||||||
|
code: String,
|
||||||
|
electric_lock: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Persistent login key ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn config_path() -> PathBuf {
|
||||||
|
let mut p = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||||
|
p.push("next-companion");
|
||||||
|
p.push("loginkey");
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_loginkey() -> Option<String> {
|
||||||
|
fs::read_to_string(config_path())
|
||||||
|
.ok()
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_loginkey(key: &str) {
|
||||||
|
let path = config_path();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
let _ = fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
let _ = fs::write(&path, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_loginkey() {
|
||||||
|
let _ = fs::remove_file(config_path());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── API calls (blocking — run via gio::spawn_blocking) ────────────────────────
|
||||||
|
|
||||||
|
fn api_login(phone: &str, pin: &str) -> Result<String, String> {
|
||||||
|
let resp = reqwest::blocking::Client::new()
|
||||||
|
.post(format!("{BASE_URL}/api/login.json"))
|
||||||
|
.form(&[("apikey", API_KEY), ("mobile", phone), ("pin", pin)])
|
||||||
|
.send()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let json: serde_json::Value =
|
||||||
|
serde_json::from_str(&resp.text().map_err(|e| e.to_string())?)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
json["user"]["loginkey"]
|
||||||
|
.as_str()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.ok_or_else(|| "Invalid credentials".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn api_get_rentals(loginkey: &str) -> Result<Vec<Bike>, String> {
|
||||||
|
let resp = reqwest::blocking::Client::new()
|
||||||
|
.post(format!("{BASE_URL}/api/getOpenRentals.json"))
|
||||||
|
.form(&[("apikey", API_KEY), ("loginkey", loginkey)])
|
||||||
|
.send()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let json: serde_json::Value =
|
||||||
|
serde_json::from_str(&resp.text().map_err(|e| e.to_string())?)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let arr = json["rentalCollection"]
|
||||||
|
.as_array()
|
||||||
|
.ok_or_else(|| "No rental data".to_string())?;
|
||||||
|
Ok(arr
|
||||||
|
.iter()
|
||||||
|
.map(|b| Bike {
|
||||||
|
id: b["bike"].as_str().unwrap_or("").to_string(),
|
||||||
|
code: b["code"].as_str().unwrap_or("").to_string(),
|
||||||
|
electric_lock: b["electric_lock"].as_str().map_or(false, |s| s == "true"),
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn api_rent(loginkey: &str, bike_id: &str) -> Result<(), String> {
|
||||||
|
reqwest::blocking::Client::new()
|
||||||
|
.post(format!("{BASE_URL}/api/rent.json"))
|
||||||
|
.form(&[("apikey", API_KEY), ("loginkey", loginkey), ("bike", bike_id)])
|
||||||
|
.send()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn api_return(loginkey: &str, bike_id: &str, station_id: &str) -> Result<(), String> {
|
||||||
|
reqwest::blocking::Client::new()
|
||||||
|
.post(format!("{BASE_URL}/api/return.json"))
|
||||||
|
.form(&[
|
||||||
|
("apikey", API_KEY),
|
||||||
|
("bike", bike_id),
|
||||||
|
("loginkey", loginkey),
|
||||||
|
("station", station_id),
|
||||||
|
("comment", ""),
|
||||||
|
])
|
||||||
|
.send()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Async helpers (run on GLib main context) ──────────────────────────────────
|
||||||
|
|
||||||
|
async fn load_rentals(key: String, bikes: Rc<RefCell<Vec<Bike>>>, bikes_list: ListBox, list_stack: Stack) {
|
||||||
|
let result = match gio::spawn_blocking(move || api_get_rentals(&key)).await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
if let Ok(new_bikes) = result {
|
||||||
|
while let Some(child) = bikes_list.first_child() {
|
||||||
|
bikes_list.remove(&child);
|
||||||
|
}
|
||||||
|
for bike in &new_bikes {
|
||||||
|
let text = format!(
|
||||||
|
"Bike {} · code: {}{}",
|
||||||
|
bike.id,
|
||||||
|
bike.code,
|
||||||
|
if bike.electric_lock { " ⚡" } else { "" }
|
||||||
|
);
|
||||||
|
let lbl = Label::builder()
|
||||||
|
.label(&text)
|
||||||
|
.xalign(0.0)
|
||||||
|
.margin_top(14)
|
||||||
|
.margin_bottom(14)
|
||||||
|
.margin_start(12)
|
||||||
|
.margin_end(12)
|
||||||
|
.build();
|
||||||
|
let row = ListBoxRow::new();
|
||||||
|
row.set_child(Some(&lbl));
|
||||||
|
bikes_list.append(&row);
|
||||||
|
}
|
||||||
|
list_stack.set_visible_child_name(if new_bikes.is_empty() { "empty" } else { "list" });
|
||||||
|
*bikes.borrow_mut() = new_bikes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn main() -> glib::ExitCode {
|
||||||
|
let app = Application::builder()
|
||||||
|
.application_id("org.nextbike.NextCompanion")
|
||||||
|
.build();
|
||||||
|
app.connect_activate(build_ui);
|
||||||
|
app.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UI ────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn build_ui(app: &Application) {
|
||||||
|
let loginkey: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(load_loginkey()));
|
||||||
|
let bikes: Rc<RefCell<Vec<Bike>>> = Rc::new(RefCell::new(vec![]));
|
||||||
|
let return_bike: Rc<RefCell<Option<Bike>>> = Rc::new(RefCell::new(None));
|
||||||
|
|
||||||
|
// ── Login page ────────────────────────────────────────────────────────────
|
||||||
|
let phone_entry = Entry::builder()
|
||||||
|
.placeholder_text("Phone number")
|
||||||
|
.build();
|
||||||
|
let pin_entry = Entry::builder()
|
||||||
|
.placeholder_text("PIN")
|
||||||
|
.visibility(false)
|
||||||
|
.build();
|
||||||
|
let login_err = Label::builder()
|
||||||
|
.css_classes(["error"])
|
||||||
|
.wrap(true)
|
||||||
|
.visible(false)
|
||||||
|
.build();
|
||||||
|
let login_btn = Button::builder()
|
||||||
|
.label("Sign In")
|
||||||
|
.css_classes(["suggested-action", "pill"])
|
||||||
|
.build();
|
||||||
|
let login_spinner = Spinner::new();
|
||||||
|
|
||||||
|
let login_form = Box::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.spacing(12)
|
||||||
|
.margin_top(24).margin_bottom(24).margin_start(12).margin_end(12)
|
||||||
|
.build();
|
||||||
|
login_form.append(&phone_entry);
|
||||||
|
login_form.append(&pin_entry);
|
||||||
|
login_form.append(&login_err);
|
||||||
|
login_form.append(&login_btn);
|
||||||
|
login_form.append(&login_spinner);
|
||||||
|
|
||||||
|
let login_clamp = Clamp::builder().maximum_size(400).build();
|
||||||
|
login_clamp.set_child(Some(&login_form));
|
||||||
|
|
||||||
|
let login_body = Box::builder().orientation(Orientation::Vertical).build();
|
||||||
|
login_body.append(&HeaderBar::new());
|
||||||
|
login_body.append(&login_clamp);
|
||||||
|
|
||||||
|
let login_page = NavigationPage::builder()
|
||||||
|
.title("Sign In")
|
||||||
|
.child(&login_body)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// ── Main page ─────────────────────────────────────────────────────────────
|
||||||
|
let bikes_list = ListBox::builder()
|
||||||
|
.css_classes(["boxed-list"])
|
||||||
|
.selection_mode(gtk::SelectionMode::None)
|
||||||
|
.margin_top(8).margin_bottom(8).margin_start(12).margin_end(12)
|
||||||
|
.build();
|
||||||
|
let empty_label = Label::builder()
|
||||||
|
.label("No active rentals")
|
||||||
|
.margin_top(48)
|
||||||
|
.css_classes(["dim-label"])
|
||||||
|
.build();
|
||||||
|
let list_stack = Stack::new();
|
||||||
|
list_stack.add_named(&empty_label, Some("empty"));
|
||||||
|
list_stack.add_named(&bikes_list, Some("list"));
|
||||||
|
list_stack.set_visible_child_name("empty");
|
||||||
|
|
||||||
|
let scroll = ScrolledWindow::builder().vexpand(true).child(&list_stack).build();
|
||||||
|
|
||||||
|
let rent_btn = Button::builder()
|
||||||
|
.label("Rent a Bike")
|
||||||
|
.css_classes(["suggested-action", "pill"])
|
||||||
|
.margin_top(8).margin_bottom(12).margin_start(12).margin_end(12)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let main_hdr = HeaderBar::new();
|
||||||
|
let logout_btn = Button::builder()
|
||||||
|
.icon_name("system-log-out-symbolic")
|
||||||
|
.tooltip_text("Logout")
|
||||||
|
.build();
|
||||||
|
let refresh_btn = Button::builder()
|
||||||
|
.icon_name("view-refresh-symbolic")
|
||||||
|
.tooltip_text("Refresh")
|
||||||
|
.build();
|
||||||
|
main_hdr.pack_end(&logout_btn);
|
||||||
|
main_hdr.pack_start(&refresh_btn);
|
||||||
|
|
||||||
|
// ── Bottom sheet (rent + return) ──────────────────────────────────────────
|
||||||
|
|
||||||
|
// — Rent form —
|
||||||
|
let bike_entry = Entry::builder()
|
||||||
|
.placeholder_text("Bike number")
|
||||||
|
.input_purpose(gtk::InputPurpose::Digits)
|
||||||
|
.build();
|
||||||
|
let rent_err = Label::builder()
|
||||||
|
.css_classes(["error"])
|
||||||
|
.wrap(true)
|
||||||
|
.visible(false)
|
||||||
|
.build();
|
||||||
|
let rent_submit = Button::builder()
|
||||||
|
.label("Rent")
|
||||||
|
.css_classes(["suggested-action", "pill"])
|
||||||
|
.build();
|
||||||
|
let rent_spinner = Spinner::new();
|
||||||
|
|
||||||
|
let rent_sheet = Box::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.spacing(12)
|
||||||
|
.build();
|
||||||
|
let rent_form = Box::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.spacing(12)
|
||||||
|
.build();
|
||||||
|
rent_form.append(&bike_entry);
|
||||||
|
rent_form.append(&rent_err);
|
||||||
|
rent_form.append(&rent_submit);
|
||||||
|
rent_form.append(&rent_spinner);
|
||||||
|
rent_sheet.append(&rent_form);
|
||||||
|
|
||||||
|
// — Return form —
|
||||||
|
let station_entry = Entry::builder()
|
||||||
|
.placeholder_text("Station number")
|
||||||
|
.input_purpose(gtk::InputPurpose::Digits)
|
||||||
|
.build();
|
||||||
|
let ret_err = Label::builder()
|
||||||
|
.css_classes(["error"])
|
||||||
|
.wrap(true)
|
||||||
|
.visible(false)
|
||||||
|
.build();
|
||||||
|
let ret_submit = Button::builder()
|
||||||
|
.label("Return Bike")
|
||||||
|
.css_classes(["destructive-action", "pill"])
|
||||||
|
.build();
|
||||||
|
let ret_spinner = Spinner::new();
|
||||||
|
let electric_msg = Label::builder()
|
||||||
|
.label("This bike has an electric lock.\nJust close the lock to return it.")
|
||||||
|
.wrap(true)
|
||||||
|
.justify(gtk::Justification::Center)
|
||||||
|
.margin_top(24)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let manual_form = Box::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.spacing(12)
|
||||||
|
.build();
|
||||||
|
manual_form.append(&station_entry);
|
||||||
|
manual_form.append(&ret_err);
|
||||||
|
manual_form.append(&ret_submit);
|
||||||
|
manual_form.append(&ret_spinner);
|
||||||
|
|
||||||
|
let ret_inner = Stack::new();
|
||||||
|
ret_inner.add_named(&manual_form, Some("manual"));
|
||||||
|
ret_inner.add_named(&electric_msg, Some("electric"));
|
||||||
|
|
||||||
|
let ret_sheet = Box::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.spacing(12)
|
||||||
|
.build();
|
||||||
|
ret_sheet.append(&ret_inner);
|
||||||
|
|
||||||
|
// — Shared sheet stack —
|
||||||
|
let sheet_stack = Stack::new();
|
||||||
|
sheet_stack.add_named(&rent_sheet, Some("rent"));
|
||||||
|
sheet_stack.add_named(&ret_sheet, Some("return"));
|
||||||
|
|
||||||
|
let sheet_box = Box::builder()
|
||||||
|
.orientation(Orientation::Vertical)
|
||||||
|
.margin_top(34)
|
||||||
|
.margin_bottom(18)
|
||||||
|
.margin_start(16)
|
||||||
|
.margin_end(16)
|
||||||
|
.build();
|
||||||
|
sheet_box.append(&sheet_stack);
|
||||||
|
|
||||||
|
let bottom_sheet = BottomSheet::builder()
|
||||||
|
.show_drag_handle(true)
|
||||||
|
.sheet(&sheet_box)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let main_content = Box::builder().orientation(Orientation::Vertical).build();
|
||||||
|
main_content.append(&scroll);
|
||||||
|
main_content.append(&rent_btn);
|
||||||
|
bottom_sheet.set_content(Some(&main_content));
|
||||||
|
|
||||||
|
let main_body = Box::builder().orientation(Orientation::Vertical).build();
|
||||||
|
main_body.append(&main_hdr);
|
||||||
|
main_body.append(&bottom_sheet);
|
||||||
|
|
||||||
|
let main_page = NavigationPage::builder()
|
||||||
|
.title("NextCompanion")
|
||||||
|
.child(&main_body)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// ── Navigation view ───────────────────────────────────────────────────────
|
||||||
|
let nav = NavigationView::new();
|
||||||
|
nav.push(&main_page);
|
||||||
|
if loginkey.borrow().is_none() {
|
||||||
|
nav.push(&login_page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Window ────────────────────────────────────────────────────────────────
|
||||||
|
let window = ApplicationWindow::builder()
|
||||||
|
.application(app)
|
||||||
|
.title("NextCompanion")
|
||||||
|
.default_width(390)
|
||||||
|
.default_height(700)
|
||||||
|
.content(&nav)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// ── Login button ──────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
let phone = phone_entry.clone();
|
||||||
|
let pin = pin_entry.clone();
|
||||||
|
let err = login_err.clone();
|
||||||
|
let spinner = login_spinner.clone();
|
||||||
|
let btn = login_btn.clone();
|
||||||
|
let nav = nav.clone();
|
||||||
|
let loginkey = loginkey.clone();
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let bikes_list = bikes_list.clone();
|
||||||
|
let list_stack = list_stack.clone();
|
||||||
|
login_btn.connect_clicked(move |_| {
|
||||||
|
let p = phone.text().to_string();
|
||||||
|
let n = pin.text().to_string();
|
||||||
|
if p.is_empty() || n.is_empty() {
|
||||||
|
err.set_label("Phone and PIN are required");
|
||||||
|
err.set_visible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
err.set_visible(false);
|
||||||
|
spinner.set_spinning(true);
|
||||||
|
btn.set_sensitive(false);
|
||||||
|
|
||||||
|
let spinner = spinner.clone();
|
||||||
|
let btn = btn.clone();
|
||||||
|
let err = err.clone();
|
||||||
|
let nav = nav.clone();
|
||||||
|
let loginkey = loginkey.clone();
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let bikes_list = bikes_list.clone();
|
||||||
|
let list_stack = list_stack.clone();
|
||||||
|
glib::MainContext::default().spawn_local(async move {
|
||||||
|
let result = match gio::spawn_blocking(move || api_login(&p, &n)).await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => Err("Internal error".to_string()),
|
||||||
|
};
|
||||||
|
spinner.set_spinning(false);
|
||||||
|
btn.set_sensitive(true);
|
||||||
|
match result {
|
||||||
|
Ok(key) => {
|
||||||
|
save_loginkey(&key);
|
||||||
|
*loginkey.borrow_mut() = Some(key.clone());
|
||||||
|
nav.pop();
|
||||||
|
load_rentals(key, bikes, bikes_list, list_stack).await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
err.set_label(&e);
|
||||||
|
err.set_visible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Logout button ─────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
let nav = nav.clone();
|
||||||
|
let login_page = login_page.clone();
|
||||||
|
let loginkey = loginkey.clone();
|
||||||
|
logout_btn.connect_clicked(move |_| {
|
||||||
|
clear_loginkey();
|
||||||
|
*loginkey.borrow_mut() = None;
|
||||||
|
nav.push(&login_page);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Refresh button ────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
let loginkey = loginkey.clone();
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let bikes_list = bikes_list.clone();
|
||||||
|
let list_stack = list_stack.clone();
|
||||||
|
refresh_btn.connect_clicked(move |_| {
|
||||||
|
if let Some(key) = loginkey.borrow().clone() {
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let bikes_list = bikes_list.clone();
|
||||||
|
let list_stack = list_stack.clone();
|
||||||
|
glib::MainContext::default().spawn_local(async move {
|
||||||
|
load_rentals(key, bikes, bikes_list, list_stack).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Open rent sheet ───────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
|
let sheet_stack = sheet_stack.clone();
|
||||||
|
let bike_entry = bike_entry.clone();
|
||||||
|
let rent_err = rent_err.clone();
|
||||||
|
rent_btn.connect_clicked(move |_| {
|
||||||
|
bike_entry.set_text("");
|
||||||
|
rent_err.set_visible(false);
|
||||||
|
sheet_stack.set_visible_child_name("rent");
|
||||||
|
bottom_sheet.set_open(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Rent submit ───────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
let loginkey = loginkey.clone();
|
||||||
|
let entry = bike_entry.clone();
|
||||||
|
let err = rent_err.clone();
|
||||||
|
let spinner = rent_spinner.clone();
|
||||||
|
let btn = rent_submit.clone();
|
||||||
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let bikes_list = bikes_list.clone();
|
||||||
|
let list_stack = list_stack.clone();
|
||||||
|
rent_submit.connect_clicked(move |_| {
|
||||||
|
let id = entry.text().to_string();
|
||||||
|
if id.is_empty() {
|
||||||
|
err.set_label("Enter a bike number");
|
||||||
|
err.set_visible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(key) = loginkey.borrow().clone() {
|
||||||
|
err.set_visible(false);
|
||||||
|
spinner.set_spinning(true);
|
||||||
|
btn.set_sensitive(false);
|
||||||
|
|
||||||
|
let spinner = spinner.clone();
|
||||||
|
let btn = btn.clone();
|
||||||
|
let err = err.clone();
|
||||||
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let bikes_list = bikes_list.clone();
|
||||||
|
let list_stack = list_stack.clone();
|
||||||
|
let key_reload = key.clone();
|
||||||
|
glib::MainContext::default().spawn_local(async move {
|
||||||
|
let result = match gio::spawn_blocking(move || api_rent(&key, &id)).await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => Err("Internal error".to_string()),
|
||||||
|
};
|
||||||
|
spinner.set_spinning(false);
|
||||||
|
btn.set_sensitive(true);
|
||||||
|
if let Err(e) = result {
|
||||||
|
err.set_label(&e);
|
||||||
|
err.set_visible(true);
|
||||||
|
} else {
|
||||||
|
bottom_sheet.set_open(false);
|
||||||
|
load_rentals(key_reload, bikes, bikes_list, list_stack).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Click rental row → open return bottom sheet ───────────────────────────
|
||||||
|
{
|
||||||
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
|
let sheet_stack = sheet_stack.clone();
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let return_bike = return_bike.clone();
|
||||||
|
let ret_inner = ret_inner.clone();
|
||||||
|
let station_entry = station_entry.clone();
|
||||||
|
let ret_err = ret_err.clone();
|
||||||
|
bikes_list.connect_row_activated(move |_, row| {
|
||||||
|
let idx = row.index() as usize;
|
||||||
|
let bike = bikes.borrow().get(idx).cloned();
|
||||||
|
if let Some(bike) = bike {
|
||||||
|
station_entry.set_text("");
|
||||||
|
ret_err.set_visible(false);
|
||||||
|
ret_inner.set_visible_child_name(if bike.electric_lock { "electric" } else { "manual" });
|
||||||
|
*return_bike.borrow_mut() = Some(bike);
|
||||||
|
sheet_stack.set_visible_child_name("return");
|
||||||
|
bottom_sheet.set_open(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Return submit ─────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
let loginkey = loginkey.clone();
|
||||||
|
let return_bike = return_bike.clone();
|
||||||
|
let entry = station_entry.clone();
|
||||||
|
let err = ret_err.clone();
|
||||||
|
let spinner = ret_spinner.clone();
|
||||||
|
let btn = ret_submit.clone();
|
||||||
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let bikes_list = bikes_list.clone();
|
||||||
|
let list_stack = list_stack.clone();
|
||||||
|
ret_submit.connect_clicked(move |_| {
|
||||||
|
let station = entry.text().to_string();
|
||||||
|
if station.is_empty() {
|
||||||
|
err.set_label("Enter a station number");
|
||||||
|
err.set_visible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let (Some(key), Some(bike)) =
|
||||||
|
(loginkey.borrow().clone(), return_bike.borrow().clone())
|
||||||
|
{
|
||||||
|
err.set_visible(false);
|
||||||
|
spinner.set_spinning(true);
|
||||||
|
btn.set_sensitive(false);
|
||||||
|
|
||||||
|
let spinner = spinner.clone();
|
||||||
|
let btn = btn.clone();
|
||||||
|
let err = err.clone();
|
||||||
|
let bottom_sheet = bottom_sheet.clone();
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let bikes_list = bikes_list.clone();
|
||||||
|
let list_stack = list_stack.clone();
|
||||||
|
let key_reload = key.clone();
|
||||||
|
glib::MainContext::default().spawn_local(async move {
|
||||||
|
let result = match gio::spawn_blocking(move || {
|
||||||
|
api_return(&key, &bike.id, &station)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => Err("Internal error".to_string()),
|
||||||
|
};
|
||||||
|
spinner.set_spinning(false);
|
||||||
|
btn.set_sensitive(true);
|
||||||
|
if let Err(e) = result {
|
||||||
|
err.set_label(&e);
|
||||||
|
err.set_visible(true);
|
||||||
|
} else {
|
||||||
|
bottom_sheet.set_open(false);
|
||||||
|
load_rentals(key_reload, bikes, bikes_list, list_stack).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Initial rentals load ──────────────────────────────────────────────────
|
||||||
|
if let Some(key) = loginkey.borrow().clone() {
|
||||||
|
let bikes = bikes.clone();
|
||||||
|
let bikes_list = bikes_list.clone();
|
||||||
|
let list_stack = list_stack.clone();
|
||||||
|
glib::MainContext::default().spawn_local(async move {
|
||||||
|
load_rentals(key, bikes, bikes_list, list_stack).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.present();
|
||||||
|
}
|
||||||