123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669 |
- #define WV_DEBUG
- using Android.App;
- using Android.OS;
- using Android.Util;
- using Android.Views;
- using Android.Webkit;
- using Java.Interop;
- using Java.Lang;
- using System;
- using System.IO;
- using System.Linq;
- using System.Threading.Tasks;
- using Android;
- using Android.Content;
- using Android.Content.PM;
- using Android.Widget;
- namespace LnoidWv
- {
- using JsLab.JsApi;
- // ReSharper disable once ClassNeverInstantiated.Global
- [Activity(Label = "!轻离阅", MainLauncher = true, Icon = "@drawable/icon")]
- public partial class MainActivity : Activity { }
- public class NativeFunctions : JsApiBase
- {
- [JsApi]
- // ReSharper disable once UnusedMember.Global
- public bool PathExist(string path)
- {
- return Directory.Exists(path);
- }
- [JsApi]
- // ReSharper disable once UnusedMember.Global
- public object GetDirs(string path, bool full = false)
- {
- var directories = Directory.GetDirectories(path);
- var array = full
- ? directories
- : directories.Select(Path.GetFileName).ToArray();
- Array.Sort(array);
- return array;
- }
- [JsApi]
- // ReSharper disable once UnusedMember.Global
- public object GetPrefixes(string dataPath, Action<object> cbProcess = null)
- {
- var directories = Directory.GetDirectories(dataPath);
- Array.Sort(directories);
- var progress = cbProcess == null
- ? new Action<int>(delegate { })
- : p => cbProcess(new { Total = directories.Length, Current = p });
- return directories.Select(delegate (string prefixPath, int index)
- {
- var foo = new
- {
- Path = prefixPath,
- Prefix = Path.GetFileName(prefixPath)?.ToUpper(),
- SeriesCount = Directory.GetDirectories(prefixPath).Length,
- };
- progress(index + 1);
- return foo;
- }).ToArray();
- }
- [JsApi]
- // ReSharper disable once UnusedMember.Global
- public object GetSeries(string prefixPath, Action<object> cbProcess = null)
- {
- var directories = Directory.GetDirectories(prefixPath);
- Array.Sort(directories);
- var progress = cbProcess == null
- ? new Action<int>(delegate { })
- : p => cbProcess(new { Total = directories.Length, Current = p });
- return directories
- .OrderBy(p => p)
- .Select((seriesPath, index) =>
- {
- var subDirs = Directory.GetDirectories(seriesPath).OrderBy(p => p).ToArray();
- var obj = new
- {
- Path = seriesPath,
- SeriesName = File.ReadAllText(Path.Combine(seriesPath, "sname.txt")),
- VolumeCount = subDirs.Length,
- CoverData = subDirs
- .Select(s => Path.Combine(s, "cover.jpg")).Where(File.Exists)
- .Select(p => "data:image/jpeg;base64," + Convert.ToBase64String(File.ReadAllBytes(p)))
- .FirstOrDefault()
- };
- progress(index + 1);
- return obj;
- }).ToArray();
- }
- [JsApi]
- // ReSharper disable once UnusedMember.Global
- public object GetVolumes(string seriesPath, Action<object> cbProcess = null)
- {
- var directories = Directory.GetDirectories(seriesPath);
- Array.Sort(directories);
- var progress = cbProcess == null
- ? new Action<int>(delegate { })
- : p => cbProcess(new { Total = directories.Length, Current = p });
- return directories.OrderBy(p => p)
- .Select((volumePath, index) =>
- {
- var b64 = Convert.ToBase64String(File.ReadAllBytes(Path.Combine(volumePath, "cover.jpg")), Base64FormattingOptions.None);
- var obj = new
- {
- Path = volumePath,
- VolumeName = File.ReadAllText(Path.Combine(volumePath, "vname.txt")),
- ChapterCount = Directory.GetDirectories(volumePath).Length,
- CoverData = $"data:image/jpeg;base64,{b64}",
- };
- progress(index + 1);
- return obj;
- }).ToArray();
- }
- [JsApi]
- // ReSharper disable once UnusedMember.Global
- public object GetChapters(string volumePath, Action<object> cbProcess = null)
- {
- var directories = Directory.GetDirectories(volumePath);
- Array.Sort(directories);
- var progress = cbProcess == null
- ? new Action<int>(delegate { })
- : p => cbProcess(new { Total = directories.Length, Current = p });
- return directories
- .OrderBy(p => p)
- .Select((chapterPath, index) =>
- {
- var pictures1 = Directory.GetFiles(chapterPath, "*.jpg");
- var pictures2 = Directory.GetFiles(chapterPath, "*.JPG");
- var firstBase64 = pictures1.Concat(pictures2)
- .OrderBy(filename => filename)
- .Select(filename => "data:image/jpeg;base64," + Convert.ToBase64String(File.ReadAllBytes(filename), Base64FormattingOptions.None))
- .FirstOrDefault();
- var obj = new
- {
- Path = chapterPath,
- ChapterName = File.ReadAllText(Path.Combine(chapterPath, "cname.txt")),
- FirstPicture = firstBase64
- };
- progress(index + 1);
- return obj;
- }).ToArray();
- }
- [JsApi]
- // ReSharper disable once UnusedMember.Global
- public object GetChapterContent(string chapterPath, Action<object> cbProcess = null)
- {
- var lines = File.ReadAllLines(Path.Combine(chapterPath, "ctext.txt"));
- var progress = cbProcess == null
- ? new Action<int>(delegate { })
- : p => cbProcess(new { Total = lines.Length, Current = p });
- for (var i = 0; i < lines.Length; i++)
- {
- var line = lines[i];
- if (!line.StartsWith("<img")) continue;
- var picnam = System.Web.HttpUtility.HtmlDecode(line)
- .Replace("<img src=\"hybrid://pic?", "")
- .Replace("\"/>", "")
- ;
- var picabs = Path.Combine(chapterPath, picnam);
- var b64 = Convert.ToBase64String(File.ReadAllBytes(picabs), Base64FormattingOptions.None);
- lines[i] = $"<img src='data:image/jpeg;base64,{b64}' />";
- progress(i + 1);
- }
- return lines;
- }
- [JsApi]
- // ReSharper disable once UnusedMember.Global
- public string ReadSetting()
- {
- #if !Android22
- if (Android.OS.Environment.IsExternalStorageManager == false)
- {
- var uri = Android.Net.Uri.Parse("package:" + Application.Context.ApplicationInfo.PackageName);
- var intent = new Intent(Android.Provider.Settings.ActionManageAppAllFilesAccessPermission, uri);
- _mainActivity.StartActivityForResult(intent, 9527);
- }
- #endif
- var localStorage = _mainActivity.Application.FilesDir.Path;
- var configFilePath = Path.Combine(localStorage, "config.json");
- return File.Exists(configFilePath)
- ? File.ReadAllText(configFilePath)
- : null;
- }
- [JsApi]
- // ReSharper disable once UnusedMember.Global
- public void WriteSetting(string json)
- {
- var localStorage = _mainActivity.Application.FilesDir.Path;
- var configFilePath = Path.Combine(localStorage, "config.json");
- File.WriteAllText(configFilePath, json);
- }
- [JsApi]
- // ReSharper disable once UnusedMember.Global
- public void ExitApp()
- {
- JavaSystem.Exit(0);
- }
- [JsApi]
- // ReSharper disable once UnusedMember.Global
- public void WriteLog(string content)
- {
- Log.Debug($"{_mainActivity.Application.PackageName}::JsApi::{nameof(WriteLog)}", content);
- }
- [JsApi]
- // ReSharper disable once UnusedMember.Global
- public void CopyText(string text)
- {
- var myClipboard = (ClipboardManager)_mainActivity.GetSystemService(Context.ClipboardService);
- var myClip = ClipData.NewPlainText("text", text);
- myClipboard.PrimaryClip = myClip;
- Toast.MakeText(_mainActivity.Application, "文本已复制", ToastLength.Short).Show();
- }
- private readonly MainActivity _mainActivity;
- public NativeFunctions(MainActivity mainActivity, string jsBindName) : base(jsBindName)
- {
- _mainActivity = mainActivity;
- }
- protected override void CallJs(string code)
- {
- _mainActivity.RunOnUiThread(() => _mainActivity.CallJs(code));
- }
- #if DEBUG || WV_DEBUG
- [JsApi]
- // ReSharper disable once UnusedMember.Global
- public void ReLoad()
- {
- _mainActivity.RunOnUiThread(() => _mainActivity.ReLoad());
- }
- #endif
- // TODO: UI Config - GridSize(0.5x-10x) - TextSize(5-127) - UiZoom(25-500%) - TextZoom(25-500%) - PictureMode(Auto,FitW/H)
- // TODO: Fav/Continue (Chapter Entry) -> (Content Scroll Percent)
- // TODO: State S/L -- Restore on back
- // TODO: Read Zip file directly
- // TODO: Indexing for Improve load speed
- // BUG: DON'T Reload after screen rotated
- // TODO: Implement more functions to improve this app.
- }
- // ReSharper disable once ClassNeverInstantiated.Global
- public partial class MainActivity
- {
- private const string MainUrl =
- //#if DEBUG
- // "http://192.168.23.97/main.html"
- // // "http://192.168.1.233/main.html"
- // // "http://lnoidebugserver/main.html"
- //#else
- "file:///android_asset/main.html"
- //#endif
- ;
- private const string JsBindName = "LnoidNativeFunctions";
- private WebView _view;
- protected override void OnCreate(Bundle bundle)
- {
- base.OnCreate(bundle);
- #if !Android22
- if (CheckSelfPermission(Manifest.Permission.ReadExternalStorage) != Permission.Granted)
- RequestPermissions(new[] { Manifest.Permission.ReadExternalStorage }, 123321123); // is an app-defined int constant that should be quite unique
- //var uri = Android.Net.Uri.Parse("package:" + Application.Context.ApplicationInfo.PackageName);
- //var intent = new Intent(Android.Provider.Settings.ActionManageAppAllFilesAccessPermission, uri, ApplicationContext, GetType());
- //StartActivity(intent);
- #endif
- RequestWindowFeature(WindowFeatures.NoTitle);
- #if DEBUG || WV_DEBUG
- #if !Android22
- if (Build.VERSION.SdkInt >= BuildVersionCodes.Kitkat)
- WebView.SetWebContentsDebuggingEnabled(true);
- #else
- WebView.EnablePlatformNotifications();
- #endif
- #else
- #if !Android22
- if (Build.VERSION.SdkInt >= BuildVersionCodes.Kitkat)
- WebView.SetWebContentsDebuggingEnabled(false);
- #endif
- #endif
- _view = new WebView(this);
- _view.SetWebChromeClient(new WebChromeClient());
- #pragma warning disable 618
- _view.LayoutParameters = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FillParent, ViewGroup.LayoutParams.FillParent);
- #pragma warning restore 618
- _view.SetPadding(0, 0, 0, 0);
- SetContentView(_view);
- _view.SetInitialScale(100);
- _view.Settings.JavaScriptEnabled = true;
- var nativeFunctions = new NativeFunctions(this, JsBindName);
- _view.AddJavascriptInterface(nativeFunctions, JsBindName);
- _view.LoadUrl(MainUrl);
- }
- internal void CallJs(string code)
- {
- _view.LoadUrl($"javascript:{code}");
- }
- public override void OnBackPressed()
- {
- CallJs("__backButton_Pressed();");
- }
- public override bool OnKeyUp(Keycode keyCode, KeyEvent e)
- {
- if (keyCode == Keycode.Menu)
- {
- CallJs("__menuButton_Pressed();");
- return true;
- }
- return base.OnKeyUp(keyCode, e);
- }
- #if WV_DEBUG
- public void ReLoad()
- {
- _view.LoadUrl(MainUrl);
- }
- #endif
- }
- }
- // ReSharper disable once CheckNamespace
- namespace JsLab.JsApi
- {
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Reflection;
- public abstract class JsApiBase : Java.Lang.Object
- {
- private readonly string _jsBindName;
- private readonly IReadOnlyDictionary<string, JsApiMethod> _jsApiMethodDictionary;
- [AttributeUsage(AttributeTargets.Method)]
- protected sealed class JsApiAttribute : Attribute
- {
- }
- // ReSharper disable UnusedAutoPropertyAccessor.Local
- // ReSharper disable ClassNeverInstantiated.Local
- private sealed class InvokeRequestModel
- {
- public string MethodName { get; set; }
- public object[] Arguments { get; set; }
- }
- private sealed class InvokeRequestCallback
- {
- public string CallBack { get; set; }
- }
- // ReSharper restore ClassNeverInstantiated.Local
- // ReSharper restore UnusedAutoPropertyAccessor.Local
- private class JsApiMethod
- {
- public MethodInfo MethodInfo { get; }
- public IReadOnlyList<JsApiParmeter> ParameterInfos { get; }
- public JsApiMethod(MethodInfo methodInfo, IReadOnlyList<JsApiParmeter> parameterInfos)
- {
- MethodInfo = methodInfo;
- ParameterInfos = parameterInfos;
- }
- }
- private class JsApiParmeter
- {
- public string Name { get; }
- public bool IsCallback { get; }
- public JsApiParmeter(string name, bool isCallback)
- {
- Name = name;
- IsCallback = isCallback;
- }
- }
- protected JsApiBase(string jsBindName)
- {
- var myType = GetType();
- var tObjectAction = typeof(Action<object>);
- _jsApiMethodDictionary = myType.GetMethods()
- .Where(p => p.IsDefined(typeof(JsApiAttribute), true))
- .Select(p => new JsApiMethod(
- p,
- p.GetParameters()
- .Select(q => new JsApiParmeter(q.Name, q.ParameterType == tObjectAction))
- .ToArray()
- ))
- .ToDictionary(p => p.MethodInfo.Name);
- _jsBindName = jsBindName;
- }
- [Export]
- #if !Android22
- [JavascriptInterface]
- #endif
- // ReSharper disable once MemberCanBePrivate.Global
- public string GenerateFunctionJsonStub()
- {
- var methods = _jsApiMethodDictionary.Values.ToArray();
- // ReSharper disable once UseObjectOrCollectionInitializer
- var list = new List<string>(methods.Length);
- list.Add("$mkCallback:function(c,r){" +
- "if(typeof c!=='function') "
- + "return undefined;" +
- "if(!this.$cbIdx)"
- + "this.$cbIdx=1;" +
- "else "
- + "this.$cbIdx++;" +
- "var cbName='__cb_'+this.$cbIdx;" +
- "window[cbName]=function(d){"
- + "if(r)delete window[cbName];"
- + "c(d);" +
- "};" +
- "return cbName;" +
- "}");
- foreach (var jsApiMethod in methods)
- {
- var method = jsApiMethod.MethodInfo;
- var parameterInfos = jsApiMethod.ParameterInfos;
- var lstArgs = parameterInfos.Select((p, i) => "p" + i + "_" + p.Name).ToList();
- var argList = string.Join(",", lstArgs);
- var fnSync =
- $"{method.Name}:function({argList})" + "{" +
- "var arg={" + $"MethodName:'{method.Name}',Arguments:[{argList}]" + "};" +
- "var argson=JSON.stringify(arg);" +
- $"return JSON.parse({_jsBindName}.InvokeInterface(argson));" + "}";
- lstArgs.Insert(0, "p_callback");
- var argListAsync = string.Join(",", lstArgs);
- //Async version with callback
- var fnAsync =
- $"{method.Name}Async:function({argListAsync})" + "{" +
- $"if(!p_callback) throw 'callback are required.[{method.Name}Async]';" +
- "var args=[];";
- var lstCbToDel = new List<string>(parameterInfos.Count);
- for (var i = 0; i < parameterInfos.Count; i++)
- {
- var item = parameterInfos[i];
- var pName = lstArgs[i + 1];// skip p_callback
- if (item.IsCallback)
- {
- fnAsync +=
- $"var cbName_{i}='undefined';" +
- $"if(typeof {pName}!=='function')" + "{"
- + "args.push(null);" +
- "}else{"
- + $"cbName_{i}=this.$mkCallback({pName},false);"
- + $"args.push(cbName_{i});" +
- "}";
- lstCbToDel.Add($"cbName_{i}");
- }
- else
- {
- fnAsync += $"args.push({pName});";
- }
- }
- fnAsync +=
- "var cbName=this.$mkCallback(function(retr){"
- + string.Join("", lstCbToDel.Select(p => $"delete window[{p}];"))
- + "p_callback(retr);" +
- "},true);" +
- "var arg={" + $"MethodName:'{method.Name}'" + ",Arguments:args" + ",CallBack:cbName" + "};" +
- "var argson=JSON.stringify(arg);" +
- $"{_jsBindName}.InvokeInterfaceAsync(argson)" +
- // cancel token handle: too lazy to implement!
- "}";
- list.Add(fnSync);
- list.Add(fnAsync);
- }
- return "{" + string.Join(",", list) + "}";
- }
- [Export]
- #if !Android22
- [JavascriptInterface]
- #endif
- // ReSharper disable once UnusedMember.Global
- public string GenerateFunctionStubForEval()
- {
- var ret = $"({GenerateFunctionJsonStub()})";
- return ret;
- }
- [Export]
- #if !Android22
- [JavascriptInterface]
- #endif
- // ReSharper disable once UnusedMember.Global
- public void InvokeInterfaceAsync(string json)
- {
- // cancel token handle: too lazy to implement!
- #if Android22
- var im = fastJSON.JSON.ToObject<InvokeRequestCallback>(json);
- #else
- var im = Newtonsoft.Json.JsonConvert.DeserializeObject<InvokeRequestCallback>(json);
- #endif
- Task.Factory.StartNew(() =>
- {
- var ret = InvokeInterface(json);
- var code = $"{im.CallBack}({ret})";
- CallJs(code);
- });
- }
- [Export]
- #if !Android22
- [JavascriptInterface]
- #endif
- // ReSharper disable once UnusedMember.Global
- // ReSharper disable once MemberCanBePrivate.Global
- public string InvokeInterface(string json)
- {
- object r;
- try
- {
- #if Android22
- var im = fastJSON.JSON.ToObject<InvokeRequestModel>(json);
- #else
- var im = Newtonsoft.Json.JsonConvert.DeserializeObject<InvokeRequestModel>(json);
- #endif
- var method = _jsApiMethodDictionary[im.MethodName];
- for (var i = 0; i < im.Arguments.Length; i++)
- {
- if (im.Arguments[i] == null)
- {
- im.Arguments[i] = Type.Missing;
- }
- else if (method.ParameterInfos[i].IsCallback)
- {
- var jsFunction = (string)im.Arguments[i];
- im.Arguments[i] = new Action<object>(
- o => CallJs($"{jsFunction}({Newtonsoft.Json.JsonConvert.SerializeObject(o)})"));
- }
- }
- r = new
- {
- Success = true,
- Result = method.MethodInfo.Invoke(this, im.Arguments)
- };
- }
- catch (Exception ex)
- {
- r = new
- {
- Success = false,
- Exception = ex
- };
- }
- #if Android22
- return fastJSON.JSON.ToJSON(r);
- #else
- return Newtonsoft.Json.JsonConvert.SerializeObject(r);
- #endif
- }
- protected abstract void CallJs(string code);
- }
- }
- #if Android22
- namespace System.Net
- {
- internal class WebUtility
- {
- public static readonly Func<string, string> HtmlEncode = Web.HttpUtility.HtmlEncode;
- }
- }
- #endif
|