1+ package community .dcts .app ;
2+
3+ import android .app .Activity ;
4+ import android .app .AlertDialog ;
5+ import android .content .Context ;
6+ import android .content .SharedPreferences ;
7+ import android .graphics .Color ;
8+ import android .graphics .Typeface ;
9+ import android .graphics .drawable .GradientDrawable ;
10+ import android .view .Gravity ;
11+ import android .view .View ;
12+ import android .view .ViewGroup ;
13+ import android .widget .ImageView ;
14+ import android .widget .LinearLayout ;
15+ import android .widget .TextView ;
16+
17+ import org .json .JSONArray ;
18+ import org .json .JSONObject ;
19+
20+ import java .util .ArrayList ;
21+ import java .util .Iterator ;
22+ import java .util .List ;
23+
24+ public class Accounts {
25+
26+ private static final String PREFS = "dcts_accounts" ;
27+ private final SharedPreferences prefs ;
28+ private final Context context ;
29+
30+ public interface OnPick { void onAccount (JSONObject account ); }
31+ public interface OnScan { void onScanRequested (); }
32+
33+ public Accounts (Context context ) {
34+ this .context = context ;
35+ this .prefs = context .getSharedPreferences (PREFS , Context .MODE_PRIVATE );
36+ }
37+
38+ private String norm (String url ) {
39+ if (url == null ) return "" ;
40+ url = url .toLowerCase ().trim ();
41+ url = url .replaceFirst ("^https?://" , "" );
42+ while (url .endsWith ("/" )) url = url .substring (0 , url .length () - 1 );
43+ return url ;
44+ }
45+
46+ private JSONObject loadAll () {
47+ try {
48+ return new JSONObject (prefs .getString ("data" , "{}" ));
49+ } catch (Exception e ) {
50+ return new JSONObject ();
51+ }
52+ }
53+
54+ private JSONArray load (String url ) {
55+ JSONArray arr = loadAll ().optJSONArray (url );
56+ return arr != null ? arr : new JSONArray ();
57+ }
58+
59+ private void write (String url , JSONArray list ) {
60+ try {
61+ JSONObject all = loadAll ();
62+
63+ if (list .length () == 0 ) all .remove (url );
64+ else all .put (url , list );
65+
66+ prefs .edit ().putString ("data" , all .toString ()).apply ();
67+ } catch (Exception e ) {
68+ e .printStackTrace ();
69+ }
70+ }
71+
72+ private int findIndex (JSONArray list , String id ) {
73+ for (int i = 0 ; i < list .length (); i ++) {
74+ try {
75+ if (id .equals (list .getJSONObject (i ).optString ("id" ))) return i ;
76+ } catch (Exception ignored ) {}
77+ }
78+ return -1 ;
79+ }
80+
81+ public void save (String url , JSONObject account ) {
82+ url = norm (url );
83+
84+ try {
85+ JSONArray list = load (url );
86+ String id = account .getString ("id" );
87+ int idx = findIndex (list , id );
88+
89+ if (idx >= 0 ) list .put (idx , account );
90+ else list .put (account );
91+
92+ write (url , list );
93+ } catch (Exception e ) {
94+ e .printStackTrace ();
95+ }
96+ }
97+
98+ public void delete (String url , String id ) {
99+ url = norm (url );
100+ JSONArray list = load (url );
101+ JSONArray filtered = new JSONArray ();
102+
103+ for (int i = 0 ; i < list .length (); i ++) {
104+ try {
105+ if (!id .equals (list .getJSONObject (i ).optString ("id" )))
106+ filtered .put (list .getJSONObject (i ));
107+ } catch (Exception ignored ) {}
108+ }
109+
110+ write (url , filtered );
111+ }
112+
113+ public JSONObject get (String url , String id ) {
114+ url = norm (url );
115+ JSONArray list = load (url );
116+ int idx = findIndex (list , id );
117+
118+ if (idx >= 0 ) {
119+ try { return list .getJSONObject (idx ); }
120+ catch (Exception ignored ) {}
121+ }
122+
123+ return null ;
124+ }
125+
126+ public List <JSONObject > getAll (String url ) {
127+ url = norm (url );
128+ JSONArray list = load (url );
129+ List <JSONObject > result = new ArrayList <>();
130+
131+ for (int i = 0 ; i < list .length (); i ++) {
132+ try { result .add (list .getJSONObject (i )); }
133+ catch (Exception ignored ) {}
134+ }
135+
136+ return result ;
137+ }
138+
139+ private List <JSONObject > collectAllAccounts (List <String > origins ) {
140+ List <JSONObject > items = new ArrayList <>();
141+ List <String > seenIds = new ArrayList <>();
142+
143+ try {
144+ JSONObject all = loadAll ();
145+ Iterator <String > keys = all .keys ();
146+
147+ while (keys .hasNext ()) {
148+ String url = keys .next ();
149+ JSONArray list = all .getJSONArray (url );
150+
151+ for (int i = 0 ; i < list .length (); i ++) {
152+ JSONObject acc = list .getJSONObject (i );
153+ String accId = acc .optString ("id" );
154+
155+ if (seenIds .contains (accId )) continue ;
156+
157+ seenIds .add (accId );
158+ items .add (acc );
159+ origins .add (url );
160+ }
161+ }
162+ } catch (Exception ignored ) {}
163+
164+ return items ;
165+ }
166+
167+ private View buildAvatar (String icon , String name ) {
168+ GradientDrawable circle = new GradientDrawable ();
169+ circle .setShape (GradientDrawable .OVAL );
170+ circle .setColor (Color .parseColor ("#000000" ));
171+ circle .setSize (96 , 96 );
172+
173+ if (icon != null && !icon .isEmpty () && icon .startsWith ("https" )) {
174+ ImageView img = new ImageView (context );
175+ img .setScaleType (ImageView .ScaleType .CENTER_CROP );
176+ img .setLayoutParams (new LinearLayout .LayoutParams (96 , 96 ));
177+ img .setBackground (circle );
178+ img .setClipToOutline (true );
179+
180+ new Thread (() -> {
181+ try {
182+ java .io .InputStream in = new java .net .URL (icon ).openStream ();
183+ android .graphics .Bitmap bmp = android .graphics .BitmapFactory .decodeStream (in );
184+ ((Activity ) context ).runOnUiThread (() -> img .setImageBitmap (bmp ));
185+ } catch (Exception ignored ) {}
186+ }).start ();
187+
188+ return img ;
189+ }
190+
191+ TextView letter = new TextView (context );
192+ letter .setBackground (circle );
193+ letter .setText (name .substring (0 , 1 ).toUpperCase ());
194+ letter .setTextColor (Color .WHITE );
195+ letter .setTextSize (18 );
196+ letter .setGravity (Gravity .CENTER );
197+ letter .setWidth (96 );
198+ letter .setHeight (96 );
199+
200+ return letter ;
201+ }
202+
203+ private LinearLayout buildInfoColumn (String name , String status ) {
204+ LinearLayout info = new LinearLayout (context );
205+ info .setOrientation (LinearLayout .VERTICAL );
206+ info .setPadding (24 , 0 , 0 , 0 );
207+
208+ TextView nameView = new TextView (context );
209+ nameView .setText (name );
210+ nameView .setTextSize (16 );
211+ nameView .setTypeface (null , Typeface .BOLD );
212+ info .addView (nameView );
213+
214+ if (status != null && !status .isEmpty ()) {
215+ TextView statusView = new TextView (context );
216+ statusView .setText (status );
217+ statusView .setTextSize (13 );
218+ statusView .setAlpha (0.6f );
219+ info .addView (statusView );
220+ }
221+
222+ return info ;
223+ }
224+
225+ private TextView buildDeleteButton (String origin , String id , String name ,
226+ String currentUrl , AlertDialog [] dialogRef ,
227+ OnPick onPick , OnScan onScan ) {
228+
229+ TextView btn = new TextView (context );
230+ btn .setText ("\u2715 " );
231+ btn .setTextSize (18 );
232+ btn .setTextColor (Color .parseColor ("#FF4444" ));
233+ btn .setPadding (24 , 0 , 0 , 0 );
234+
235+ btn .setOnClickListener (v -> {
236+ new AlertDialog .Builder (context )
237+ .setMessage ("Remove " + name + "?" )
238+ .setPositiveButton ("Remove" , (d , w ) -> {
239+ delete (origin , id );
240+ if (dialogRef [0 ] != null ) dialogRef [0 ].dismiss ();
241+ pick (currentUrl , onPick , onScan );
242+ })
243+ .setNegativeButton ("Cancel" , null )
244+ .show ();
245+ });
246+
247+ return btn ;
248+ }
249+
250+ private LinearLayout buildScanButton () {
251+ LinearLayout scanBtn = new LinearLayout (context );
252+ scanBtn .setOrientation (LinearLayout .HORIZONTAL );
253+ scanBtn .setGravity (Gravity .CENTER_VERTICAL );
254+ scanBtn .setPadding (48 , 24 , 48 , 24 );
255+
256+ TextView icon = new TextView (context );
257+ icon .setText ("\uD83D \uDCF7 " );
258+ icon .setTextSize (20 );
259+ scanBtn .addView (icon );
260+
261+ TextView label = new TextView (context );
262+ label .setText ("Scan QR Code" );
263+ label .setTextSize (16 );
264+ label .setPadding (24 , 0 , 0 , 0 );
265+ scanBtn .addView (label );
266+
267+ return scanBtn ;
268+ }
269+
270+ private LinearLayout buildAccountRow (JSONObject account , String origin , String currentUrl ,
271+ AlertDialog [] dialogRef , OnPick onPick , OnScan onScan ) {
272+
273+ String id = account .optString ("id" );
274+ String icon = account .optString ("icon" );
275+ String name = account .optString ("name" , account .optString ("loginName" , "?" ));
276+ String status = account .optString ("status" , "" );
277+ boolean foreign = !origin .equals (currentUrl );
278+
279+ LinearLayout row = new LinearLayout (context );
280+ row .setOrientation (LinearLayout .HORIZONTAL );
281+ row .setGravity (Gravity .CENTER_VERTICAL );
282+ row .setPadding (48 , 28 , 48 , 28 );
283+
284+ if (foreign ) row .setAlpha (0.6f );
285+
286+ row .addView (buildAvatar (icon , name ));
287+
288+ String displayName = name ;
289+ LinearLayout info = buildInfoColumn (displayName , status );
290+ row .addView (info , new LinearLayout .LayoutParams (0 , ViewGroup .LayoutParams .WRAP_CONTENT , 1 ));
291+
292+ row .addView (buildDeleteButton (origin , id , name , currentUrl , dialogRef , onPick , onScan ));
293+
294+ row .setOnClickListener (v -> {
295+ if (dialogRef [0 ] != null ) dialogRef [0 ].dismiss ();
296+
297+ try {
298+ JSONObject safe = new JSONObject (account .toString ());
299+ if (foreign ) safe .remove ("token" );
300+ onPick .onAccount (safe );
301+ } catch (Exception ignored ) {}
302+ });
303+
304+ return row ;
305+ }
306+
307+ public void pick (String currentUrl , OnPick onPick , OnScan onScan ) {
308+ currentUrl = norm (currentUrl );
309+ String finalUrl = currentUrl ;
310+
311+ List <String > origins = new ArrayList <>();
312+ List <JSONObject > items = collectAllAccounts (origins );
313+
314+ LinearLayout root = new LinearLayout (context );
315+ root .setOrientation (LinearLayout .VERTICAL );
316+ root .setPadding (0 , 24 , 0 , 0 );
317+
318+ LinearLayout scanBtn = buildScanButton ();
319+ root .addView (scanBtn );
320+
321+ View divider = new View (context );
322+ divider .setBackgroundColor (Color .parseColor ("#33888888" ));
323+ root .addView (divider , new LinearLayout .LayoutParams (ViewGroup .LayoutParams .MATCH_PARENT , 2 ));
324+
325+ AlertDialog [] dialogRef = new AlertDialog [1 ];
326+
327+ for (int i = 0 ; i < items .size (); i ++) {
328+ root .addView (buildAccountRow (items .get (i ), origins .get (i ), finalUrl , dialogRef , onPick , onScan ));
329+ }
330+
331+ if (items .isEmpty ()) {
332+ TextView empty = new TextView (context );
333+ empty .setText ("No accounts yet" );
334+ empty .setGravity (Gravity .CENTER );
335+ empty .setPadding (48 , 64 , 48 , 64 );
336+ empty .setAlpha (0.5f );
337+ root .addView (empty );
338+ }
339+
340+ scanBtn .setOnClickListener (v -> {
341+ if (dialogRef [0 ] != null ) dialogRef [0 ].dismiss ();
342+ onScan .onScanRequested ();
343+ });
344+
345+ dialogRef [0 ] = new AlertDialog .Builder (context )
346+ .setTitle ("Pick Account" )
347+ .setView (root )
348+ .setNegativeButton ("Cancel" , null )
349+ .show ();
350+ }
351+ }
0 commit comments