From dca6af5f466a762f343a25635afaf99e7bb61dff Mon Sep 17 00:00:00 2001 From: DevEmperor <56255079+devemperor@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:53:18 +0100 Subject: [PATCH] feature: added image creation with Dall-E --- app/build.gradle | 5 +- app/src/main/AndroidManifest.xml | 14 +- .../wristassist/activities/ChatActivity.java | 2 +- .../activities/CreateImageActivity.java | 274 ++++++++++++++++++ .../wristassist/activities/ImageActivity.java | 95 ++++++ .../wristassist/activities/MainActivity.java | 19 +- .../activities/OpenImageActivity.java | 126 ++++++++ .../activities/PreferencesFragment.java | 60 +++- .../activities/QRCodeActivity.java | 31 ++ .../wristassist/adapters/ImageAdapter.java | 72 +++++ .../wristassist/adapters/UsageAdapter.java | 11 +- .../wristassist/database/ImageModel.java | 61 ++++ .../database/ImagesDatabaseHelper.java | 88 ++++++ .../net/devemperor/wristassist/util/Util.java | 40 ++- app/src/main/res/drawable/add_image.png | Bin 0 -> 9105 bytes .../twotone_add_photo_alternate_24.xml | 9 + .../main/res/drawable/twotone_replay_24.xml | 5 + .../main/res/drawable/twotone_share_24.xml | 14 + .../main/res/layout/activity_create_image.xml | 145 +++++++++ app/src/main/res/layout/activity_image.xml | 8 + app/src/main/res/layout/activity_main.xml | 25 +- .../main/res/layout/activity_open_image.xml | 210 ++++++++++++++ app/src/main/res/layout/activity_qrcode.xml | 14 + app/src/main/res/layout/item_gallery.xml | 28 ++ app/src/main/res/values-de-rDE/strings.xml | 36 ++- app/src/main/res/values/arrays.xml | 15 +- app/src/main/res/values/strings.xml | 35 ++- app/src/main/res/values/themes.xml | 2 + app/src/main/res/xml/fragment_preferences.xml | 54 +++- settings.gradle | 1 + 30 files changed, 1451 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/net/devemperor/wristassist/activities/CreateImageActivity.java create mode 100644 app/src/main/java/net/devemperor/wristassist/activities/ImageActivity.java create mode 100644 app/src/main/java/net/devemperor/wristassist/activities/OpenImageActivity.java create mode 100644 app/src/main/java/net/devemperor/wristassist/activities/QRCodeActivity.java create mode 100644 app/src/main/java/net/devemperor/wristassist/adapters/ImageAdapter.java create mode 100644 app/src/main/java/net/devemperor/wristassist/database/ImageModel.java create mode 100644 app/src/main/java/net/devemperor/wristassist/database/ImagesDatabaseHelper.java create mode 100644 app/src/main/res/drawable/add_image.png create mode 100644 app/src/main/res/drawable/twotone_add_photo_alternate_24.xml create mode 100644 app/src/main/res/drawable/twotone_replay_24.xml create mode 100644 app/src/main/res/drawable/twotone_share_24.xml create mode 100644 app/src/main/res/layout/activity_create_image.xml create mode 100644 app/src/main/res/layout/activity_image.xml create mode 100644 app/src/main/res/layout/activity_open_image.xml create mode 100644 app/src/main/res/layout/activity_qrcode.xml create mode 100644 app/src/main/res/layout/item_gallery.xml diff --git a/app/build.gradle b/app/build.gradle index eb7eb9b..83117d4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,11 +43,14 @@ dependencies { implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'androidx.wear.watchface:watchface-complications-data-source-ktx:1.1.1' - implementation 'com.theokanning.openai-gpt3-java:service:0.16.1' + implementation 'com.theokanning.openai-gpt3-java:service:0.18.2' implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-jackson:2.9.0' implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' implementation 'commons-validator:commons-validator:1.7' + implementation 'com.jsibbold:zoomage:1.3.1' + implementation 'com.github.kenglxn.QRGen:android:3.0.1' + implementation 'com.squareup.picasso:picasso:2.8' implementation 'io.noties.markwon:core:4.6.2' implementation 'io.noties.markwon:ext-strikethrough:4.6.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ba810f5..b07d97a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,7 +31,6 @@ - @@ -50,6 +49,18 @@ + + + + @@ -71,7 +82,6 @@ diff --git a/app/src/main/java/net/devemperor/wristassist/activities/ChatActivity.java b/app/src/main/java/net/devemperor/wristassist/activities/ChatActivity.java index b2a2eb9..2ead3c8 100644 --- a/app/src/main/java/net/devemperor/wristassist/activities/ChatActivity.java +++ b/app/src/main/java/net/devemperor/wristassist/activities/ChatActivity.java @@ -276,7 +276,7 @@ private void query(String query) throws JSONException, IOException { Usage usage = result.getUsage(); ChatItem assistantItem = new ChatItem(answer, usage.getTotalTokens()); - usageDatabaseHelper.edit(finalModel, usage.getTotalTokens(), Util.calcCost(finalModel, usage.getPromptTokens(), usage.getCompletionTokens())); + usageDatabaseHelper.edit(finalModel, usage.getTotalTokens(), Util.calcCostChat(finalModel, usage.getPromptTokens(), usage.getCompletionTokens())); if (Thread.interrupted()) { return; diff --git a/app/src/main/java/net/devemperor/wristassist/activities/CreateImageActivity.java b/app/src/main/java/net/devemperor/wristassist/activities/CreateImageActivity.java new file mode 100644 index 0000000..c5a38f8 --- /dev/null +++ b/app/src/main/java/net/devemperor/wristassist/activities/CreateImageActivity.java @@ -0,0 +1,274 @@ +package net.devemperor.wristassist.activities; + +import static com.theokanning.openai.service.OpenAiService.defaultClient; +import static com.theokanning.openai.service.OpenAiService.defaultObjectMapper; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Bundle; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ProgressBar; +import android.widget.ScrollView; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.firebase.crashlytics.FirebaseCrashlytics; +import com.jsibbold.zoomage.ZoomageView; +import com.theokanning.openai.client.OpenAiApi; +import com.theokanning.openai.image.CreateImageRequest; +import com.theokanning.openai.image.Image; +import com.theokanning.openai.image.ImageResult; +import com.theokanning.openai.service.OpenAiService; + +import net.devemperor.wristassist.R; +import net.devemperor.wristassist.database.ImageModel; +import net.devemperor.wristassist.database.ImagesDatabaseHelper; +import net.devemperor.wristassist.database.UsageDatabaseHelper; +import net.devemperor.wristassist.util.Util; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.time.Duration; +import java.util.Objects; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import retrofit2.converter.jackson.JacksonConverterFactory; + +public class CreateImageActivity extends AppCompatActivity { + + SharedPreferences sp; + UsageDatabaseHelper usageDatabaseHelper; + ImagesDatabaseHelper imagesDatabaseHelper; + OpenAiService service; + Vibrator vibrator; + + ScrollView createImageSv; + ProgressBar imagePb; + TextView errorTv; + ImageButton retryBtn; + ZoomageView imageView; + ImageButton shareBtn; + TextView expiresInTv; + ConstraintLayout saveDiscardBtns; + + String prompt; + String model; + String quality; + String style; + String size; + ImageResult imageResult; + Image image; + Bitmap bitmap; + ExecutorService thread; + Timer timer = new Timer(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_create_image); + + sp = getSharedPreferences("net.devemperor.wristassist", MODE_PRIVATE); + imagesDatabaseHelper = new ImagesDatabaseHelper(this); + usageDatabaseHelper = new UsageDatabaseHelper(this); + + String apiKey = sp.getString("net.devemperor.wristassist.api_key", "noApiKey"); + String apiHost = sp.getString("net.devemperor.wristassist.custom_server_host", "https://api.openai.com/"); + ObjectMapper mapper = defaultObjectMapper(); // replaces all control chars (#10 @ GH) + OkHttpClient client = defaultClient(apiKey.replaceAll("[^ -~]", ""), Duration.ofSeconds(120)).newBuilder().build(); + Retrofit retrofit = new Retrofit.Builder() + .baseUrl(apiHost) + .client(client) + .addConverterFactory(JacksonConverterFactory.create(mapper)) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build(); + OpenAiApi api = retrofit.create(OpenAiApi.class); + + service = new OpenAiService(api); + vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE); + + createImageSv = findViewById(R.id.create_image_sv); + imagePb = findViewById(R.id.image_pb); + errorTv = findViewById(R.id.error_image_tv); + retryBtn = findViewById(R.id.retry_image_btn); + imageView = findViewById(R.id.create_image_iv); + shareBtn = findViewById(R.id.share_image_btn); + expiresInTv = findViewById(R.id.expires_image_tv); + saveDiscardBtns = findViewById(R.id.save_discard_image_btns); + + prompt = getIntent().getStringExtra("net.devemperor.wristassist.prompt"); + model = sp.getBoolean("net.devemperor.wristassist.image_model", false) ? "dall-e-3" : "dall-e-2"; + quality = sp.getBoolean("net.devemperor.wristassist.image_quality", false) ? "hd" : "standard"; + style = sp.getBoolean("net.devemperor.wristassist.image_style", false) ? "natural" : "vivid"; + size = sp.getBoolean("net.devemperor.wristassist.image_model", false) ? "1024x1024" : sp.getString("net.devemperor.wristassist.image_size", "1024x1024"); + + createAndDownloadImage(); + createImageSv.requestFocus(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + timer.cancel(); + if (thread != null) { + thread.shutdownNow(); + } + } + + private void createAndDownloadImage() { + imagePb.setVisibility(View.VISIBLE); + errorTv.setVisibility(View.GONE); + retryBtn.setVisibility(View.GONE); + + thread = Executors.newSingleThreadExecutor(); + thread.execute(() -> { + try { + CreateImageRequest cir = CreateImageRequest.builder() + .responseFormat("url") + .n(1) + .prompt(prompt) + .model(model) + .quality(quality) + .size(size) + .style(style) + .build(); + imageResult = service.createImage(cir); + image = imageResult.getData().get(0); + + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + long minutes = (imageResult.getCreated()*1000 + 60*60*1000 - System.currentTimeMillis()) / 60 / 1000; + runOnUiThread(() -> { + if (minutes <= 0) { + expiresInTv.setVisibility(View.GONE); + shareBtn.setVisibility(View.GONE); + timer.cancel(); + } else { + expiresInTv.setText(getString(R.string.wristassist_image_expires_in, minutes)); + } + }); + } + }, 0, 60*1000); + + usageDatabaseHelper.edit(model, 1, Util.calcCostImage(model, quality, size)); + + OkHttpClient downloadClient = new OkHttpClient(); + Request request = new Request.Builder().url(image.getUrl()).build(); + + Response response = downloadClient.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new IOException("Unexpected code " + response); + } + + assert response.body() != null; + InputStream inputStream = response.body().byteStream(); + bitmap = BitmapFactory.decodeStream(inputStream); + if (bitmap == null) { + throw new IOException("Bitmap is null"); + } else { + runOnUiThread(() -> { + if (sp.getBoolean("net.devemperor.wristassist.vibrate", true)) { + vibrator.vibrate(VibrationEffect.createOneShot(300, VibrationEffect.DEFAULT_AMPLITUDE)); + } + + imageView.setImageBitmap(bitmap); + imagePb.setVisibility(View.GONE); + imageView.setVisibility(View.VISIBLE); + shareBtn.setVisibility(View.VISIBLE); + expiresInTv.setVisibility(View.VISIBLE); + saveDiscardBtns.setVisibility(View.VISIBLE); + }); + } + } catch (RuntimeException | IOException e) { + FirebaseCrashlytics fc = FirebaseCrashlytics.getInstance(); + fc.setCustomKey("settings", sp.getAll().toString()); + fc.setUserId(sp.getString("net.devemperor.wristassist.userid", "null")); + fc.recordException(e); + fc.sendUnsentReports(); + + e.printStackTrace(); + runOnUiThread(() -> { + imagePb.setVisibility(View.GONE); + errorTv.setVisibility(View.VISIBLE); + retryBtn.setVisibility(View.VISIBLE); + timer.cancel(); + + if (sp.getBoolean("net.devemperor.wristassist.vibrate", true)) { + vibrator.vibrate(VibrationEffect.createWaveform(new long[]{50, 50, 50, 50, 50}, new int[]{-1, 0, -1, 0, -1}, -1)); + } + + if (Objects.requireNonNull(e.getMessage()).contains("SocketTimeoutException")) { + errorTv.setText(R.string.wristassist_timeout); + } else if (e.getMessage().contains("API key")) { + errorTv.setText(getString(R.string.wristassist_invalid_api_key_message)); + } else if (e.getMessage().contains("rejected")) { + errorTv.setText(R.string.wristassist_image_request_rejected); + } else if (e.getMessage().contains("quota") || e.getMessage().contains("limit")) { + errorTv.setText(R.string.wristassist_quota_exceeded); + } else if (e.getMessage().contains("does not exist")) { + errorTv.setText(R.string.wristassist_no_access); + } else { + errorTv.setText(R.string.wristassist_no_internet); + } + }); + } + }); + } + + public void retry(View view) { + createAndDownloadImage(); + } + + public void shareImage(View view) { + Intent intent = new Intent(this, QRCodeActivity.class); + intent.putExtra("net.devemperor.wristassist.image_url", image.getUrl()); + startActivity(intent); + } + + public void saveImage(View view) { + ImageModel imageModel; + if (model.equals("dall-e-3")) { + imageModel = new ImageModel(-1, prompt, image.getRevisedPrompt(), model, quality, size, style, imageResult.getCreated() * 1000, image.getUrl()); + } else { + imageModel = new ImageModel(-1, prompt, null, model, null, size, null, imageResult.getCreated() * 1000, image.getUrl()); + } + int id = imagesDatabaseHelper.add(imageModel); + + try { + FileOutputStream out = openFileOutput("image_" + id + ".png", MODE_PRIVATE); + bitmap.compress(Bitmap.CompressFormat.PNG, 90, out); + out.flush(); + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + timer.cancel(); + + Intent data = new Intent(); + data.putExtra("net.devemperor.wristassist.imageId", id); + setResult(RESULT_OK, data); + finish(); + } + + public void discardImage(View view) { + timer.cancel(); + finish(); + } +} \ No newline at end of file diff --git a/app/src/main/java/net/devemperor/wristassist/activities/ImageActivity.java b/app/src/main/java/net/devemperor/wristassist/activities/ImageActivity.java new file mode 100644 index 0000000..2d00b86 --- /dev/null +++ b/app/src/main/java/net/devemperor/wristassist/activities/ImageActivity.java @@ -0,0 +1,95 @@ +package net.devemperor.wristassist.activities; + +import android.content.Intent; +import android.os.Bundle; +import android.view.MotionEvent; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityOptionsCompat; +import androidx.core.view.InputDeviceCompat; +import androidx.core.view.MotionEventCompat; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.wear.widget.WearableRecyclerView; + +import net.devemperor.wristassist.R; +import net.devemperor.wristassist.adapters.ImageAdapter; +import net.devemperor.wristassist.database.ImageModel; +import net.devemperor.wristassist.database.ImagesDatabaseHelper; + +import java.util.List; + +public class ImageActivity extends AppCompatActivity { + + ImagesDatabaseHelper imagesDatabaseHelper; + + WearableRecyclerView galleryWrv; + List imageData; + ImageAdapter imageAdapter; + + int currentOpenPosition = -1; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_image); + + imagesDatabaseHelper = new ImagesDatabaseHelper(this); + + galleryWrv = findViewById(R.id.gallery_wrv); + galleryWrv.setEdgeItemsCenteringEnabled(true); + galleryWrv.setHasFixedSize(true); + galleryWrv.setLayoutManager(new GridLayoutManager(this, 3)); + imageData = imagesDatabaseHelper.getAll(); + imageData.add(0, null); + + imageAdapter = new ImageAdapter(imageData, (menuPosition, image) -> { + if (menuPosition == 0) { + Intent intent = new Intent(this, InputActivity.class); + intent.putExtra("net.devemperor.wristassist.input.title", getString(R.string.wristassist_describe_image)); + intent.putExtra("net.devemperor.wristassist.input.hint", getString(R.string.wristassist_image_hint)); + intent.putExtra("net.devemperor.wristassist.input.hands_free", getSharedPreferences("net.devemperor.wristassist", MODE_PRIVATE) + .getBoolean("net.devemperor.wristassist.hands_free", false)); + startActivityForResult(intent, 1337); + } else { + currentOpenPosition = menuPosition; + Intent intent = new Intent(this, OpenImageActivity.class); + intent.putExtra("net.devemperor.wristassist.imageId", imageData.get(menuPosition).getId()); + ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, image, "image"); + startActivityForResult(intent, 1338, options.toBundle()); + } + }); + galleryWrv.setAdapter(imageAdapter); + + galleryWrv.requestFocus(); + galleryWrv.setOnGenericMotionListener((v, ev) -> { + if (ev.getAction() == MotionEvent.ACTION_SCROLL && ev.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER)) { + v.scrollBy(0, (int) (galleryWrv.getChildAt(0).getHeight() * -ev.getAxisValue(MotionEventCompat.AXIS_SCROLL))); + return true; + } + return false; + }); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode != RESULT_OK) return; + + if (requestCode == 1337) { + Intent intent = new Intent(this, CreateImageActivity.class); + intent.putExtra("net.devemperor.wristassist.prompt", data.getStringExtra("net.devemperor.wristassist.input.content")); + startActivityForResult(intent, 1339); + } + if (requestCode == 1338 && data.getBooleanExtra("net.devemperor.wristassist.input.image_deleted", false)) { + imageAdapter.getData().remove(currentOpenPosition); + imageAdapter.notifyItemRemoved(currentOpenPosition); + } + if (requestCode == 1339) { + int imageId = data.getIntExtra("net.devemperor.wristassist.imageId", -1); + if (imageId != -1) { + imageData.add(1, imagesDatabaseHelper.get(imageId)); + imageAdapter.notifyItemInserted(1); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/devemperor/wristassist/activities/MainActivity.java b/app/src/main/java/net/devemperor/wristassist/activities/MainActivity.java index 64f108f..a4d7941 100644 --- a/app/src/main/java/net/devemperor/wristassist/activities/MainActivity.java +++ b/app/src/main/java/net/devemperor/wristassist/activities/MainActivity.java @@ -5,6 +5,7 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.view.View; +import android.widget.ProgressBar; import androidx.core.splashscreen.SplashScreen; import androidx.wear.widget.WearableLinearLayoutManager; @@ -23,6 +24,7 @@ public class MainActivity extends Activity { WearableRecyclerView mainWrv; + ProgressBar mainPb; SharedPreferences sp; @Override @@ -54,6 +56,8 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_main); mainWrv = findViewById(R.id.main_wrv); + mainPb = findViewById(R.id.main_pb); + mainWrv.setHasFixedSize(true); mainWrv.setEdgeItemsCenteringEnabled(true); mainWrv.setLayoutManager(new WearableLinearLayoutManager(this)); @@ -61,6 +65,7 @@ protected void onCreate(Bundle savedInstanceState) { ArrayList menuItems = new ArrayList<>(); menuItems.add(new MainItem(R.drawable.twotone_add_24, getString(R.string.wristassist_menu_new_chat))); menuItems.add(new MainItem(R.drawable.twotone_chat_24, getString(R.string.wristassist_menu_saved_chats))); + menuItems.add(new MainItem(R.drawable.twotone_add_photo_alternate_24, getString(R.string.wristassist_menu_images))); menuItems.add(new MainItem(R.drawable.twotone_insert_chart_outlined_24, getString(R.string.wristassist_menu_usage))); menuItems.add(new MainItem(R.drawable.twotone_settings_24, getString(R.string.wristassist_menu_settings))); menuItems.add(new MainItem(R.drawable.twotone_info_24, getString(R.string.wristassist_menu_about))); @@ -75,12 +80,16 @@ protected void onCreate(Bundle savedInstanceState) { intent = new Intent(this, SavedChatsActivity.class); startActivity(intent); } else if (menuPosition == 2) { - intent = new Intent(this, UsageActivity.class); + intent = new Intent(this, ImageActivity.class); startActivity(intent); + mainPb.setVisibility(View.VISIBLE); } else if (menuPosition == 3) { - intent = new Intent(this, SettingsActivity.class); + intent = new Intent(this, UsageActivity.class); startActivity(intent); } else if (menuPosition == 4) { + intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + } else if (menuPosition == 5) { intent = new Intent(this, AboutActivity.class); startActivity(intent); } @@ -131,4 +140,10 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { sp.edit().putBoolean("net.devemperor.wristassist.onboarding_complete", true).apply(); } } + + @Override + protected void onResume() { + super.onResume(); + mainPb.setVisibility(View.GONE); + } } \ No newline at end of file diff --git a/app/src/main/java/net/devemperor/wristassist/activities/OpenImageActivity.java b/app/src/main/java/net/devemperor/wristassist/activities/OpenImageActivity.java new file mode 100644 index 0000000..2c0fd21 --- /dev/null +++ b/app/src/main/java/net/devemperor/wristassist/activities/OpenImageActivity.java @@ -0,0 +1,126 @@ +package net.devemperor.wristassist.activities; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ScrollView; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +import com.squareup.picasso.Picasso; + +import net.devemperor.wristassist.R; +import net.devemperor.wristassist.database.ImageModel; +import net.devemperor.wristassist.database.ImagesDatabaseHelper; +import net.devemperor.wristassist.util.Util; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.Timer; +import java.util.TimerTask; + +public class OpenImageActivity extends AppCompatActivity { + + int imageId; + ImagesDatabaseHelper imagesDatabaseHelper; + ImageModel imageModel; + Timer timer = new Timer(); + + ScrollView openImageSv; + ImageView imageView; + ImageButton shareBtn; + TextView expiresInTv; + TextView promptTv; + TextView revisedPromptTv; + TextView modelTv; + TextView qualityTv; + TextView sizeTv; + TextView styleTv; + TextView createdTv; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_open_image); + + imageId = getIntent().getIntExtra("net.devemperor.wristassist.imageId", -1); + imagesDatabaseHelper = new ImagesDatabaseHelper(this); + imageModel = imagesDatabaseHelper.get(imageId); + + openImageSv = findViewById(R.id.open_image_sv); + imageView = findViewById(R.id.open_image_iv); + shareBtn = findViewById(R.id.share_image_btn); + expiresInTv = findViewById(R.id.expires_image_tv); + promptTv = findViewById(R.id.open_image_prompt_tv); + revisedPromptTv = findViewById(R.id.open_image_revised_prompt_tv); + modelTv = findViewById(R.id.open_image_model_tv); + qualityTv = findViewById(R.id.open_image_quality_tv); + sizeTv = findViewById(R.id.open_image_size_tv); + styleTv = findViewById(R.id.open_image_style_tv); + createdTv = findViewById(R.id.open_image_created_tv); + + Picasso.get().load(new File(getFilesDir().getAbsolutePath() + "/image_" + imageModel.getId() + ".png")).into(imageView); + + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd\nHH:mm:ss", Locale.getDefault()); + promptTv.setText(imageModel.getPrompt()); + modelTv.setText(Util.translate(this, imageModel.getModel())); + createdTv.setText(formatter.format(imageModel.getCreated())); + sizeTv.setText(imageModel.getSize()); + + if (imageModel.getRevisedPrompt() != null && imageModel.getQuality() != null && imageModel.getStyle() != null) { + revisedPromptTv.setText(imageModel.getRevisedPrompt()); + qualityTv.setText(Util.translate(this, imageModel.getQuality())); + styleTv.setText(Util.translate(this, imageModel.getStyle())); + } else { + findViewById(R.id.open_image_revised_prompt_descriptor_tv).setVisibility(TextView.GONE); + revisedPromptTv.setVisibility(TextView.GONE); + findViewById(R.id.open_image_quality_descriptor_tv).setVisibility(TextView.GONE); + qualityTv.setVisibility(TextView.GONE); + findViewById(R.id.open_image_style_descriptor_tv).setVisibility(TextView.GONE); + styleTv.setVisibility(TextView.GONE); + } + + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + long minutes = (imageModel.getCreated() + 60*60*1000 - System.currentTimeMillis()) / 60 / 1000; + runOnUiThread(() -> { + if (minutes <= 0) { + expiresInTv.setVisibility(View.GONE); + shareBtn.setVisibility(View.GONE); + timer.cancel(); + } else { + expiresInTv.setText(getString(R.string.wristassist_image_expires_in, minutes)); + } + }); + } + }, 0, 60*1000); + + openImageSv.requestFocus(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + timer.cancel(); + } + + public void shareImage(View view) { + Intent intent = new Intent(this, QRCodeActivity.class); + intent.putExtra("net.devemperor.wristassist.image_url", imageModel.getUrl()); + startActivity(intent); + } + + public void deleteImage(View view) { + timer.cancel(); + imagesDatabaseHelper.delete(imageId); + Intent data = new Intent(); + data.putExtra("net.devemperor.wristassist.input.image_deleted", true); + setResult(RESULT_OK, data); + finish(); + } +} \ No newline at end of file diff --git a/app/src/main/java/net/devemperor/wristassist/activities/PreferencesFragment.java b/app/src/main/java/net/devemperor/wristassist/activities/PreferencesFragment.java index 64c5603..c6d2f7d 100644 --- a/app/src/main/java/net/devemperor/wristassist/activities/PreferencesFragment.java +++ b/app/src/main/java/net/devemperor/wristassist/activities/PreferencesFragment.java @@ -23,7 +23,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat { SharedPreferences sp; SwitchPreference customServerPreference; - ListPreference modelPreference; + ListPreference chatModelPreference; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -52,7 +52,7 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { if (!((Boolean) newValue)) { sp.edit().remove("net.devemperor.wristassist.custom_server_host").apply(); sp.edit().remove("net.devemperor.wristassist.custom_server_model").apply(); - modelPreference.setEnabled(true); + chatModelPreference.setEnabled(true); } else { Intent intent = new Intent(getContext(), InputActivity.class); intent.putExtra("net.devemperor.wristassist.input.title", getString(R.string.wristassist_custom_host)); @@ -65,9 +65,9 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { }); } - modelPreference = findPreference("net.devemperor.wristassist.model"); - if (modelPreference != null) modelPreference.setSummaryProvider(preference -> modelPreference.getEntry()); - if (customServerPreference.isChecked()) modelPreference.setEnabled(false); + chatModelPreference = findPreference("net.devemperor.wristassist.model"); + if (chatModelPreference != null) chatModelPreference.setSummaryProvider(preference -> chatModelPreference.getEntry()); + if (customServerPreference.isChecked()) chatModelPreference.setEnabled(false); ListPreference ttsPreference = findPreference("net.devemperor.wristassist.tts"); if (ttsPreference != null) { @@ -78,6 +78,50 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { }); ttsPreference.setSummaryProvider(preference -> ttsPreference.getEntry()); } + + SwitchPreference imageModelPreference = findPreference("net.devemperor.wristassist.image_model"); + SwitchPreference imageQualityPreference = findPreference("net.devemperor.wristassist.image_quality"); + SwitchPreference imageStylePreference = findPreference("net.devemperor.wristassist.image_style"); + ListPreference imageSizePreference = findPreference("net.devemperor.wristassist.image_size"); + if (imageModelPreference != null && imageQualityPreference != null && imageStylePreference != null && imageSizePreference != null) { + imageModelPreference.setOnPreferenceChangeListener((preference, newValue) -> { + if ((Boolean) newValue) { + imageModelPreference.setSummaryProvider(preference1 -> "Dall-E 3"); + imageQualityPreference.setEnabled(true); + imageStylePreference.setEnabled(true); + imageSizePreference.setEnabled(false); + + } else { + imageModelPreference.setSummaryProvider(preference1 -> "Dall-E 2"); + imageQualityPreference.setEnabled(false); + imageStylePreference.setEnabled(false); + imageSizePreference.setEnabled(true); + } + return true; + }); + + imageQualityPreference.setOnPreferenceChangeListener((preference, newValue) -> { + if ((Boolean) newValue) imageQualityPreference.setSummaryProvider(preference1 -> "HD"); + else imageQualityPreference.setSummaryProvider(preference1 -> "Standard"); + return true; + }); + + imageStylePreference.setOnPreferenceChangeListener((preference, newValue) -> { + if ((Boolean) newValue) imageStylePreference.setSummaryProvider(preference1 -> getString(R.string.wristassist_image_quality_natural)); + else imageStylePreference.setSummaryProvider(preference1 -> getString(R.string.wristassist_image_quality_vivid)); + return true; + }); + + if (imageModelPreference.isChecked()) { + imageModelPreference.setSummaryProvider(preference -> "Dall-E 3"); + imageQualityPreference.setEnabled(true); + imageStylePreference.setEnabled(true); + imageSizePreference.setEnabled(false); + } + if (imageQualityPreference.isChecked()) imageQualityPreference.setSummaryProvider(preference -> "HD"); + if (imageStylePreference.isChecked()) imageStylePreference.setSummaryProvider(preference -> getString(R.string.wristassist_image_quality_natural)); + imageSizePreference.setSummaryProvider(preference -> imageSizePreference.getEntry()); + } } @Override @@ -90,16 +134,16 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { if (new UrlValidator().isValid(host)) { sp.edit().putString("net.devemperor.wristassist.custom_server_host", host).apply(); sp.edit().putString("net.devemperor.wristassist.custom_server_model", model).apply(); - modelPreference.setEnabled(false); + chatModelPreference.setEnabled(false); } else { customServerPreference.setChecked(false); - modelPreference.setEnabled(true); + chatModelPreference.setEnabled(true); Toast.makeText(getContext(), R.string.wristassist_invalid_host, Toast.LENGTH_SHORT).show(); } } } else if (resultCode == Activity.RESULT_CANCELED && requestCode == 1337) { customServerPreference.setChecked(false); - modelPreference.setEnabled(true); + chatModelPreference.setEnabled(true); } } } diff --git a/app/src/main/java/net/devemperor/wristassist/activities/QRCodeActivity.java b/app/src/main/java/net/devemperor/wristassist/activities/QRCodeActivity.java new file mode 100644 index 0000000..35fdcd7 --- /dev/null +++ b/app/src/main/java/net/devemperor/wristassist/activities/QRCodeActivity.java @@ -0,0 +1,31 @@ +package net.devemperor.wristassist.activities; + +import android.graphics.Bitmap; +import android.os.Bundle; +import android.widget.ImageView; + +import androidx.appcompat.app.AppCompatActivity; + +import net.devemperor.wristassist.R; +import net.glxn.qrgen.android.QRCode; + +public class QRCodeActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_qrcode); + + String imageUrl = getIntent().getStringExtra("net.devemperor.wristassist.image_url"); + + ImageView qrCodeIv = findViewById(R.id.qrcode_iv); + + Bitmap code = QRCode.from(imageUrl) + .withSize(256, 256) + .withColor(getColor(R.color.wristassist_purple), getColor(R.color.wristassist_black)) + .bitmap(); + + code = Bitmap.createBitmap(code, 12, 12, code.getWidth() - 24, code.getHeight() - 24); + qrCodeIv.setImageBitmap(code); + } +} \ No newline at end of file diff --git a/app/src/main/java/net/devemperor/wristassist/adapters/ImageAdapter.java b/app/src/main/java/net/devemperor/wristassist/adapters/ImageAdapter.java new file mode 100644 index 0000000..b3939f4 --- /dev/null +++ b/app/src/main/java/net/devemperor/wristassist/adapters/ImageAdapter.java @@ -0,0 +1,72 @@ +package net.devemperor.wristassist.adapters; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.jsibbold.zoomage.ZoomageView; +import com.squareup.picasso.Picasso; + +import net.devemperor.wristassist.R; +import net.devemperor.wristassist.database.ImageModel; + +import java.io.File; +import java.util.List; + +public class ImageAdapter extends RecyclerView.Adapter { + + private final List data; + private final AdapterCallback callback; + + public interface AdapterCallback { + void onItemClicked(Integer menuPosition, ZoomageView image); + } + + public ImageAdapter(List data, AdapterCallback callback) { + this.data = data; + this.callback = callback; + } + + public static class RecyclerViewHolder extends RecyclerView.ViewHolder { + final ZoomageView image; + + public RecyclerViewHolder(View view) { + super(view); + image = view.findViewById(R.id.open_image_iv); + } + } + + @NonNull + @Override + public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_gallery, parent, false); + return new RecyclerViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerViewHolder holder, final int position) { + if (position == 0) { + holder.image.setImageResource(R.drawable.add_image); + } else { + Picasso.get().load(new File(holder.image.getContext().getFilesDir().getAbsolutePath() + + "/image_" + data.get(position).getId() + ".png")).into(holder.image); + } + holder.image.setOnClickListener(v -> { + if (callback != null) { + callback.onItemClicked(holder.getAdapterPosition(), holder.image); + } + }); + } + + @Override + public int getItemCount() { + return data.size(); + } + + public List getData() { + return data; + } +} diff --git a/app/src/main/java/net/devemperor/wristassist/adapters/UsageAdapter.java b/app/src/main/java/net/devemperor/wristassist/adapters/UsageAdapter.java index b3c4ae8..eb084e3 100644 --- a/app/src/main/java/net/devemperor/wristassist/adapters/UsageAdapter.java +++ b/app/src/main/java/net/devemperor/wristassist/adapters/UsageAdapter.java @@ -36,12 +36,17 @@ public View getView (int position, View convertView, @NonNull ViewGroup parent) UsageModel dataProvider = objects.get(position); TextView modelNameTv = listItem.findViewById(R.id.usage_model_tv); - modelNameTv.setText(Util.translateModelNames(dataProvider.getModelName())); + modelNameTv.setText(Util.translate(context, dataProvider.getModelName())); modelNameTv.setTextSize(18 * Util.getFontMultiplier(context)); TextView tokensTv = listItem.findViewById(R.id.usage_tokens_tv); - tokensTv.setText(context.getString(R.string.wristassist_token_usage, - String.format(Locale.getDefault(), "%,d", dataProvider.getTokens()))); + if (dataProvider.getModelName().startsWith("gpt")) { + tokensTv.setText(context.getString(R.string.wristassist_token_usage, + String.format(Locale.getDefault(), "%,d", dataProvider.getTokens()))); + } else { + tokensTv.setText(context.getString(R.string.wristassist_images_count, + String.format(Locale.getDefault(), "%,d", dataProvider.getTokens()))); + } tokensTv.setTextSize(16 * Util.getFontMultiplier(context)); TextView costTv = listItem.findViewById(R.id.usage_cost_tv); diff --git a/app/src/main/java/net/devemperor/wristassist/database/ImageModel.java b/app/src/main/java/net/devemperor/wristassist/database/ImageModel.java new file mode 100644 index 0000000..e76bf89 --- /dev/null +++ b/app/src/main/java/net/devemperor/wristassist/database/ImageModel.java @@ -0,0 +1,61 @@ +package net.devemperor.wristassist.database; + +public class ImageModel { + int id; + String prompt; + String revisedPrompt; + String model; + String quality; + String size; + String style; + long created; + String url; + + public ImageModel(int id, String prompt, String revisedPrompt, String model, String quality, String size, String style, long created, String url) { + this.id = id; + this.prompt = prompt; + this.revisedPrompt = revisedPrompt; + this.model = model; + this.quality = quality; + this.size = size; + this.style = style; + this.created = created; + this.url = url; + } + + public int getId() { + return id; + } + + public String getPrompt() { + return prompt; + } + + public String getRevisedPrompt() { + return revisedPrompt; + } + + public String getModel() { + return model; + } + + public String getQuality() { + return quality; + } + + public String getSize() { + return size; + } + + public String getStyle() { + return style; + } + + public long getCreated() { + return created; + } + + public String getUrl() { + return url; + } +} diff --git a/app/src/main/java/net/devemperor/wristassist/database/ImagesDatabaseHelper.java b/app/src/main/java/net/devemperor/wristassist/database/ImagesDatabaseHelper.java new file mode 100644 index 0000000..e643fb4 --- /dev/null +++ b/app/src/main/java/net/devemperor/wristassist/database/ImagesDatabaseHelper.java @@ -0,0 +1,88 @@ +package net.devemperor.wristassist.database; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import androidx.annotation.Nullable; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class ImagesDatabaseHelper extends SQLiteOpenHelper { + + Context context; + + public ImagesDatabaseHelper(@Nullable Context context) { + super(context, "images.db", null, 1); + this.context = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE IMAGES (ID INTEGER PRIMARY KEY AUTOINCREMENT, " + + "PROMPT TEXT, REVISED_PROMPT TEXT, MODEL TEXT, QUALITY TEXT, SIZE TEXT, STYLE TEXT, CREATED LONG, URL TEXT)"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } + + public int add(ImageModel imageModel) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues cv = new ContentValues(); + cv.put("PROMPT", imageModel.getPrompt()); + cv.put("REVISED_PROMPT", imageModel.getRevisedPrompt()); + cv.put("MODEL", imageModel.getModel()); + cv.put("QUALITY", imageModel.getQuality()); + cv.put("SIZE", imageModel.getSize()); + cv.put("STYLE", imageModel.getStyle()); + cv.put("CREATED", imageModel.getCreated()); + cv.put("URL", imageModel.getUrl()); + + return (int) db.insert("IMAGES", null, cv); + } + + public void delete(int id) { + SQLiteDatabase db = this.getWritableDatabase(); + db.delete("IMAGES", "ID=" + id, null); + db.close(); + + String filePath = context.getFilesDir().getAbsolutePath() + "/image_" + id + ".png"; + new File(filePath).delete(); + } + + public List getAll() { + SQLiteDatabase db = this.getWritableDatabase(); + Cursor cursor = db.rawQuery("SELECT * FROM IMAGES ORDER BY CREATED DESC", null); + + List models = new ArrayList<>(); + if (cursor.moveToFirst()) { + do { + models.add(new ImageModel(cursor.getInt(0), cursor.getString(1), + cursor.getString(2), cursor.getString(3), cursor.getString(4), + cursor.getString(5), cursor.getString(6), cursor.getLong(7), cursor.getString(8))); + } while (cursor.moveToNext()); + } + cursor.close(); + db.close(); + return models; + } + + public ImageModel get(long id) { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.rawQuery("SELECT * FROM IMAGES WHERE ID=" + id, null); + + ImageModel model = null; + if (cursor.moveToFirst()) { + model = new ImageModel(cursor.getInt(0), cursor.getString(1), + cursor.getString(2), cursor.getString(3), cursor.getString(4), + cursor.getString(5), cursor.getString(6), cursor.getLong(7), cursor.getString(8)); + } + cursor.close(); + db.close(); + return model; + } +} diff --git a/app/src/main/java/net/devemperor/wristassist/util/Util.java b/app/src/main/java/net/devemperor/wristassist/util/Util.java index 7261a35..5897e3d 100644 --- a/app/src/main/java/net/devemperor/wristassist/util/Util.java +++ b/app/src/main/java/net/devemperor/wristassist/util/Util.java @@ -6,6 +6,8 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import net.devemperor.wristassist.R; + public class Util { public static Bitmap drawableToBitmap(Drawable drawable) { @@ -33,7 +35,7 @@ public static float getFontMultiplier(Context context) { } } - public static double calcCost(String model, long promptTokens, long completionTokens) { + public static double calcCostChat(String model, long promptTokens, long completionTokens) { double inputPrice = 0; double outputPrice = 0; switch (model) { @@ -57,7 +59,29 @@ public static double calcCost(String model, long promptTokens, long completionTo return (inputPrice * promptTokens / 1000) + (outputPrice * completionTokens / 1000); } - public static String translateModelNames(String origin) { + public static double calcCostImage(String model, String quality, String size) { + switch (model) { + case "dall-e-3": + switch (quality) { + case "hd": + return 0.08; + case "standard": + return 0.04; + } + case "dall-e-2": + switch (size) { + case "1024x1024": + return 0.02; + case "512x512": + return 0.018; + case "256x256": + return 0.016; + } + } + return 0; + } + + public static String translate(Context context, String origin) { switch (origin) { case "gpt-3.5-turbo": return "GPT-3.5 Turbo"; @@ -67,6 +91,18 @@ public static String translateModelNames(String origin) { return "GPT-4"; case "gpt-4-32k": return "GPT-4 32K"; + case "dall-e-3": + return "Dall-E 3"; + case "dall-e-2": + return "Dall-E 2"; + case "hd": + return "HD"; + case "standard": + return "Standard"; + case "natural": + return context.getString(R.string.wristassist_image_quality_natural); + case "vivid": + return context.getString(R.string.wristassist_image_quality_vivid); default: return origin; } diff --git a/app/src/main/res/drawable/add_image.png b/app/src/main/res/drawable/add_image.png new file mode 100644 index 0000000000000000000000000000000000000000..1d6d1487357759a8eda9ca96f063d601959a603e GIT binary patch literal 9105 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/twotone_add_photo_alternate_24.xml b/app/src/main/res/drawable/twotone_add_photo_alternate_24.xml new file mode 100644 index 0000000..8e42921 --- /dev/null +++ b/app/src/main/res/drawable/twotone_add_photo_alternate_24.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/drawable/twotone_replay_24.xml b/app/src/main/res/drawable/twotone_replay_24.xml new file mode 100644 index 0000000..9389efa --- /dev/null +++ b/app/src/main/res/drawable/twotone_replay_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/twotone_share_24.xml b/app/src/main/res/drawable/twotone_share_24.xml new file mode 100644 index 0000000..cf7eeef --- /dev/null +++ b/app/src/main/res/drawable/twotone_share_24.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_create_image.xml b/app/src/main/res/layout/activity_create_image.xml new file mode 100644 index 0000000..3b22213 --- /dev/null +++ b/app/src/main/res/layout/activity_create_image.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_image.xml b/app/src/main/res/layout/activity_image.xml new file mode 100644 index 0000000..9a8fc60 --- /dev/null +++ b/app/src/main/res/layout/activity_image.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 69e240b..020d909 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,6 +1,25 @@ - \ No newline at end of file + xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_open_image.xml b/app/src/main/res/layout/activity_open_image.xml new file mode 100644 index 0000000..f3f7676 --- /dev/null +++ b/app/src/main/res/layout/activity_open_image.xml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_qrcode.xml b/app/src/main/res/layout/activity_qrcode.xml new file mode 100644 index 0000000..953b145 --- /dev/null +++ b/app/src/main/res/layout/activity_qrcode.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_gallery.xml b/app/src/main/res/layout/item_gallery.xml new file mode 100644 index 0000000..31db3f7 --- /dev/null +++ b/app/src/main/res/layout/item_gallery.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index d6ae423..3874823 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -13,13 +13,13 @@ Vibrieren Schriftgröße Verbrauch anzeigen - KI Model wechseln + Chat-KI Modell OK Abbrechen Klicke zum Anzeigen Willkommen bei\nWristAssist Bitte scanne den QR-Code, um Anweisungen zur Einrichtung und Nutzung von WristAssist zu erhalten. - KI Einstellungen + API Einstellungen Chat Einstellungen TTS Einstellungen TTS @@ -29,10 +29,10 @@ Chat zurückgesetzt Chat gelöscht Chat Titel ändern - Gib eine Anweisung ein - Anweisung - Gib eine System-Anweisung an - System-Anweisung + Anweisung: + Schreibe einen Artikel … + System-Anweisung: + Sprich wie Yoda API Schlüssel festlegen API Schlüssel Chat Titel festlegen @@ -46,7 +46,7 @@ Keine Tastatur mit Spracherkennung installiert Freisprech Eingabe Eigener Host - Eigenes Model + Eigenes Modell Eigener API Server Ungültiges URL-Format KI @@ -65,4 +65,26 @@ Kein Verbrauch bisher Direkte Eingabe ### Version 2.7.0 \n#### Aktivität zum Überprüfen des Verbrauchs \nAktivität hinzugefügt, um die Nutzung der einzelnen Modelle zu überprüfen. \n#### Direkte Eingabe \nEinstellung zum sofortigen Starten eines neuen Chats hinzugefügt. \n#### Mehrere Fehlerbehebungen \n + Bilder + Beschreibe das Bild + Ein süßer Hund mit einem Hut + Zuletzt bearbeitet: + Token Verbrauch: + Bilder Einstellungen + Bilder-KI Modell + Bild Qualität + Bild Größe + Bild Stil + Lebendig + Natürlich + Beschreibung: + Überarbeitete Beschreibung: + Modell: + Qualität: + Größe: + Stil: + Erstellt: + Anzahl an Bildern: %1$s + Geteilter Link läuft in %1$d Minuten ab + Die Anfrage wurde abgelehnt. Deine Anfrage enthält möglicherweise Text, der nicht erlaubt ist. \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 4a933dc..27d1061 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1,18 +1,29 @@ - + GPT-3.5 Turbo GPT-4 Turbo GPT-4 GPT-4 32K - + gpt-3.5-turbo gpt-4-turbo-preview gpt-4 gpt-4-32k + + 256x256 + 512x512 + 1024x1024 + + + 256x256 + 512x512 + 1024x1024 + + @string/wristassist_tts_off @string/wristassist_tts_on diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ecdd077..9cf7165 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,7 +12,7 @@ Reset chat You haven\'t saved any chats yet.\nTouch the Save-Button in a new chat to save. - Tokens:\n%1$s k + Token usage: Deleted chat Change API key @@ -20,8 +20,8 @@ Vibrate Font size Show cost - Select AI model - AI settings + Chat-AI model + API settings Chat settings TTS settings TTS @@ -37,10 +37,10 @@ Welcome to\nWristAssist Please scan the QR code for instructions on how to set up and use WristAssist. Edit chat title - Enter a prompt - Prompt - Enter a system prompt - System prompt + Prompt: + Write an article … + System prompt: + Talk like Yoda Set API key API key Set chat title @@ -75,4 +75,25 @@ No usage yet Instant input ### Version 2.7.0 \n#### Added usage activity \nAdded activity to check the usage of the individual models. \n#### Instant input \nAdded setting to start a new chat immediately. \n#### Several bugfixes \n + Images + Describe the image + A cute dog with a hat + Last modified: + Images settings + Images-AI model + Image quality + Image size + Image style + Vivid + Natural + Prompt: + Revised prompt: + Model: + Quality: + Size: + Style: + Created: + Number of images: %1$s + Shared link will expire in %1$d minutes + The request was rejected. Your prompt may contain text that is not allowed. \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 27aa219..df50457 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -17,6 +17,8 @@ @style/Theme.AlertDialog @style/Theme.AlertDialog @style/Theme.AlertDialog + + true