Consolidate docs and finalize APK companion updates
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -22,22 +28,19 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" android:host="dewemoji.com" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" android:host="www.dewemoji.com" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".OverlayBubbleService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:stopWithTask="false">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Floating overlay bubble for quick emoji search and copy" />
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
@@ -48,8 +51,4 @@
|
||||
android:resource="@xml/file_paths"></meta-data>
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
|
||||
import com.dewemoji.app.plugins.DewemojiOverlayPlugin;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
import org.json.JSONObject;
|
||||
@@ -30,24 +31,44 @@ import java.nio.charset.StandardCharsets;
|
||||
public class MainActivity extends BridgeActivity {
|
||||
private static final String TAG = "DewemojiUpdater";
|
||||
private static final String VERSION_URL = "https://dewemoji.com/downloads/version.json";
|
||||
private static final String EXTRA_OPENED_FROM_BUBBLE = "dewemoji_opened_from_bubble";
|
||||
private static final int CONNECT_TIMEOUT_MS = 10000;
|
||||
private static final int READ_TIMEOUT_MS = 15000;
|
||||
private boolean openedFromBubble = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
registerPlugin(DewemojiOverlayPlugin.class);
|
||||
super.onCreate(savedInstanceState);
|
||||
hideSystemBars();
|
||||
openedFromBubble = wasOpenedFromBubble(getIntent());
|
||||
applySystemBarMode();
|
||||
getWindow().getDecorView().post(this::applySystemBarMode);
|
||||
checkForUpdates(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
openedFromBubble = wasOpenedFromBubble(intent);
|
||||
applySystemBarMode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
if (hasFocus) {
|
||||
hideSystemBars();
|
||||
applySystemBarMode();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
applySystemBarMode();
|
||||
getWindow().getDecorView().post(this::applySystemBarMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
WebView webView = getBridge() != null ? getBridge().getWebView() : null;
|
||||
@@ -80,6 +101,20 @@ public class MainActivity extends BridgeActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean wasOpenedFromBubble(Intent intent) {
|
||||
return intent != null && intent.getBooleanExtra(EXTRA_OPENED_FROM_BUBBLE, false);
|
||||
}
|
||||
|
||||
private void applySystemBarMode() {
|
||||
// Fallback to non-immersive mode whenever the overlay bubble service is active;
|
||||
// some launch paths may not preserve the intent extra on all OEMs.
|
||||
if (openedFromBubble || OverlayBubbleService.isRunning()) {
|
||||
showSystemBars();
|
||||
return;
|
||||
}
|
||||
hideSystemBars();
|
||||
}
|
||||
|
||||
private void hideSystemBars() {
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
WindowInsetsControllerCompat controller =
|
||||
@@ -90,6 +125,13 @@ public class MainActivity extends BridgeActivity {
|
||||
);
|
||||
}
|
||||
|
||||
private void showSystemBars() {
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), true);
|
||||
WindowInsetsControllerCompat controller =
|
||||
new WindowInsetsControllerCompat(getWindow(), getWindow().getDecorView());
|
||||
controller.show(WindowInsetsCompat.Type.statusBars() | WindowInsetsCompat.Type.navigationBars());
|
||||
}
|
||||
|
||||
private void checkForUpdates(boolean manual) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,715 @@
|
||||
package com.dewemoji.app;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
public class OverlayBubbleService extends Service {
|
||||
private static final String TAG = "DewemojiBubbleService";
|
||||
public static final String ACTION_START = "com.dewemoji.app.action.BUBBLE_START";
|
||||
public static final String ACTION_STOP = "com.dewemoji.app.action.BUBBLE_STOP";
|
||||
public static final String ACTION_OPEN = "com.dewemoji.app.action.BUBBLE_OPEN";
|
||||
|
||||
public static final String PREFS_NAME = "dewemoji_native_state";
|
||||
public static final String PREF_BUBBLE_X = "bubblePositionX";
|
||||
public static final String PREF_BUBBLE_Y = "bubblePositionY";
|
||||
public static final String NOTIFICATION_CHANNEL_ID = "dewemoji_bubble";
|
||||
|
||||
private static final int NOTIFICATION_ID = 41001;
|
||||
private static volatile boolean running = false;
|
||||
|
||||
private WindowManager windowManager;
|
||||
private ImageView bubbleView;
|
||||
private View panelBackdropView;
|
||||
private View panelView;
|
||||
private WebView panelWebView;
|
||||
private WindowManager.LayoutParams bubbleParams;
|
||||
private WindowManager.LayoutParams panelBackdropParams;
|
||||
private WindowManager.LayoutParams panelParams;
|
||||
private SharedPreferences prefs;
|
||||
|
||||
public static boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
|
||||
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
ensureNotificationChannel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
final String action = intent != null ? intent.getAction() : null;
|
||||
|
||||
if (ACTION_STOP.equals(action)) {
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
if (ACTION_OPEN.equals(action)) {
|
||||
openMainActivity();
|
||||
if (running) {
|
||||
startForegroundCompat();
|
||||
return START_STICKY;
|
||||
}
|
||||
}
|
||||
|
||||
if (!canDrawOverlays()) {
|
||||
Toast.makeText(this, "Overlay permission is required for Dewemoji bubble", Toast.LENGTH_SHORT).show();
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
try {
|
||||
startForegroundCompat();
|
||||
ensureBubbleView();
|
||||
// `isAttachedToWindow()` can be false immediately after addView on some
|
||||
// devices/OEM builds even though the overlay is successfully added.
|
||||
running = bubbleView != null;
|
||||
if (!running) {
|
||||
Toast.makeText(this, "Could not show Dewemoji bubble", Toast.LENGTH_SHORT).show();
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
return START_STICKY;
|
||||
} catch (Exception ex) {
|
||||
Log.e(TAG, "Failed to start bubble service", ex);
|
||||
Toast.makeText(this, "Failed to start Dewemoji bubble", Toast.LENGTH_SHORT).show();
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
running = false;
|
||||
removePanelView();
|
||||
removeBubbleView();
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean canDrawOverlays() {
|
||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(this);
|
||||
}
|
||||
|
||||
private void ensureNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (manager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationChannel channel = manager.getNotificationChannel(NOTIFICATION_CHANNEL_ID);
|
||||
if (channel != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
channel = new NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID,
|
||||
"Dewemoji Bubble",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
channel.setDescription("Quick access bubble for search and copy");
|
||||
manager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
private Notification buildNotification() {
|
||||
PendingIntent openPendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
1001,
|
||||
buildMainActivityIntent(),
|
||||
pendingIntentFlags(true)
|
||||
);
|
||||
|
||||
Intent stopIntent = new Intent(this, OverlayBubbleService.class);
|
||||
stopIntent.setAction(ACTION_STOP);
|
||||
PendingIntent stopPendingIntent = PendingIntent.getService(
|
||||
this,
|
||||
1002,
|
||||
stopIntent,
|
||||
pendingIntentFlags(false)
|
||||
);
|
||||
|
||||
return new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle("Dewemoji bubble is running")
|
||||
.setContentText("Tap bubble to open Dewemoji, copy emoji, then paste in your other app")
|
||||
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
||||
.setContentIntent(openPendingIntent)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.addAction(android.R.drawable.ic_menu_view, "Open Dewemoji", openPendingIntent)
|
||||
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop Bubble", stopPendingIntent)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void startForegroundCompat() {
|
||||
Notification notification = buildNotification();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
);
|
||||
return;
|
||||
}
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
}
|
||||
|
||||
private int pendingIntentFlags(boolean updateCurrent) {
|
||||
int flags = updateCurrent ? PendingIntent.FLAG_UPDATE_CURRENT : 0;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
flags |= PendingIntent.FLAG_IMMUTABLE;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
private Intent buildMainActivityIntent() {
|
||||
Intent launch = new Intent(this, MainActivity.class);
|
||||
launch.setAction(Intent.ACTION_MAIN);
|
||||
launch.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
launch.putExtra("dewemoji_opened_from_bubble", true);
|
||||
return launch;
|
||||
}
|
||||
|
||||
private void openMainActivity() {
|
||||
try {
|
||||
hidePanelView();
|
||||
startActivity(buildMainActivityIntent());
|
||||
} catch (Exception ex) {
|
||||
Toast.makeText(this, "Could not open Dewemoji", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureBubbleView() {
|
||||
if (bubbleView != null && bubbleView.isAttachedToWindow()) {
|
||||
return;
|
||||
}
|
||||
if (windowManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
bubbleView = new ImageView(this);
|
||||
bubbleView.setImageResource(R.drawable.dewemoji_bubble_mark);
|
||||
bubbleView.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
int padding = dp(3);
|
||||
bubbleView.setPadding(padding, padding, padding, padding);
|
||||
|
||||
GradientDrawable background = new GradientDrawable();
|
||||
background.setShape(GradientDrawable.OVAL);
|
||||
background.setColor(0xFFFFFFFF);
|
||||
background.setStroke(dp(1), 0x22000000);
|
||||
bubbleView.setBackground(background);
|
||||
bubbleView.setElevation(dp(6));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
bubbleView.setClipToOutline(true);
|
||||
}
|
||||
|
||||
int type = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
||||
: WindowManager.LayoutParams.TYPE_PHONE;
|
||||
|
||||
bubbleParams = new WindowManager.LayoutParams(
|
||||
dp(64),
|
||||
dp(64),
|
||||
type,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
|
||||
PixelFormat.TRANSLUCENT
|
||||
);
|
||||
bubbleParams.gravity = Gravity.TOP | Gravity.START;
|
||||
bubbleParams.x = prefs.getInt(PREF_BUBBLE_X, dp(10));
|
||||
bubbleParams.y = prefs.getInt(PREF_BUBBLE_Y, dp(180));
|
||||
clampBubblePosition();
|
||||
|
||||
bubbleView.setOnTouchListener(new View.OnTouchListener() {
|
||||
private int startX;
|
||||
private int startY;
|
||||
private float downRawX;
|
||||
private float downRawY;
|
||||
private long downTime;
|
||||
private boolean moved;
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if (bubbleParams == null || windowManager == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
startX = bubbleParams.x;
|
||||
startY = bubbleParams.y;
|
||||
downRawX = event.getRawX();
|
||||
downRawY = event.getRawY();
|
||||
downTime = System.currentTimeMillis();
|
||||
moved = false;
|
||||
return true;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
int dx = Math.round(event.getRawX() - downRawX);
|
||||
int dy = Math.round(event.getRawY() - downRawY);
|
||||
if (Math.abs(dx) > dp(3) || Math.abs(dy) > dp(3)) {
|
||||
moved = true;
|
||||
}
|
||||
bubbleParams.x = startX + dx;
|
||||
bubbleParams.y = startY + dy;
|
||||
clampBubblePosition();
|
||||
try {
|
||||
windowManager.updateViewLayout(bubbleView, bubbleParams);
|
||||
} catch (Exception ignored) {}
|
||||
return true;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
persistBubblePosition();
|
||||
long pressDuration = System.currentTimeMillis() - downTime;
|
||||
if (!moved && pressDuration < 325) {
|
||||
togglePanelView();
|
||||
} else if (moved) {
|
||||
positionPanelNearBubble(false);
|
||||
try {
|
||||
if (panelView != null && panelView.isAttachedToWindow()) {
|
||||
windowManager.updateViewLayout(panelView, panelParams);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
windowManager.addView(bubbleView, bubbleParams);
|
||||
} catch (Exception ex) {
|
||||
Log.e(TAG, "windowManager.addView failed", ex);
|
||||
String reason = ex.getClass().getSimpleName();
|
||||
String message = ex.getMessage();
|
||||
if (message != null && !message.trim().isEmpty()) {
|
||||
String oneLine = message.replace('\n', ' ').trim();
|
||||
if (oneLine.length() > 90) oneLine = oneLine.substring(0, 90) + "…";
|
||||
reason = reason + ": " + oneLine;
|
||||
}
|
||||
Toast.makeText(this, reason, Toast.LENGTH_LONG).show();
|
||||
bubbleView = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void persistBubblePosition() {
|
||||
if (bubbleParams == null || prefs == null) {
|
||||
return;
|
||||
}
|
||||
prefs.edit()
|
||||
.putInt(PREF_BUBBLE_X, bubbleParams.x)
|
||||
.putInt(PREF_BUBBLE_Y, bubbleParams.y)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private void togglePanelView() {
|
||||
if (panelView != null && panelView.isAttachedToWindow()) {
|
||||
hidePanelView();
|
||||
return;
|
||||
}
|
||||
showPanelView();
|
||||
}
|
||||
|
||||
private void showPanelView() {
|
||||
if (windowManager == null) return;
|
||||
ensurePanelBackdropView();
|
||||
ensurePanelView();
|
||||
if (panelView == null || panelParams == null) return;
|
||||
|
||||
showPanelBackdrop();
|
||||
positionPanelNearBubble(true);
|
||||
try {
|
||||
if (panelView.isAttachedToWindow()) {
|
||||
windowManager.updateViewLayout(panelView, panelParams);
|
||||
} else {
|
||||
windowManager.addView(panelView, panelParams);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Log.e(TAG, "windowManager.addView(panel) failed", ex);
|
||||
Toast.makeText(this, "Failed to show panel", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void hidePanelView() {
|
||||
if (windowManager != null && panelView != null) {
|
||||
try {
|
||||
if (panelView.isAttachedToWindow()) {
|
||||
windowManager.removeView(panelView);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
hidePanelBackdrop();
|
||||
}
|
||||
|
||||
private void removePanelView() {
|
||||
hidePanelView();
|
||||
if (panelWebView != null) {
|
||||
try {
|
||||
panelWebView.stopLoading();
|
||||
panelWebView.loadUrl("about:blank");
|
||||
panelWebView.destroy();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
panelWebView = null;
|
||||
panelView = null;
|
||||
panelParams = null;
|
||||
panelBackdropView = null;
|
||||
panelBackdropParams = null;
|
||||
}
|
||||
|
||||
private void ensurePanelBackdropView() {
|
||||
if (panelBackdropView != null && panelBackdropParams != null) return;
|
||||
if (windowManager == null) return;
|
||||
|
||||
View backdrop = new View(this);
|
||||
backdrop.setBackgroundColor(0x26000000);
|
||||
backdrop.setClickable(true);
|
||||
backdrop.setFocusable(false);
|
||||
backdrop.setOnTouchListener((v, event) -> {
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||
hidePanelView();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
panelBackdropView = backdrop;
|
||||
|
||||
int type = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
||||
: WindowManager.LayoutParams.TYPE_PHONE;
|
||||
|
||||
panelBackdropParams = new WindowManager.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
type,
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT
|
||||
);
|
||||
panelBackdropParams.gravity = Gravity.TOP | Gravity.START;
|
||||
panelBackdropParams.x = 0;
|
||||
panelBackdropParams.y = 0;
|
||||
}
|
||||
|
||||
private void showPanelBackdrop() {
|
||||
if (windowManager == null || panelBackdropView == null || panelBackdropParams == null) return;
|
||||
try {
|
||||
if (panelBackdropView.isAttachedToWindow()) {
|
||||
windowManager.updateViewLayout(panelBackdropView, panelBackdropParams);
|
||||
} else {
|
||||
windowManager.addView(panelBackdropView, panelBackdropParams);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Log.w(TAG, "Failed to show panel backdrop", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void hidePanelBackdrop() {
|
||||
if (windowManager == null || panelBackdropView == null) return;
|
||||
try {
|
||||
if (panelBackdropView.isAttachedToWindow()) {
|
||||
windowManager.removeView(panelBackdropView);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void ensurePanelView() {
|
||||
if (panelView != null && panelParams != null) return;
|
||||
if (windowManager == null) return;
|
||||
|
||||
FrameLayout card = new FrameLayout(this);
|
||||
card.setClickable(true);
|
||||
card.setFocusable(true);
|
||||
card.setFocusableInTouchMode(true);
|
||||
card.setElevation(dp(18));
|
||||
|
||||
GradientDrawable bg = new GradientDrawable();
|
||||
bg.setColor(0xFFFFFFFF);
|
||||
bg.setCornerRadius(dp(18));
|
||||
bg.setStroke(dp(1), 0x22000000);
|
||||
card.setBackground(bg);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
card.setClipToOutline(true);
|
||||
}
|
||||
|
||||
WebView webView = new WebView(this);
|
||||
webView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||
webView.setVerticalScrollBarEnabled(false);
|
||||
webView.setHorizontalScrollBarEnabled(false);
|
||||
webView.setBackgroundColor(0xFFFFFFFF);
|
||||
webView.setWebChromeClient(new WebChromeClient());
|
||||
webView.setWebViewClient(new WebViewClient());
|
||||
webView.addJavascriptInterface(new OverlayWebBridge(), "DewemojiOverlayHost");
|
||||
|
||||
WebSettings ws = webView.getSettings();
|
||||
ws.setJavaScriptEnabled(true);
|
||||
ws.setDomStorageEnabled(true);
|
||||
ws.setAllowFileAccess(true);
|
||||
ws.setAllowContentAccess(true);
|
||||
ws.setMediaPlaybackRequiresUserGesture(false);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
ws.setAllowFileAccessFromFileURLs(true);
|
||||
ws.setAllowUniversalAccessFromFileURLs(true);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
ws.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
|
||||
}
|
||||
|
||||
FrameLayout.LayoutParams webLp = new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
);
|
||||
card.addView(webView, webLp);
|
||||
panelWebView = webView;
|
||||
|
||||
try {
|
||||
webView.loadUrl(buildOverlayWebViewUrl());
|
||||
} catch (Exception ex) {
|
||||
Log.w(TAG, "Failed to load overlay webview URL", ex);
|
||||
}
|
||||
|
||||
panelView = card;
|
||||
|
||||
int type = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
||||
: WindowManager.LayoutParams.TYPE_PHONE;
|
||||
panelParams = new WindowManager.LayoutParams(
|
||||
preferredPanelWidthPx(),
|
||||
maxPanelHeightPx(),
|
||||
type,
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
|
||||
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
|
||||
PixelFormat.TRANSLUCENT
|
||||
);
|
||||
panelParams.gravity = Gravity.TOP | Gravity.START;
|
||||
panelParams.x = dp(12);
|
||||
panelParams.y = dp(120);
|
||||
panelParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
|
||||
}
|
||||
|
||||
private void positionPanelNearBubble(boolean resetIfOffscreen) {
|
||||
if (panelParams == null) return;
|
||||
int screenW = getResources().getDisplayMetrics().widthPixels;
|
||||
int screenH = getResources().getDisplayMetrics().heightPixels;
|
||||
int topSafe = safeTopInsetPx();
|
||||
int bottomSafe = safeBottomInsetPx();
|
||||
int availableH = Math.max(dp(280), screenH - topSafe - bottomSafe);
|
||||
panelParams.width = preferredPanelWidthPx();
|
||||
panelParams.height = Math.min(panelParams.height > 0 ? panelParams.height : maxPanelHeightPx(), maxPanelHeightPx());
|
||||
panelParams.x = Math.max(safeSideMarginPx(), (screenW - panelParams.width) / 2);
|
||||
panelParams.y = topSafe + Math.max(0, (availableH - panelParams.height) / 2);
|
||||
clampPanelPosition();
|
||||
}
|
||||
|
||||
private void clampPanelPosition() {
|
||||
if (panelParams == null) return;
|
||||
int screenW = getResources().getDisplayMetrics().widthPixels;
|
||||
int screenH = getResources().getDisplayMetrics().heightPixels;
|
||||
int sideMargin = safeSideMarginPx();
|
||||
int topSafe = safeTopInsetPx();
|
||||
int bottomSafe = safeBottomInsetPx();
|
||||
int panelW = panelParams.width > 0 ? panelParams.width : preferredPanelWidthPx();
|
||||
int panelH = panelParams.height > 0 ? panelParams.height : maxPanelHeightPx();
|
||||
|
||||
int maxX = Math.max(sideMargin, screenW - panelW - sideMargin);
|
||||
int maxY = Math.max(topSafe, screenH - panelH - bottomSafe);
|
||||
panelParams.x = Math.max(sideMargin, Math.min(panelParams.x, maxX));
|
||||
panelParams.y = Math.max(topSafe, Math.min(panelParams.y, maxY));
|
||||
}
|
||||
|
||||
private void clampBubblePosition() {
|
||||
if (bubbleParams == null) return;
|
||||
int screenW = getResources().getDisplayMetrics().widthPixels;
|
||||
int screenH = getResources().getDisplayMetrics().heightPixels;
|
||||
int sideMargin = safeSideMarginPx();
|
||||
int topSafe = safeTopInsetPx();
|
||||
int bottomSafe = safeBottomInsetPx();
|
||||
int bubbleW = bubbleParams.width > 0 ? bubbleParams.width : dp(64);
|
||||
int bubbleH = bubbleParams.height > 0 ? bubbleParams.height : dp(64);
|
||||
|
||||
int maxX = Math.max(sideMargin, screenW - bubbleW - sideMargin);
|
||||
int maxY = Math.max(topSafe, screenH - bubbleH - bottomSafe);
|
||||
bubbleParams.x = Math.max(sideMargin, Math.min(bubbleParams.x, maxX));
|
||||
bubbleParams.y = Math.max(topSafe, Math.min(bubbleParams.y, maxY));
|
||||
}
|
||||
|
||||
private int preferredPanelWidthPx() {
|
||||
int screenW = getResources().getDisplayMetrics().widthPixels;
|
||||
int minMargin = dp(10);
|
||||
int maxWidth = Math.max(dp(220), screenW - (minMargin * 2));
|
||||
int target = Math.round(screenW * 0.94f);
|
||||
int minWidth = Math.min(dp(280), maxWidth);
|
||||
return Math.max(minWidth, Math.min(maxWidth, target));
|
||||
}
|
||||
|
||||
private int maxPanelHeightPx() {
|
||||
int screenH = getResources().getDisplayMetrics().heightPixels;
|
||||
int topSafe = safeTopInsetPx();
|
||||
int bottomSafe = safeBottomInsetPx();
|
||||
int available = Math.max(dp(360), screenH - topSafe - bottomSafe);
|
||||
int maxHeight = Math.max(dp(260), available - dp(8));
|
||||
int target = Math.round(available * 0.92f);
|
||||
int minHeight = Math.min(dp(360), maxHeight);
|
||||
return Math.max(minHeight, Math.min(maxHeight, target));
|
||||
}
|
||||
|
||||
private int minPanelHeightPx() {
|
||||
int max = maxPanelHeightPx();
|
||||
return Math.min(max, dp(260));
|
||||
}
|
||||
|
||||
private int safeSideMarginPx() {
|
||||
return dp(10);
|
||||
}
|
||||
|
||||
private int safeTopInsetPx() {
|
||||
return readSystemDimenPx("status_bar_height") + dp(8);
|
||||
}
|
||||
|
||||
private int safeBottomInsetPx() {
|
||||
return readSystemDimenPx("navigation_bar_height") + dp(8);
|
||||
}
|
||||
|
||||
private int readSystemDimenPx(String name) {
|
||||
try {
|
||||
int resId = getResources().getIdentifier(name, "dimen", "android");
|
||||
if (resId > 0) {
|
||||
return getResources().getDimensionPixelSize(resId);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private String buildOverlayWebViewUrl() {
|
||||
return "file:///android_asset/public/index.html?mode=overlay";
|
||||
}
|
||||
|
||||
private boolean copyTextToClipboardNative(String text) {
|
||||
try {
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (clipboard == null) return false;
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("dewemoji", String.valueOf(text == null ? "" : text)));
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
Log.w(TAG, "Native clipboard copy failed", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private final class OverlayWebBridge {
|
||||
@JavascriptInterface
|
||||
public boolean copyText(String text) {
|
||||
return copyTextToClipboardNative(text);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void setContentHeight(int cssPx) {
|
||||
if (cssPx <= 0) return;
|
||||
View target = panelView;
|
||||
if (target == null) return;
|
||||
target.post(() -> applyPanelContentHeightCss(cssPx));
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void closePanel() {
|
||||
hidePanelView();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void openFullApp() {
|
||||
openMainActivity();
|
||||
}
|
||||
}
|
||||
|
||||
private void applyPanelContentHeightCss(int cssPx) {
|
||||
if (panelParams == null || panelView == null || windowManager == null) return;
|
||||
float density = getResources().getDisplayMetrics().density;
|
||||
int desiredPx = Math.round(cssPx * density);
|
||||
desiredPx += dp(2);
|
||||
desiredPx = Math.max(minPanelHeightPx(), Math.min(desiredPx, maxPanelHeightPx()));
|
||||
if (Math.abs(desiredPx - panelParams.height) < dp(6)) {
|
||||
return;
|
||||
}
|
||||
panelParams.height = desiredPx;
|
||||
|
||||
int screenH = getResources().getDisplayMetrics().heightPixels;
|
||||
int topSafe = safeTopInsetPx();
|
||||
int bottomSafe = safeBottomInsetPx();
|
||||
int availableH = Math.max(dp(280), screenH - topSafe - bottomSafe);
|
||||
panelParams.y = topSafe + Math.max(0, (availableH - panelParams.height) / 2);
|
||||
clampPanelPosition();
|
||||
|
||||
try {
|
||||
if (panelView.isAttachedToWindow()) {
|
||||
windowManager.updateViewLayout(panelView, panelParams);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Log.w(TAG, "Failed to apply panel content height", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeBubbleView() {
|
||||
if (windowManager == null || bubbleView == null) {
|
||||
bubbleView = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
windowManager.removeView(bubbleView);
|
||||
} catch (Exception ignored) {
|
||||
} finally {
|
||||
bubbleView = null;
|
||||
}
|
||||
}
|
||||
|
||||
private int dp(int value) {
|
||||
float density = getResources().getDisplayMetrics().density;
|
||||
return Math.round(value * density);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.dewemoji.app.plugins;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.dewemoji.app.OverlayBubbleService;
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
|
||||
@CapacitorPlugin(name = "DewemojiOverlay")
|
||||
public class DewemojiOverlayPlugin extends Plugin {
|
||||
private static final String APP_STATE_KEY = "app_state_json";
|
||||
|
||||
@PluginMethod
|
||||
public void isOverlayPermissionGranted(PluginCall call) {
|
||||
JSObject out = new JSObject();
|
||||
out.put("granted", canDrawOverlays());
|
||||
call.resolve(out);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void openOverlayPermissionSettings(PluginCall call) {
|
||||
try {
|
||||
Intent intent = new Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
Uri.parse("package:" + getContext().getPackageName())
|
||||
);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
getContext().startActivity(intent);
|
||||
call.resolve();
|
||||
} catch (Exception ex) {
|
||||
call.reject("Failed to open overlay settings", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void areNotificationsEnabled(PluginCall call) {
|
||||
JSObject out = new JSObject();
|
||||
out.put("enabled", notificationsEnabled());
|
||||
call.resolve(out);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void openNotificationSettings(PluginCall call) {
|
||||
try {
|
||||
Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getContext().getPackageName());
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
getContext().startActivity(intent);
|
||||
call.resolve();
|
||||
} catch (Exception ex) {
|
||||
call.reject("Failed to open notification settings", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void isBubbleRunning(PluginCall call) {
|
||||
JSObject out = new JSObject();
|
||||
out.put("running", OverlayBubbleService.isRunning());
|
||||
call.resolve(out);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void startBubble(PluginCall call) {
|
||||
JSObject out = new JSObject();
|
||||
|
||||
if (!canDrawOverlays()) {
|
||||
out.put("started", false);
|
||||
out.put("reason", "overlay_permission_required");
|
||||
call.resolve(out);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!notificationsEnabled()) {
|
||||
out.put("started", false);
|
||||
out.put("reason", "notifications_required");
|
||||
call.resolve(out);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Intent intent = new Intent(getContext(), OverlayBubbleService.class);
|
||||
intent.setAction(OverlayBubbleService.ACTION_START);
|
||||
ContextCompat.startForegroundService(getContext(), intent);
|
||||
out.put("started", true);
|
||||
call.resolve(out);
|
||||
} catch (Exception ex) {
|
||||
call.reject("Failed to start Dewemoji bubble", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void stopBubble(PluginCall call) {
|
||||
try {
|
||||
Intent intent = new Intent(getContext(), OverlayBubbleService.class);
|
||||
intent.setAction(OverlayBubbleService.ACTION_STOP);
|
||||
getContext().startService(intent);
|
||||
JSObject out = new JSObject();
|
||||
out.put("stopped", true);
|
||||
call.resolve(out);
|
||||
} catch (Exception ex) {
|
||||
call.reject("Failed to stop Dewemoji bubble", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void getAppState(PluginCall call) {
|
||||
JSObject out = new JSObject();
|
||||
try {
|
||||
SharedPreferences prefs = nativePrefs();
|
||||
String raw = prefs.getString(APP_STATE_KEY, "{}");
|
||||
JSObject state = (raw == null || raw.trim().isEmpty()) ? new JSObject() : new JSObject(raw);
|
||||
out.put("state", state);
|
||||
call.resolve(out);
|
||||
} catch (Exception ex) {
|
||||
call.reject("Failed to read app state", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void setAppState(PluginCall call) {
|
||||
try {
|
||||
JSObject incoming = call.getObject("state", new JSObject());
|
||||
if (incoming == null) {
|
||||
incoming = new JSObject();
|
||||
}
|
||||
nativePrefs().edit().putString(APP_STATE_KEY, incoming.toString()).apply();
|
||||
JSObject out = new JSObject();
|
||||
out.put("saved", true);
|
||||
call.resolve(out);
|
||||
} catch (Exception ex) {
|
||||
call.reject("Failed to save app state", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private SharedPreferences nativePrefs() {
|
||||
return getContext().getSharedPreferences(OverlayBubbleService.PREFS_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
private boolean canDrawOverlays() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return true;
|
||||
}
|
||||
return Settings.canDrawOverlays(getContext());
|
||||
}
|
||||
|
||||
private boolean notificationsEnabled() {
|
||||
if (!NotificationManagerCompat.from(getContext()).areNotificationsEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return ContextCompat.checkSelfPermission(
|
||||
getContext(),
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
@@ -1,9 +1,5 @@
|
||||
{
|
||||
"appId": "com.dewemoji.app",
|
||||
"appName": "Dewemoji",
|
||||
"webDir": "www",
|
||||
"server": {
|
||||
"url": "https://dewemoji.com",
|
||||
"cleartext": false
|
||||
}
|
||||
"webDir": "www"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"build": "node ./scripts/build-web.js",
|
||||
"test": "echo \"No tests configured\""
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
||||
26
dewemoji-capacitor/scripts/build-web.js
Normal file
26
dewemoji-capacitor/scripts/build-web.js
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
const srcDir = path.join(rootDir, 'src');
|
||||
const outDir = path.join(rootDir, 'www');
|
||||
|
||||
function fail(message) {
|
||||
console.error(`error: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
fail(`missing source directory: ${srcDir}`);
|
||||
}
|
||||
|
||||
try {
|
||||
fs.rmSync(outDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.cpSync(srcDir, outDir, { recursive: true });
|
||||
|
||||
console.log(`Built web assets: ${path.relative(rootDir, outDir)}`);
|
||||
} catch (error) {
|
||||
fail(error && error.message ? error.message : String(error));
|
||||
}
|
||||
967
dewemoji-capacitor/src/app.css
Normal file
967
dewemoji-capacitor/src/app.css
Normal file
@@ -0,0 +1,967 @@
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--bg:#fff; --fg:#111; --mut:#6b7280; --dim:#f3f4f6; --br:#e5e7eb; --active: #60a5fa2b;
|
||||
}
|
||||
@media (prefers-color-scheme: dark){
|
||||
:root { --bg:#0b1220; --fg:#e5e7eb; --mut:#9ca3af; --dim:#111827; --br:#374151; }
|
||||
}
|
||||
/* explicit override by class */
|
||||
.theme-light {
|
||||
color-scheme: light;
|
||||
--bg:#fff; --fg:#111; --mut:#6b7280; --dim:#f3f4f6; --br:#e5e7eb;
|
||||
}
|
||||
.theme-dark {
|
||||
color-scheme: dark;
|
||||
--bg:#0b1220; --fg:#e5e7eb; --mut:#9ca3af; --dim:#111827; --br:#374151;
|
||||
}
|
||||
|
||||
html, body { margin:0; padding:0; background:var(--bg); color:var(--fg);
|
||||
font:14px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Inter, sans-serif; }
|
||||
|
||||
.hdr { position:sticky; top:0; background:var(--bg); padding:10px 10px 6px; border-bottom:1px solid var(--br); z-index: 9;}
|
||||
.ttl { margin:0 0 8px; font-size:16px; font-weight:700; }
|
||||
.bar { display:flex; gap:6px; }
|
||||
.inp { flex:1; padding:8px 8px; border:1px solid var(--br); border-radius:8px; background:var(--bg); color:var(--fg); }
|
||||
/* buttons */
|
||||
.btn { padding:8px 10px; border:1px solid var(--br); border-radius:8px; background:var(--dim); cursor:pointer; }
|
||||
.btn:hover { filter: brightness(0.98); }
|
||||
.btn.icon { width:36px; height:36px; display:grid; place-items:center; padding:0; }
|
||||
|
||||
/* disabled buttons (e.g., Load more) */
|
||||
.btn[disabled] { opacity:.55; cursor:not-allowed; }
|
||||
|
||||
.filters { display:grid; grid-template-columns:1fr 1fr; gap:6px; padding:8px 10px; border-bottom:1px solid var(--br); }
|
||||
.grid { display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:8px; padding:10px; }
|
||||
|
||||
.card {
|
||||
display:flex; flex-direction:column; align-items:center; justify-content:center;
|
||||
gap:6px; padding:10px; border:1px solid var(--br); border-radius:12px; background:var(--bg);
|
||||
cursor: pointer;
|
||||
}
|
||||
.card .emo { font-size:28px; line-height:1; }
|
||||
.card .nm { font-size:12px; color:var(--mut); text-align:center; max-width:100%;
|
||||
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
|
||||
.ft { display:flex; align-items:center; justify-content: space-between; gap:8px; padding:10px; border-top:1px solid var(--br);
|
||||
position:sticky; bottom:0; background:var(--bg); }
|
||||
.muted { color:var(--mut); }
|
||||
.nowrap { white-space: nowrap; }
|
||||
|
||||
/* Toast */
|
||||
.toast {
|
||||
position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%);
|
||||
background: var(--dim); color: var(--fg); padding: 8px 12px; border-radius: 8px;
|
||||
border:1px solid var(--br); opacity: 0; pointer-events:none; transition: opacity .18s ease;
|
||||
}
|
||||
.toast.show { opacity: 1; }
|
||||
|
||||
.sel {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--br);
|
||||
border-radius: 8px;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
appearance: none; /* cleaner look */
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, var(--mut) 50%),
|
||||
linear-gradient(135deg, var(--mut) 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 18px) calc(50% - 3px),
|
||||
calc(100% - 12px) calc(50% - 3px);
|
||||
background-size: 6px 6px, 6px 6px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.sel:disabled { opacity: .6; }
|
||||
.badge {
|
||||
padding:2px 8px; border-radius:9999px; font-size:11px; line-height:1.6;
|
||||
background: var(--dim); color: var(--fg); border:1px solid var(--br);
|
||||
}
|
||||
|
||||
/* Backdrop + Sheet (modal) */
|
||||
.backdrop {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,.35);
|
||||
z-index: 80; opacity: 0; transition: opacity .18s ease;
|
||||
}
|
||||
.backdrop.show { opacity: 1; }
|
||||
|
||||
.sheet {
|
||||
position: fixed; right: 12px; bottom: 12px; left: 12px; max-width: 520px;
|
||||
margin: 0 auto; z-index: 81; background: var(--bg); color: var(--fg);
|
||||
border: 1px solid var(--br); border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,.25);
|
||||
transform: translateY(8px); opacity: 0; transition: transform .18s ease, opacity .18s ease;
|
||||
}
|
||||
.sheet.show { transform: translateY(0); opacity: 1; }
|
||||
.sheet-head { display:flex; align-items:center; justify-content:space-between; padding:12px; border-bottom:1px solid var(--br); border-radius: inherit}
|
||||
.sheet-body { padding:12px; display:grid; gap:14px; }
|
||||
.field .lbl { display:block; font-weight:600; margin-bottom:6px; }
|
||||
.field .hint { margin-top:6px; font-size:12px; }
|
||||
.row { display:flex; gap:8px; }
|
||||
|
||||
/* Radios */
|
||||
.radios { display:grid; gap:8px; }
|
||||
.radio { display:flex; align-items:center; gap:8px; padding:8px; border:1px solid var(--br); border-radius:8px; background: var(--bg); }
|
||||
.radio input[type="radio"] { accent-color: #3b82f6; }
|
||||
.radio[aria-disabled="true"] { opacity:.55; }
|
||||
|
||||
.tag {
|
||||
margin-left:auto; font-size:11px; padding:2px 8px; border-radius:9999px;
|
||||
border:1px solid var(--br); background: var(--dim); color: var(--fg);
|
||||
}
|
||||
|
||||
/* Badge (version) already exists — keep it; this is just a reminder */
|
||||
.badge { padding:2px 8px; border-radius:9999px; font-size:11px; line-height:1.6;
|
||||
background: var(--dim); color: var(--fg); border:1px solid var(--br); }
|
||||
|
||||
/* Buttons & inputs already set; ensure icon buttons look good */
|
||||
.btn.icon { width:36px; height:36px; display:grid; place-items:center; padding:0; }
|
||||
.btn[disabled] { opacity:.55; cursor:not-allowed; }
|
||||
|
||||
.diagbox {
|
||||
margin-top: 8px;
|
||||
padding: 10px;
|
||||
border: 1px dashed var(--br);
|
||||
border-radius: 8px;
|
||||
background: var(--dim);
|
||||
color: var(--fg);
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* apply to the glyph container you use */
|
||||
.emo, .emo * {
|
||||
font-family:
|
||||
"Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji",
|
||||
"Twemoji Mozilla", "EmojiOne Color", "Segoe UI Symbol",
|
||||
system-ui, sans-serif !important;
|
||||
font-variant-emoji: emoji;
|
||||
line-height: 1;
|
||||
}
|
||||
.emo img {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#dewemoji-status {
|
||||
float: right;
|
||||
}
|
||||
/* Settings tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--br);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.tab {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background: var(--dim);
|
||||
border: 1px solid var(--br);
|
||||
border-bottom: none;
|
||||
border-radius: 8px 8px 0 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tab:not(.active) {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.tab.active {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
opacity: 1;
|
||||
}
|
||||
.tabpane { display: none; }
|
||||
.tabpane.active { display: block; }
|
||||
|
||||
/* Tone palette theming */
|
||||
.theme-light, :root.theme-light {
|
||||
--c-bg: var(--bg, #ffffff);
|
||||
--c-chip: var(--dim, #f3f4f6);
|
||||
--c-border: var(--br, #e5e7eb);
|
||||
}
|
||||
.theme-dark, :root.theme-dark {
|
||||
--c-bg: var(--bg, #0f172a);
|
||||
--c-chip: var(--dim, #111827);
|
||||
--c-border: var(--br, #374151);
|
||||
}
|
||||
|
||||
/* Accent color for focus/selection rings */
|
||||
.theme-light, :root.theme-light { --accent: #3b82f6; }
|
||||
.theme-dark, :root.theme-dark { --accent: #60a5fa; }
|
||||
|
||||
/* Tone palette buttons (popover + settings) */
|
||||
.tone-btn, .tone-chip {
|
||||
border: 1px solid var(--c-border);
|
||||
background: var(--c-chip);
|
||||
}
|
||||
.tone-btn.selected, .tone-chip.selected {
|
||||
border-color: var(--accent) !important;
|
||||
box-shadow: 0 0 0 2px var(--accent) !important;
|
||||
}
|
||||
.tone-btn:focus-visible, .tone-chip:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ===== Settings sheet polish ===== */
|
||||
.sheet { max-width: 540px; }
|
||||
.sheet-head {
|
||||
position: sticky; top: 0; background: var(--bg);
|
||||
z-index: 2; border-bottom: 1px solid var(--br);
|
||||
}
|
||||
.sheet-head h3 { margin: 0; }
|
||||
|
||||
.sheet-body { padding-top: 10px; }
|
||||
.field { margin: 12px 0 16px; }
|
||||
.field .lbl { display: block; font-weight: 600; margin-bottom: 6px; }
|
||||
|
||||
/* Compact rows */
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto; /* input | Activate | Deactivate */
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.row + .row { margin-top: 6px; }
|
||||
|
||||
/* Inputs and small buttons */
|
||||
.inp { padding: 6px 10px; }
|
||||
.btn.sm { padding: 6px 10px; font-size: 12px; line-height: 1; }
|
||||
.btn.ghost { background: transparent; border: 1px solid var(--br); }
|
||||
.link { background: none; border: 0; color: var(--accent); cursor: pointer; padding: 0; }
|
||||
.link:hover { text-decoration: underline; }
|
||||
|
||||
/* Tabs = segmented control */
|
||||
.tabs {
|
||||
background: var(--dim); padding: 4px; border-radius: 10px;
|
||||
display: inline-flex; gap: 4px; border: 1px solid var(--br);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.tab { border: 0; background: transparent; padding: 6px 12px; border-radius: 8px; font-weight: 600; }
|
||||
.tab.active { background: var(--bg); box-shadow: inset 0 0 0 1px var(--br); }
|
||||
|
||||
/* License subtext wraps neatly */
|
||||
#license-status { display: inline-block; margin-left: 8px; }
|
||||
|
||||
/* Tone palette spacin.g + buttons */
|
||||
.tone-palette { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.tone-chip {
|
||||
min-width: 40px; height: 36px;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
border-radius: 8px; border: 1px solid var(--c-border); background: var(--c-chip);
|
||||
}
|
||||
.tone-chip.selected { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
|
||||
|
||||
/* Toast a tad higher so it doesn’t overlap sheet */
|
||||
.toast { bottom: 18px; }
|
||||
|
||||
/* Make the top controls a proper stacking context above the grid */
|
||||
.topbar, .filters-row {
|
||||
position: sticky; /* or relative if not sticky */
|
||||
z-index: 5;
|
||||
background: var(--c-bg, #0f172a); /* ensure it has a solid bg in dark & light */
|
||||
}
|
||||
|
||||
/* Or ensure the select dropdown sits above adjacent icons */
|
||||
select#sub {
|
||||
position: relative;
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
div.card.active {
|
||||
background-color: var(--active)!important;
|
||||
}
|
||||
|
||||
.tone-row {
|
||||
grid-column: 1 / -1;
|
||||
background-color: var(--active);
|
||||
border: 1px solid var(--c-border, rgba(0, 0, 0, .12));
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0px, 1fr));
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tone-option {
|
||||
height: 56px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--c-border, rgba(0, 0, 0, .12));
|
||||
background: var(--c-bg, #f3f4f6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: x-large;
|
||||
}
|
||||
|
||||
/* ===== APK companion overrides (extension-like, denser) ===== */
|
||||
html {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
padding-top: max(0px, env(safe-area-inset-top));
|
||||
padding-bottom: max(0px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.hdr {
|
||||
padding-top: calc(10px + env(safe-area-inset-top) * 0.25);
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(68px, 1fr));
|
||||
gap: 6px;
|
||||
align-content: start;
|
||||
min-height: calc(100vh - 170px);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
body.overlay-mode .grid {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
html.overlay-mode,
|
||||
body.overlay-mode {
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
body.overlay-mode {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
body.overlay-mode .hdr,
|
||||
body.overlay-mode .ft {
|
||||
position: static;
|
||||
}
|
||||
|
||||
body.overlay-mode #theme {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.overlay-mode #settings-sheet,
|
||||
body.overlay-mode #sheet-backdrop {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.overlay-mode {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
body.overlay-mode .bar {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
body.overlay-mode .filters {
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
body.overlay-mode .inp,
|
||||
body.overlay-mode .sel {
|
||||
min-height: 48px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
body.overlay-mode .btn {
|
||||
min-height: 46px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
body.overlay-mode .btn.icon {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
min-height: 46px;
|
||||
padding: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
body.overlay-mode .btn.w {
|
||||
min-width: 124px;
|
||||
}
|
||||
|
||||
body.overlay-mode .ft {
|
||||
padding: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body.overlay-mode .sheet {
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
max-width: none;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 16px 44px rgba(0,0,0,.28);
|
||||
}
|
||||
|
||||
body.overlay-mode .sheet-head {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
body.overlay-mode .sheet-head h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
body.overlay-mode .sheet-body {
|
||||
padding: 14px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
body.overlay-mode .field {
|
||||
margin: 14px 0 18px;
|
||||
}
|
||||
|
||||
body.overlay-mode .field .lbl {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
body.overlay-mode .field .hint,
|
||||
body.overlay-mode .diagbox,
|
||||
body.overlay-mode .muted {
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
body.overlay-mode .diagbox {
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
body.overlay-mode .row,
|
||||
body.overlay-mode .row-inline,
|
||||
body.overlay-mode .row-wrap {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body.overlay-mode .row-inline {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
body.overlay-mode .row-wrap .btn,
|
||||
body.overlay-mode .row-inline .btn {
|
||||
min-height: 46px;
|
||||
}
|
||||
|
||||
body.overlay-mode #account-connect-form .row-block .inp,
|
||||
body.overlay-mode #account-connected .row-inline .btn {
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
body.overlay-mode .tabs {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
padding: 5px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
body.overlay-mode .tab {
|
||||
min-height: 44px;
|
||||
padding: 10px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
body.overlay-mode .radio {
|
||||
min-height: 44px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
body.overlay-mode .tone-pref-group {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body.overlay-mode .tone-swatch {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
body.overlay-mode .tone-circle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
body.overlay-mode .bubble-grid {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body.overlay-mode .bubble-kv {
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
body.overlay-mode .bubble-kv > span:last-child {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body.overlay-mode .tone-row {
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
body.overlay-mode .tone-option {
|
||||
min-height: 48px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@media (pointer: coarse), (max-width: 900px) {
|
||||
body:not(.overlay-mode) .inp,
|
||||
body:not(.overlay-mode) .sel {
|
||||
min-height: 46px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .btn {
|
||||
min-height: 44px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .btn.icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
min-height: 44px;
|
||||
padding: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .filters {
|
||||
gap: 8px;
|
||||
padding: 10px 10px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .bar {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .ft {
|
||||
gap: 10px;
|
||||
padding: 12px 10px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .sheet {
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .sheet-head {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .sheet-head h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .sheet-body {
|
||||
padding: 14px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .field {
|
||||
margin: 14px 0 18px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .row,
|
||||
body:not(.overlay-mode) .row-inline,
|
||||
body:not(.overlay-mode) .row-wrap {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .row-inline {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .tabs {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
padding: 5px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .tab {
|
||||
min-height: 44px;
|
||||
padding: 10px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .radio {
|
||||
min-height: 44px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) #account-connect-form .row-block .inp,
|
||||
body:not(.overlay-mode) #account-connected .row-inline .btn {
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .bubble-kv {
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
min-height: 72px;
|
||||
padding: 8px 6px;
|
||||
gap: 4px;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.card * {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.card .emo {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.card .nm {
|
||||
font-size: 10px;
|
||||
line-height: 1.15;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body.emoji-names-hidden .card {
|
||||
min-height: 58px;
|
||||
gap: 2px;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
body.emoji-names-hidden .card .nm {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.emoji-names-hidden .card .emo {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.btn.w {
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
.ft {
|
||||
padding-bottom: calc(10px + env(safe-area-inset-bottom) * 0.5);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.sheet {
|
||||
bottom: calc(12px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.row-inline {
|
||||
display: flex;
|
||||
grid-template-columns: none;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row-wrap {
|
||||
display: flex;
|
||||
grid-template-columns: none;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row-block {
|
||||
display: block;
|
||||
grid-template-columns: none;
|
||||
}
|
||||
|
||||
.row-block .inp {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.diagbox {
|
||||
margin-top: 8px;
|
||||
padding: 10px;
|
||||
border: 1px dashed var(--br);
|
||||
border-radius: 8px;
|
||||
background: var(--dim);
|
||||
color: var(--fg);
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tone-pref-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tone-swatch {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tone-swatch input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
inset: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tone-circle {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--br);
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,.08);
|
||||
}
|
||||
|
||||
.tone-default {
|
||||
background:
|
||||
radial-gradient(circle at 50% 50%, transparent 0 28%, rgba(255,255,255,.7) 29% 34%, transparent 35%),
|
||||
linear-gradient(180deg, #d7e0eb 0%, #a4b2c3 100%);
|
||||
}
|
||||
|
||||
.tone-1 { background: #f7d7c4; }
|
||||
.tone-2 { background: #e8bf95; }
|
||||
.tone-3 { background: #c68642; }
|
||||
.tone-4 { background: #8d5524; }
|
||||
.tone-5 { background: #5e3b22; }
|
||||
|
||||
.tone-swatch input:checked + .tone-circle {
|
||||
outline: 2px solid #60a5fa;
|
||||
outline-offset: 2px;
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
|
||||
.tone-swatch input:focus-visible + .tone-circle {
|
||||
outline: 2px solid #60a5fa;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.bubble-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin: 8px 0 10px;
|
||||
}
|
||||
|
||||
.bubble-kv {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
border: 1px solid var(--br);
|
||||
background: var(--bg);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bubble-kv > span:last-child {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.bubble-kv .ok { color: #059669; }
|
||||
.bubble-kv .warn { color: #d97706; }
|
||||
.bubble-kv .bad { color: #dc2626; }
|
||||
|
||||
.alert {
|
||||
grid-column: 1 / -1;
|
||||
border: 1px solid var(--br);
|
||||
background: var(--dim);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.alert strong {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.alert.error {
|
||||
border-color: rgba(220, 38, 38, .35);
|
||||
}
|
||||
|
||||
.alert.warn {
|
||||
border-color: rgba(217, 119, 6, .35);
|
||||
}
|
||||
|
||||
.autoload-sentinel {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.card.private {
|
||||
box-shadow: inset 0 0 0 1px rgba(59, 130, 246, .35);
|
||||
}
|
||||
|
||||
.card.private .nm::after {
|
||||
content: " · private";
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.card.toneable::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 999px;
|
||||
background: #60a5fa;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.tone-row {
|
||||
grid-column: 1 / -1;
|
||||
border: 1px solid var(--br);
|
||||
background: var(--dim);
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tone-option {
|
||||
min-height: 42px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--br);
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.tone-option.selected {
|
||||
box-shadow: inset 0 0 0 2px #60a5fa;
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
|
||||
.tone-option.label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tone-note {
|
||||
grid-column: 1 / -1;
|
||||
font-size: 12px;
|
||||
color: var(--mut);
|
||||
padding: 2px 2px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||
gap: 5px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.card {
|
||||
min-height: 64px;
|
||||
padding: 6px 4px;
|
||||
}
|
||||
|
||||
.card .emo {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.card .nm {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
body.emoji-names-hidden .card {
|
||||
min-height: 52px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
body.emoji-names-hidden .card .emo {
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.bubble-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tone-row {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.tabs {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.tab {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.card {
|
||||
min-height: 54px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.card .emo {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.card .nm {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
body.emoji-names-hidden .card .emo {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.tone-row {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 5px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.tone-option {
|
||||
min-height: 38px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
1892
dewemoji-capacitor/src/app.js
Normal file
1892
dewemoji-capacitor/src/app.js
Normal file
File diff suppressed because it is too large
Load Diff
182
dewemoji-capacitor/src/index.html
Normal file
182
dewemoji-capacitor/src/index.html
Normal file
@@ -0,0 +1,182 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Dewemoji</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#0b1220" />
|
||||
<link rel="preconnect" href="https://twemoji.maxcdn.com" crossorigin />
|
||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin />
|
||||
<link href="app.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="theme-dark">
|
||||
<header class="hdr">
|
||||
<h1 class="ttl">Find Your Emoji <span id="dewemoji-status" class="tag free">Guest</span></h1>
|
||||
<div class="bar">
|
||||
<input id="q" class="inp" type="search" placeholder="Search (e.g. love)" aria-label="Search emojis" />
|
||||
<button id="clear" class="btn icon" title="Clear" aria-label="Clear">✕</button>
|
||||
<button id="theme" class="btn icon" title="Toggle theme" aria-label="Toggle theme">🌙</button>
|
||||
<button id="settings" class="btn icon" title="Settings" aria-label="Settings">⚙️</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="filters">
|
||||
<select id="cat" class="sel" aria-label="Category filter">
|
||||
<option value="">All categories</option>
|
||||
</select>
|
||||
<select id="sub" class="sel" aria-label="Subcategory filter" disabled>
|
||||
<option value="">All subcategories</option>
|
||||
</select>
|
||||
</section>
|
||||
|
||||
<main id="list" class="grid" aria-live="polite"></main>
|
||||
<div id="autoload-sentinel" class="autoload-sentinel" aria-hidden="true"></div>
|
||||
|
||||
<footer class="ft">
|
||||
<button id="more" class="btn w">Load more</button>
|
||||
<span id="count" class="muted nowrap">0 items</span>
|
||||
<span id="ver" class="badge nowrap">APK</span>
|
||||
</footer>
|
||||
|
||||
<div id="sheet-backdrop" class="backdrop" hidden></div>
|
||||
<aside id="settings-sheet" class="sheet" hidden role="dialog" aria-modal="true" aria-labelledby="settings-title">
|
||||
<div class="sheet-head">
|
||||
<h3 id="settings-title">Settings</h3>
|
||||
<button id="sheet-close" class="btn icon" title="Close" aria-label="Close">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="sheet-body">
|
||||
<div class="tabs" role="tablist" aria-label="Settings tabs">
|
||||
<button class="tab active" data-tab="general" role="tab" aria-selected="true">General</button>
|
||||
<button class="tab" data-tab="account" role="tab" aria-selected="false">Account</button>
|
||||
<button class="tab" data-tab="bubble" role="tab" aria-selected="false">Bubble</button>
|
||||
</div>
|
||||
|
||||
<section id="tab-general" class="tabpane active" role="tabpanel">
|
||||
<div class="field">
|
||||
<label class="lbl">Instant Access</label>
|
||||
<p class="hint muted">Tap an emoji to copy. Then switch back and paste in the app you are typing in.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="lbl">Current Session</label>
|
||||
<div class="diagbox" id="session-summary">Guest mode. Public keywords only.</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="lbl">Grid display</label>
|
||||
<label class="radio row-inline" for="show-emoji-names-toggle">
|
||||
<input id="show-emoji-names-toggle" type="checkbox" />
|
||||
<span>Show emoji names under each emoji</span>
|
||||
</label>
|
||||
<p class="hint muted">Off by default for larger emoji and faster scanning.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="lbl">Skin tone</label>
|
||||
<label class="radio row-inline" for="tone-lock-toggle">
|
||||
<input id="tone-lock-toggle" type="checkbox" />
|
||||
<span>Use preferred skin tone on tap (tone lock)</span>
|
||||
</label>
|
||||
<div class="row row-block" style="margin-top:8px;">
|
||||
<div id="preferred-skin-tone-radios" class="tone-pref-group" role="radiogroup" aria-label="Preferred skin tone">
|
||||
<label class="tone-swatch" title="Default (no tone)">
|
||||
<input type="radio" name="preferredSkinTone" value="0" aria-label="Default (no tone)" />
|
||||
<span class="tone-circle tone-default" aria-hidden="true"></span>
|
||||
</label>
|
||||
<label class="tone-swatch" title="Light skin tone">
|
||||
<input type="radio" name="preferredSkinTone" value="1" aria-label="Light skin tone" />
|
||||
<span class="tone-circle tone-1" aria-hidden="true"></span>
|
||||
</label>
|
||||
<label class="tone-swatch" title="Medium-light skin tone">
|
||||
<input type="radio" name="preferredSkinTone" value="2" aria-label="Medium-light skin tone" />
|
||||
<span class="tone-circle tone-2" aria-hidden="true"></span>
|
||||
</label>
|
||||
<label class="tone-swatch" title="Medium skin tone">
|
||||
<input type="radio" name="preferredSkinTone" value="3" aria-label="Medium skin tone" />
|
||||
<span class="tone-circle tone-3" aria-hidden="true"></span>
|
||||
</label>
|
||||
<label class="tone-swatch" title="Medium-dark skin tone">
|
||||
<input type="radio" name="preferredSkinTone" value="4" aria-label="Medium-dark skin tone" />
|
||||
<span class="tone-circle tone-4" aria-hidden="true"></span>
|
||||
</label>
|
||||
<label class="tone-swatch" title="Dark skin tone">
|
||||
<input type="radio" name="preferredSkinTone" value="5" aria-label="Dark skin tone" />
|
||||
<span class="tone-circle tone-5" aria-hidden="true"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint muted">Long press an emoji with skin tones to choose a tone quickly.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="lbl">Refresh catalog</label>
|
||||
<div class="row row-inline">
|
||||
<button id="refresh" class="btn">Refresh</button>
|
||||
<span id="api-status" class="muted">Loading…</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="tab-account" class="tabpane" role="tabpanel">
|
||||
<div class="field">
|
||||
<label class="lbl">Account</label>
|
||||
<div id="account-connect-form">
|
||||
<div class="row row-block">
|
||||
<input id="account-email" class="inp" placeholder="Email" type="email" autocomplete="username" />
|
||||
</div>
|
||||
<div class="row row-block" style="margin-top:6px;">
|
||||
<input id="account-password" class="inp" placeholder="Password" type="password" autocomplete="current-password" />
|
||||
</div>
|
||||
<div class="row row-inline" style="margin-top:8px;">
|
||||
<button id="account-login" class="btn">Connect</button>
|
||||
</div>
|
||||
<div class="row row-inline" style="margin-top:6px;">
|
||||
<span id="account-status" class="muted">Not connected. Public keywords only.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="account-connected" style="display:none;">
|
||||
<div class="row row-inline">
|
||||
<span id="account-greeting" class="muted">Connected.</span>
|
||||
</div>
|
||||
<div class="row row-inline" style="margin-top:8px;">
|
||||
<button id="account-logout" class="btn ghost">Logout</button>
|
||||
</div>
|
||||
<div class="row row-inline" style="margin-top:6px;">
|
||||
<span class="muted">Private keyword matches appear in search when available.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="tab-bubble" class="tabpane" role="tabpanel">
|
||||
<div class="field">
|
||||
<label class="lbl">Floating bubble (quick search + copy)</label>
|
||||
<p class="hint muted">Use Dewemoji bubble to quickly search and copy emoji while using other apps. Paste manually in the app you’re typing in.</p>
|
||||
</div>
|
||||
|
||||
<div class="bubble-grid">
|
||||
<div class="bubble-kv"><span class="muted">Platform</span><span id="bubble-platform-status">Checking…</span></div>
|
||||
<div class="bubble-kv"><span class="muted">Overlay permission</span><span id="bubble-overlay-status">Checking…</span></div>
|
||||
<div class="bubble-kv"><span class="muted">Notifications</span><span id="bubble-notify-status">Checking…</span></div>
|
||||
<div class="bubble-kv"><span class="muted">Bubble service</span><span id="bubble-running-status">Checking…</span></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="row row-wrap">
|
||||
<button id="bubble-enable-btn" class="btn">Enable bubble</button>
|
||||
<button id="bubble-disable-btn" class="btn ghost">Disable bubble</button>
|
||||
</div>
|
||||
<div class="row row-wrap" style="margin-top:8px;">
|
||||
<button id="bubble-overlay-settings-btn" class="btn ghost">Grant overlay permission</button>
|
||||
<button id="bubble-notify-settings-btn" class="btn ghost">Open notification settings</button>
|
||||
</div>
|
||||
<div class="diagbox" id="bubble-help" style="margin-top:8px;">Bubble is off by default. It opens Dewemoji for quick search and copy.</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||
|
||||
<script src="twemoji-lite.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
31
dewemoji-capacitor/src/twemoji-lite.js
Normal file
31
dewemoji-capacitor/src/twemoji-lite.js
Normal file
@@ -0,0 +1,31 @@
|
||||
(() => {
|
||||
const root = window;
|
||||
if (root?.twemoji?.convert?.toCodePoint) return;
|
||||
|
||||
function toCodePoint(unicodeSurrogates, sep = '-') {
|
||||
const r = [];
|
||||
let c = 0;
|
||||
let p = 0;
|
||||
let i = 0;
|
||||
const s = String(unicodeSurrogates || '');
|
||||
|
||||
while (i < s.length) {
|
||||
c = s.charCodeAt(i++);
|
||||
if (p) {
|
||||
r.push((0x10000 + ((p - 0xD800) * 0x400) + (c - 0xDC00)).toString(16));
|
||||
p = 0;
|
||||
} else if (c >= 0xD800 && c <= 0xDBFF) {
|
||||
p = c;
|
||||
} else {
|
||||
r.push(c.toString(16));
|
||||
}
|
||||
}
|
||||
|
||||
return r.join(sep);
|
||||
}
|
||||
|
||||
root.twemoji = root.twemoji || {};
|
||||
root.twemoji.convert = root.twemoji.convert || {};
|
||||
root.twemoji.convert.toCodePoint = toCodePoint;
|
||||
})();
|
||||
|
||||
967
dewemoji-capacitor/www/app.css
Normal file
967
dewemoji-capacitor/www/app.css
Normal file
@@ -0,0 +1,967 @@
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--bg:#fff; --fg:#111; --mut:#6b7280; --dim:#f3f4f6; --br:#e5e7eb; --active: #60a5fa2b;
|
||||
}
|
||||
@media (prefers-color-scheme: dark){
|
||||
:root { --bg:#0b1220; --fg:#e5e7eb; --mut:#9ca3af; --dim:#111827; --br:#374151; }
|
||||
}
|
||||
/* explicit override by class */
|
||||
.theme-light {
|
||||
color-scheme: light;
|
||||
--bg:#fff; --fg:#111; --mut:#6b7280; --dim:#f3f4f6; --br:#e5e7eb;
|
||||
}
|
||||
.theme-dark {
|
||||
color-scheme: dark;
|
||||
--bg:#0b1220; --fg:#e5e7eb; --mut:#9ca3af; --dim:#111827; --br:#374151;
|
||||
}
|
||||
|
||||
html, body { margin:0; padding:0; background:var(--bg); color:var(--fg);
|
||||
font:14px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Inter, sans-serif; }
|
||||
|
||||
.hdr { position:sticky; top:0; background:var(--bg); padding:10px 10px 6px; border-bottom:1px solid var(--br); z-index: 9;}
|
||||
.ttl { margin:0 0 8px; font-size:16px; font-weight:700; }
|
||||
.bar { display:flex; gap:6px; }
|
||||
.inp { flex:1; padding:8px 8px; border:1px solid var(--br); border-radius:8px; background:var(--bg); color:var(--fg); }
|
||||
/* buttons */
|
||||
.btn { padding:8px 10px; border:1px solid var(--br); border-radius:8px; background:var(--dim); cursor:pointer; }
|
||||
.btn:hover { filter: brightness(0.98); }
|
||||
.btn.icon { width:36px; height:36px; display:grid; place-items:center; padding:0; }
|
||||
|
||||
/* disabled buttons (e.g., Load more) */
|
||||
.btn[disabled] { opacity:.55; cursor:not-allowed; }
|
||||
|
||||
.filters { display:grid; grid-template-columns:1fr 1fr; gap:6px; padding:8px 10px; border-bottom:1px solid var(--br); }
|
||||
.grid { display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:8px; padding:10px; }
|
||||
|
||||
.card {
|
||||
display:flex; flex-direction:column; align-items:center; justify-content:center;
|
||||
gap:6px; padding:10px; border:1px solid var(--br); border-radius:12px; background:var(--bg);
|
||||
cursor: pointer;
|
||||
}
|
||||
.card .emo { font-size:28px; line-height:1; }
|
||||
.card .nm { font-size:12px; color:var(--mut); text-align:center; max-width:100%;
|
||||
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
|
||||
.ft { display:flex; align-items:center; justify-content: space-between; gap:8px; padding:10px; border-top:1px solid var(--br);
|
||||
position:sticky; bottom:0; background:var(--bg); }
|
||||
.muted { color:var(--mut); }
|
||||
.nowrap { white-space: nowrap; }
|
||||
|
||||
/* Toast */
|
||||
.toast {
|
||||
position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%);
|
||||
background: var(--dim); color: var(--fg); padding: 8px 12px; border-radius: 8px;
|
||||
border:1px solid var(--br); opacity: 0; pointer-events:none; transition: opacity .18s ease;
|
||||
}
|
||||
.toast.show { opacity: 1; }
|
||||
|
||||
.sel {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--br);
|
||||
border-radius: 8px;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
appearance: none; /* cleaner look */
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, var(--mut) 50%),
|
||||
linear-gradient(135deg, var(--mut) 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 18px) calc(50% - 3px),
|
||||
calc(100% - 12px) calc(50% - 3px);
|
||||
background-size: 6px 6px, 6px 6px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.sel:disabled { opacity: .6; }
|
||||
.badge {
|
||||
padding:2px 8px; border-radius:9999px; font-size:11px; line-height:1.6;
|
||||
background: var(--dim); color: var(--fg); border:1px solid var(--br);
|
||||
}
|
||||
|
||||
/* Backdrop + Sheet (modal) */
|
||||
.backdrop {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,.35);
|
||||
z-index: 80; opacity: 0; transition: opacity .18s ease;
|
||||
}
|
||||
.backdrop.show { opacity: 1; }
|
||||
|
||||
.sheet {
|
||||
position: fixed; right: 12px; bottom: 12px; left: 12px; max-width: 520px;
|
||||
margin: 0 auto; z-index: 81; background: var(--bg); color: var(--fg);
|
||||
border: 1px solid var(--br); border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,.25);
|
||||
transform: translateY(8px); opacity: 0; transition: transform .18s ease, opacity .18s ease;
|
||||
}
|
||||
.sheet.show { transform: translateY(0); opacity: 1; }
|
||||
.sheet-head { display:flex; align-items:center; justify-content:space-between; padding:12px; border-bottom:1px solid var(--br); border-radius: inherit}
|
||||
.sheet-body { padding:12px; display:grid; gap:14px; }
|
||||
.field .lbl { display:block; font-weight:600; margin-bottom:6px; }
|
||||
.field .hint { margin-top:6px; font-size:12px; }
|
||||
.row { display:flex; gap:8px; }
|
||||
|
||||
/* Radios */
|
||||
.radios { display:grid; gap:8px; }
|
||||
.radio { display:flex; align-items:center; gap:8px; padding:8px; border:1px solid var(--br); border-radius:8px; background: var(--bg); }
|
||||
.radio input[type="radio"] { accent-color: #3b82f6; }
|
||||
.radio[aria-disabled="true"] { opacity:.55; }
|
||||
|
||||
.tag {
|
||||
margin-left:auto; font-size:11px; padding:2px 8px; border-radius:9999px;
|
||||
border:1px solid var(--br); background: var(--dim); color: var(--fg);
|
||||
}
|
||||
|
||||
/* Badge (version) already exists — keep it; this is just a reminder */
|
||||
.badge { padding:2px 8px; border-radius:9999px; font-size:11px; line-height:1.6;
|
||||
background: var(--dim); color: var(--fg); border:1px solid var(--br); }
|
||||
|
||||
/* Buttons & inputs already set; ensure icon buttons look good */
|
||||
.btn.icon { width:36px; height:36px; display:grid; place-items:center; padding:0; }
|
||||
.btn[disabled] { opacity:.55; cursor:not-allowed; }
|
||||
|
||||
.diagbox {
|
||||
margin-top: 8px;
|
||||
padding: 10px;
|
||||
border: 1px dashed var(--br);
|
||||
border-radius: 8px;
|
||||
background: var(--dim);
|
||||
color: var(--fg);
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* apply to the glyph container you use */
|
||||
.emo, .emo * {
|
||||
font-family:
|
||||
"Segoe UI Emoji", "Noto Color Emoji", "Apple Color Emoji",
|
||||
"Twemoji Mozilla", "EmojiOne Color", "Segoe UI Symbol",
|
||||
system-ui, sans-serif !important;
|
||||
font-variant-emoji: emoji;
|
||||
line-height: 1;
|
||||
}
|
||||
.emo img {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#dewemoji-status {
|
||||
float: right;
|
||||
}
|
||||
/* Settings tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--br);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.tab {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background: var(--dim);
|
||||
border: 1px solid var(--br);
|
||||
border-bottom: none;
|
||||
border-radius: 8px 8px 0 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tab:not(.active) {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.tab.active {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
opacity: 1;
|
||||
}
|
||||
.tabpane { display: none; }
|
||||
.tabpane.active { display: block; }
|
||||
|
||||
/* Tone palette theming */
|
||||
.theme-light, :root.theme-light {
|
||||
--c-bg: var(--bg, #ffffff);
|
||||
--c-chip: var(--dim, #f3f4f6);
|
||||
--c-border: var(--br, #e5e7eb);
|
||||
}
|
||||
.theme-dark, :root.theme-dark {
|
||||
--c-bg: var(--bg, #0f172a);
|
||||
--c-chip: var(--dim, #111827);
|
||||
--c-border: var(--br, #374151);
|
||||
}
|
||||
|
||||
/* Accent color for focus/selection rings */
|
||||
.theme-light, :root.theme-light { --accent: #3b82f6; }
|
||||
.theme-dark, :root.theme-dark { --accent: #60a5fa; }
|
||||
|
||||
/* Tone palette buttons (popover + settings) */
|
||||
.tone-btn, .tone-chip {
|
||||
border: 1px solid var(--c-border);
|
||||
background: var(--c-chip);
|
||||
}
|
||||
.tone-btn.selected, .tone-chip.selected {
|
||||
border-color: var(--accent) !important;
|
||||
box-shadow: 0 0 0 2px var(--accent) !important;
|
||||
}
|
||||
.tone-btn:focus-visible, .tone-chip:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ===== Settings sheet polish ===== */
|
||||
.sheet { max-width: 540px; }
|
||||
.sheet-head {
|
||||
position: sticky; top: 0; background: var(--bg);
|
||||
z-index: 2; border-bottom: 1px solid var(--br);
|
||||
}
|
||||
.sheet-head h3 { margin: 0; }
|
||||
|
||||
.sheet-body { padding-top: 10px; }
|
||||
.field { margin: 12px 0 16px; }
|
||||
.field .lbl { display: block; font-weight: 600; margin-bottom: 6px; }
|
||||
|
||||
/* Compact rows */
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto; /* input | Activate | Deactivate */
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.row + .row { margin-top: 6px; }
|
||||
|
||||
/* Inputs and small buttons */
|
||||
.inp { padding: 6px 10px; }
|
||||
.btn.sm { padding: 6px 10px; font-size: 12px; line-height: 1; }
|
||||
.btn.ghost { background: transparent; border: 1px solid var(--br); }
|
||||
.link { background: none; border: 0; color: var(--accent); cursor: pointer; padding: 0; }
|
||||
.link:hover { text-decoration: underline; }
|
||||
|
||||
/* Tabs = segmented control */
|
||||
.tabs {
|
||||
background: var(--dim); padding: 4px; border-radius: 10px;
|
||||
display: inline-flex; gap: 4px; border: 1px solid var(--br);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.tab { border: 0; background: transparent; padding: 6px 12px; border-radius: 8px; font-weight: 600; }
|
||||
.tab.active { background: var(--bg); box-shadow: inset 0 0 0 1px var(--br); }
|
||||
|
||||
/* License subtext wraps neatly */
|
||||
#license-status { display: inline-block; margin-left: 8px; }
|
||||
|
||||
/* Tone palette spacin.g + buttons */
|
||||
.tone-palette { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.tone-chip {
|
||||
min-width: 40px; height: 36px;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
border-radius: 8px; border: 1px solid var(--c-border); background: var(--c-chip);
|
||||
}
|
||||
.tone-chip.selected { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent); }
|
||||
|
||||
/* Toast a tad higher so it doesn’t overlap sheet */
|
||||
.toast { bottom: 18px; }
|
||||
|
||||
/* Make the top controls a proper stacking context above the grid */
|
||||
.topbar, .filters-row {
|
||||
position: sticky; /* or relative if not sticky */
|
||||
z-index: 5;
|
||||
background: var(--c-bg, #0f172a); /* ensure it has a solid bg in dark & light */
|
||||
}
|
||||
|
||||
/* Or ensure the select dropdown sits above adjacent icons */
|
||||
select#sub {
|
||||
position: relative;
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
div.card.active {
|
||||
background-color: var(--active)!important;
|
||||
}
|
||||
|
||||
.tone-row {
|
||||
grid-column: 1 / -1;
|
||||
background-color: var(--active);
|
||||
border: 1px solid var(--c-border, rgba(0, 0, 0, .12));
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0px, 1fr));
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tone-option {
|
||||
height: 56px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--c-border, rgba(0, 0, 0, .12));
|
||||
background: var(--c-bg, #f3f4f6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: x-large;
|
||||
}
|
||||
|
||||
/* ===== APK companion overrides (extension-like, denser) ===== */
|
||||
html {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
padding-top: max(0px, env(safe-area-inset-top));
|
||||
padding-bottom: max(0px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.hdr {
|
||||
padding-top: calc(10px + env(safe-area-inset-top) * 0.25);
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(68px, 1fr));
|
||||
gap: 6px;
|
||||
align-content: start;
|
||||
min-height: calc(100vh - 170px);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
body.overlay-mode .grid {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
html.overlay-mode,
|
||||
body.overlay-mode {
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
body.overlay-mode {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
body.overlay-mode .hdr,
|
||||
body.overlay-mode .ft {
|
||||
position: static;
|
||||
}
|
||||
|
||||
body.overlay-mode #theme {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.overlay-mode #settings-sheet,
|
||||
body.overlay-mode #sheet-backdrop {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.overlay-mode {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
body.overlay-mode .bar {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
body.overlay-mode .filters {
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
body.overlay-mode .inp,
|
||||
body.overlay-mode .sel {
|
||||
min-height: 48px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
body.overlay-mode .btn {
|
||||
min-height: 46px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
body.overlay-mode .btn.icon {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
min-height: 46px;
|
||||
padding: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
body.overlay-mode .btn.w {
|
||||
min-width: 124px;
|
||||
}
|
||||
|
||||
body.overlay-mode .ft {
|
||||
padding: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body.overlay-mode .sheet {
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
max-width: none;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 16px 44px rgba(0,0,0,.28);
|
||||
}
|
||||
|
||||
body.overlay-mode .sheet-head {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
body.overlay-mode .sheet-head h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
body.overlay-mode .sheet-body {
|
||||
padding: 14px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
body.overlay-mode .field {
|
||||
margin: 14px 0 18px;
|
||||
}
|
||||
|
||||
body.overlay-mode .field .lbl {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
body.overlay-mode .field .hint,
|
||||
body.overlay-mode .diagbox,
|
||||
body.overlay-mode .muted {
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
body.overlay-mode .diagbox {
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
body.overlay-mode .row,
|
||||
body.overlay-mode .row-inline,
|
||||
body.overlay-mode .row-wrap {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body.overlay-mode .row-inline {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
body.overlay-mode .row-wrap .btn,
|
||||
body.overlay-mode .row-inline .btn {
|
||||
min-height: 46px;
|
||||
}
|
||||
|
||||
body.overlay-mode #account-connect-form .row-block .inp,
|
||||
body.overlay-mode #account-connected .row-inline .btn {
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
body.overlay-mode .tabs {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
padding: 5px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
body.overlay-mode .tab {
|
||||
min-height: 44px;
|
||||
padding: 10px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
body.overlay-mode .radio {
|
||||
min-height: 44px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
body.overlay-mode .tone-pref-group {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body.overlay-mode .tone-swatch {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
body.overlay-mode .tone-circle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
body.overlay-mode .bubble-grid {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body.overlay-mode .bubble-kv {
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
body.overlay-mode .bubble-kv > span:last-child {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body.overlay-mode .tone-row {
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
body.overlay-mode .tone-option {
|
||||
min-height: 48px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@media (pointer: coarse), (max-width: 900px) {
|
||||
body:not(.overlay-mode) .inp,
|
||||
body:not(.overlay-mode) .sel {
|
||||
min-height: 46px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .btn {
|
||||
min-height: 44px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .btn.icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
min-height: 44px;
|
||||
padding: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .filters {
|
||||
gap: 8px;
|
||||
padding: 10px 10px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .bar {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .ft {
|
||||
gap: 10px;
|
||||
padding: 12px 10px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .sheet {
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .sheet-head {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .sheet-head h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .sheet-body {
|
||||
padding: 14px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .field {
|
||||
margin: 14px 0 18px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .row,
|
||||
body:not(.overlay-mode) .row-inline,
|
||||
body:not(.overlay-mode) .row-wrap {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .row-inline {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .tabs {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
padding: 5px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .tab {
|
||||
min-height: 44px;
|
||||
padding: 10px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .radio {
|
||||
min-height: 44px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) #account-connect-form .row-block .inp,
|
||||
body:not(.overlay-mode) #account-connected .row-inline .btn {
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
body:not(.overlay-mode) .bubble-kv {
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
min-height: 72px;
|
||||
padding: 8px 6px;
|
||||
gap: 4px;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.card * {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.card .emo {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.card .nm {
|
||||
font-size: 10px;
|
||||
line-height: 1.15;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body.emoji-names-hidden .card {
|
||||
min-height: 58px;
|
||||
gap: 2px;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
body.emoji-names-hidden .card .nm {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.emoji-names-hidden .card .emo {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.btn.w {
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
.ft {
|
||||
padding-bottom: calc(10px + env(safe-area-inset-bottom) * 0.5);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.sheet {
|
||||
bottom: calc(12px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.row-inline {
|
||||
display: flex;
|
||||
grid-template-columns: none;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row-wrap {
|
||||
display: flex;
|
||||
grid-template-columns: none;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row-block {
|
||||
display: block;
|
||||
grid-template-columns: none;
|
||||
}
|
||||
|
||||
.row-block .inp {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.diagbox {
|
||||
margin-top: 8px;
|
||||
padding: 10px;
|
||||
border: 1px dashed var(--br);
|
||||
border-radius: 8px;
|
||||
background: var(--dim);
|
||||
color: var(--fg);
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tone-pref-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tone-swatch {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tone-swatch input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
inset: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tone-circle {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--br);
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,.08);
|
||||
}
|
||||
|
||||
.tone-default {
|
||||
background:
|
||||
radial-gradient(circle at 50% 50%, transparent 0 28%, rgba(255,255,255,.7) 29% 34%, transparent 35%),
|
||||
linear-gradient(180deg, #d7e0eb 0%, #a4b2c3 100%);
|
||||
}
|
||||
|
||||
.tone-1 { background: #f7d7c4; }
|
||||
.tone-2 { background: #e8bf95; }
|
||||
.tone-3 { background: #c68642; }
|
||||
.tone-4 { background: #8d5524; }
|
||||
.tone-5 { background: #5e3b22; }
|
||||
|
||||
.tone-swatch input:checked + .tone-circle {
|
||||
outline: 2px solid #60a5fa;
|
||||
outline-offset: 2px;
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
|
||||
.tone-swatch input:focus-visible + .tone-circle {
|
||||
outline: 2px solid #60a5fa;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.bubble-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin: 8px 0 10px;
|
||||
}
|
||||
|
||||
.bubble-kv {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
border: 1px solid var(--br);
|
||||
background: var(--bg);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bubble-kv > span:last-child {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.bubble-kv .ok { color: #059669; }
|
||||
.bubble-kv .warn { color: #d97706; }
|
||||
.bubble-kv .bad { color: #dc2626; }
|
||||
|
||||
.alert {
|
||||
grid-column: 1 / -1;
|
||||
border: 1px solid var(--br);
|
||||
background: var(--dim);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.alert strong {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.alert.error {
|
||||
border-color: rgba(220, 38, 38, .35);
|
||||
}
|
||||
|
||||
.alert.warn {
|
||||
border-color: rgba(217, 119, 6, .35);
|
||||
}
|
||||
|
||||
.autoload-sentinel {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.card.private {
|
||||
box-shadow: inset 0 0 0 1px rgba(59, 130, 246, .35);
|
||||
}
|
||||
|
||||
.card.private .nm::after {
|
||||
content: " · private";
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.card.toneable::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 999px;
|
||||
background: #60a5fa;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.tone-row {
|
||||
grid-column: 1 / -1;
|
||||
border: 1px solid var(--br);
|
||||
background: var(--dim);
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tone-option {
|
||||
min-height: 42px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--br);
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.tone-option.selected {
|
||||
box-shadow: inset 0 0 0 2px #60a5fa;
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
|
||||
.tone-option.label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tone-note {
|
||||
grid-column: 1 / -1;
|
||||
font-size: 12px;
|
||||
color: var(--mut);
|
||||
padding: 2px 2px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||
gap: 5px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.card {
|
||||
min-height: 64px;
|
||||
padding: 6px 4px;
|
||||
}
|
||||
|
||||
.card .emo {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.card .nm {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
body.emoji-names-hidden .card {
|
||||
min-height: 52px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
body.emoji-names-hidden .card .emo {
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.bubble-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tone-row {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.tabs {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.tab {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.card {
|
||||
min-height: 54px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.card .emo {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.card .nm {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
body.emoji-names-hidden .card .emo {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.tone-row {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 5px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.tone-option {
|
||||
min-height: 38px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
1892
dewemoji-capacitor/www/app.js
Normal file
1892
dewemoji-capacitor/www/app.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,182 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Dewemoji</title>
|
||||
<style>
|
||||
html, body { height: 100%; margin: 0; font-family: system-ui, -apple-system, sans-serif; background: #0b1220; color: #fff; }
|
||||
.wrap { display: grid; place-items: center; height: 100%; padding: 24px; text-align: center; }
|
||||
.card { max-width: 460px; }
|
||||
h1 { margin: 0 0 8px; font-size: 24px; }
|
||||
p { opacity: 0.85; line-height: 1.5; }
|
||||
</style>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#0b1220" />
|
||||
<link rel="preconnect" href="https://twemoji.maxcdn.com" crossorigin />
|
||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin />
|
||||
<link href="app.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
<h1>Dewemoji</h1>
|
||||
<p>If this screen appears, the app could not load <code>https://dewemoji.com</code>. Check internet connection and try again.</p>
|
||||
<body class="theme-dark">
|
||||
<header class="hdr">
|
||||
<h1 class="ttl">Find Your Emoji <span id="dewemoji-status" class="tag free">Guest</span></h1>
|
||||
<div class="bar">
|
||||
<input id="q" class="inp" type="search" placeholder="Search (e.g. love)" aria-label="Search emojis" />
|
||||
<button id="clear" class="btn icon" title="Clear" aria-label="Clear">✕</button>
|
||||
<button id="theme" class="btn icon" title="Toggle theme" aria-label="Toggle theme">🌙</button>
|
||||
<button id="settings" class="btn icon" title="Settings" aria-label="Settings">⚙️</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="filters">
|
||||
<select id="cat" class="sel" aria-label="Category filter">
|
||||
<option value="">All categories</option>
|
||||
</select>
|
||||
<select id="sub" class="sel" aria-label="Subcategory filter" disabled>
|
||||
<option value="">All subcategories</option>
|
||||
</select>
|
||||
</section>
|
||||
|
||||
<main id="list" class="grid" aria-live="polite"></main>
|
||||
<div id="autoload-sentinel" class="autoload-sentinel" aria-hidden="true"></div>
|
||||
|
||||
<footer class="ft">
|
||||
<button id="more" class="btn w">Load more</button>
|
||||
<span id="count" class="muted nowrap">0 items</span>
|
||||
<span id="ver" class="badge nowrap">APK</span>
|
||||
</footer>
|
||||
|
||||
<div id="sheet-backdrop" class="backdrop" hidden></div>
|
||||
<aside id="settings-sheet" class="sheet" hidden role="dialog" aria-modal="true" aria-labelledby="settings-title">
|
||||
<div class="sheet-head">
|
||||
<h3 id="settings-title">Settings</h3>
|
||||
<button id="sheet-close" class="btn icon" title="Close" aria-label="Close">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="sheet-body">
|
||||
<div class="tabs" role="tablist" aria-label="Settings tabs">
|
||||
<button class="tab active" data-tab="general" role="tab" aria-selected="true">General</button>
|
||||
<button class="tab" data-tab="account" role="tab" aria-selected="false">Account</button>
|
||||
<button class="tab" data-tab="bubble" role="tab" aria-selected="false">Bubble</button>
|
||||
</div>
|
||||
|
||||
<section id="tab-general" class="tabpane active" role="tabpanel">
|
||||
<div class="field">
|
||||
<label class="lbl">Instant Access</label>
|
||||
<p class="hint muted">Tap an emoji to copy. Then switch back and paste in the app you are typing in.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="lbl">Current Session</label>
|
||||
<div class="diagbox" id="session-summary">Guest mode. Public keywords only.</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="lbl">Grid display</label>
|
||||
<label class="radio row-inline" for="show-emoji-names-toggle">
|
||||
<input id="show-emoji-names-toggle" type="checkbox" />
|
||||
<span>Show emoji names under each emoji</span>
|
||||
</label>
|
||||
<p class="hint muted">Off by default for larger emoji and faster scanning.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="lbl">Skin tone</label>
|
||||
<label class="radio row-inline" for="tone-lock-toggle">
|
||||
<input id="tone-lock-toggle" type="checkbox" />
|
||||
<span>Use preferred skin tone on tap (tone lock)</span>
|
||||
</label>
|
||||
<div class="row row-block" style="margin-top:8px;">
|
||||
<div id="preferred-skin-tone-radios" class="tone-pref-group" role="radiogroup" aria-label="Preferred skin tone">
|
||||
<label class="tone-swatch" title="Default (no tone)">
|
||||
<input type="radio" name="preferredSkinTone" value="0" aria-label="Default (no tone)" />
|
||||
<span class="tone-circle tone-default" aria-hidden="true"></span>
|
||||
</label>
|
||||
<label class="tone-swatch" title="Light skin tone">
|
||||
<input type="radio" name="preferredSkinTone" value="1" aria-label="Light skin tone" />
|
||||
<span class="tone-circle tone-1" aria-hidden="true"></span>
|
||||
</label>
|
||||
<label class="tone-swatch" title="Medium-light skin tone">
|
||||
<input type="radio" name="preferredSkinTone" value="2" aria-label="Medium-light skin tone" />
|
||||
<span class="tone-circle tone-2" aria-hidden="true"></span>
|
||||
</label>
|
||||
<label class="tone-swatch" title="Medium skin tone">
|
||||
<input type="radio" name="preferredSkinTone" value="3" aria-label="Medium skin tone" />
|
||||
<span class="tone-circle tone-3" aria-hidden="true"></span>
|
||||
</label>
|
||||
<label class="tone-swatch" title="Medium-dark skin tone">
|
||||
<input type="radio" name="preferredSkinTone" value="4" aria-label="Medium-dark skin tone" />
|
||||
<span class="tone-circle tone-4" aria-hidden="true"></span>
|
||||
</label>
|
||||
<label class="tone-swatch" title="Dark skin tone">
|
||||
<input type="radio" name="preferredSkinTone" value="5" aria-label="Dark skin tone" />
|
||||
<span class="tone-circle tone-5" aria-hidden="true"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint muted">Long press an emoji with skin tones to choose a tone quickly.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="lbl">Refresh catalog</label>
|
||||
<div class="row row-inline">
|
||||
<button id="refresh" class="btn">Refresh</button>
|
||||
<span id="api-status" class="muted">Loading…</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="tab-account" class="tabpane" role="tabpanel">
|
||||
<div class="field">
|
||||
<label class="lbl">Account</label>
|
||||
<div id="account-connect-form">
|
||||
<div class="row row-block">
|
||||
<input id="account-email" class="inp" placeholder="Email" type="email" autocomplete="username" />
|
||||
</div>
|
||||
<div class="row row-block" style="margin-top:6px;">
|
||||
<input id="account-password" class="inp" placeholder="Password" type="password" autocomplete="current-password" />
|
||||
</div>
|
||||
<div class="row row-inline" style="margin-top:8px;">
|
||||
<button id="account-login" class="btn">Connect</button>
|
||||
</div>
|
||||
<div class="row row-inline" style="margin-top:6px;">
|
||||
<span id="account-status" class="muted">Not connected. Public keywords only.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="account-connected" style="display:none;">
|
||||
<div class="row row-inline">
|
||||
<span id="account-greeting" class="muted">Connected.</span>
|
||||
</div>
|
||||
<div class="row row-inline" style="margin-top:8px;">
|
||||
<button id="account-logout" class="btn ghost">Logout</button>
|
||||
</div>
|
||||
<div class="row row-inline" style="margin-top:6px;">
|
||||
<span class="muted">Private keyword matches appear in search when available.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="tab-bubble" class="tabpane" role="tabpanel">
|
||||
<div class="field">
|
||||
<label class="lbl">Floating bubble (quick search + copy)</label>
|
||||
<p class="hint muted">Use Dewemoji bubble to quickly search and copy emoji while using other apps. Paste manually in the app you’re typing in.</p>
|
||||
</div>
|
||||
|
||||
<div class="bubble-grid">
|
||||
<div class="bubble-kv"><span class="muted">Platform</span><span id="bubble-platform-status">Checking…</span></div>
|
||||
<div class="bubble-kv"><span class="muted">Overlay permission</span><span id="bubble-overlay-status">Checking…</span></div>
|
||||
<div class="bubble-kv"><span class="muted">Notifications</span><span id="bubble-notify-status">Checking…</span></div>
|
||||
<div class="bubble-kv"><span class="muted">Bubble service</span><span id="bubble-running-status">Checking…</span></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="row row-wrap">
|
||||
<button id="bubble-enable-btn" class="btn">Enable bubble</button>
|
||||
<button id="bubble-disable-btn" class="btn ghost">Disable bubble</button>
|
||||
</div>
|
||||
<div class="row row-wrap" style="margin-top:8px;">
|
||||
<button id="bubble-overlay-settings-btn" class="btn ghost">Grant overlay permission</button>
|
||||
<button id="bubble-notify-settings-btn" class="btn ghost">Open notification settings</button>
|
||||
</div>
|
||||
<div class="diagbox" id="bubble-help" style="margin-top:8px;">Bubble is off by default. It opens Dewemoji for quick search and copy.</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||
|
||||
<script src="twemoji-lite.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
31
dewemoji-capacitor/www/twemoji-lite.js
Normal file
31
dewemoji-capacitor/www/twemoji-lite.js
Normal file
@@ -0,0 +1,31 @@
|
||||
(() => {
|
||||
const root = window;
|
||||
if (root?.twemoji?.convert?.toCodePoint) return;
|
||||
|
||||
function toCodePoint(unicodeSurrogates, sep = '-') {
|
||||
const r = [];
|
||||
let c = 0;
|
||||
let p = 0;
|
||||
let i = 0;
|
||||
const s = String(unicodeSurrogates || '');
|
||||
|
||||
while (i < s.length) {
|
||||
c = s.charCodeAt(i++);
|
||||
if (p) {
|
||||
r.push((0x10000 + ((p - 0xD800) * 0x400) + (c - 0xDC00)).toString(16));
|
||||
p = 0;
|
||||
} else if (c >= 0xD800 && c <= 0xDBFF) {
|
||||
p = c;
|
||||
} else {
|
||||
r.push(c.toString(16));
|
||||
}
|
||||
}
|
||||
|
||||
return r.join(sep);
|
||||
}
|
||||
|
||||
root.twemoji = root.twemoji || {};
|
||||
root.twemoji.convert = root.twemoji.convert || {};
|
||||
root.twemoji.convert.toCodePoint = toCodePoint;
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user