diff --git a/src/Common.ClientLib.Droid/Extensions/UriExtensions.cs b/src/Common.ClientLib.Droid/Extensions/UriExtensions.cs new file mode 100644 index 000000000..2522c03d0 --- /dev/null +++ b/src/Common.ClientLib.Droid/Extensions/UriExtensions.cs @@ -0,0 +1,333 @@ +using Android; +using Android.OS; +using Android.Provider; +using Android.Webkit; +using System.IO; +using Xamarin.Essentials; +using AndroidUri = Android.Net.Uri; +using XEPlatform = Xamarin.Essentials.Platform; + +// ReSharper disable once CheckNamespace +namespace System +{ + public static class UriExtensions + { + // https://github.com/xamarin/Essentials/blob/main/Xamarin.Essentials/FileSystem/FileSystem.android.cs + + const string storageTypePrimary = "primary"; + const string storageTypeRaw = "raw"; + const string storageTypeImage = "image"; + const string storageTypeVideo = "video"; + const string storageTypeAudio = "audio"; + static readonly string[] contentUriPrefixes = + { + "content://downloads/public_downloads", + "content://downloads/my_downloads", + "content://downloads/all_downloads", + }; + + internal const string UriSchemeFile = "file"; + internal const string UriSchemeContent = "content"; + + internal const string UriAuthorityExternalStorage = "com.android.externalstorage.documents"; + internal const string UriAuthorityDownloads = "com.android.providers.downloads.documents"; + internal const string UriAuthorityMedia = "com.android.providers.media.documents"; + + public static string? EnsurePhysicalPath(this AndroidUri? uri, bool requireExtendedAccess = true) + { + if (uri != null) + { + // if this is a file, use that + if (uri.Scheme!.Equals(UriSchemeFile, StringComparison.OrdinalIgnoreCase)) + return uri.Path; + + // try resolve using the content provider + var absolute = ResolvePhysicalPath(uri, requireExtendedAccess); + if (!string.IsNullOrWhiteSpace(absolute) && Path.IsPathRooted(absolute)) + return absolute; + + // fall back to just copying it + var cached = CacheContentFile(uri); + if (!string.IsNullOrWhiteSpace(cached) && Path.IsPathRooted(cached)) + return cached; + } + + return null; + } + + static string? ResolvePhysicalPath(AndroidUri uri, bool requireExtendedAccess = true) + { + if (uri.Scheme!.Equals(UriSchemeFile, StringComparison.OrdinalIgnoreCase)) + { + // if it is a file, then return directly + + var resolved = uri.Path; + if (File.Exists(resolved)) + return resolved; + } + else if (!requireExtendedAccess || !((int)Build.VERSION.SdkInt >= (int)BuildVersionCodes.Q)) + { + // if this is on an older OS version, or we just need it now + + if (DocumentsContract.IsDocumentUri(XEPlatform.AppContext, uri)) + { + var resolved = ResolveDocumentPath(uri); + if (File.Exists(resolved)) + return resolved; + } + else if (uri.Scheme!.Equals(UriSchemeContent, StringComparison.OrdinalIgnoreCase)) + { + var resolved = ResolveContentPath(uri); + if (File.Exists(resolved)) + return resolved; + } + } + + return null; + } + + static string? ResolveDocumentPath(AndroidUri uri) + { + var docId = DocumentsContract.GetDocumentId(uri); + + var docIdParts = docId?.Split(':'); + if (docIdParts == null || docIdParts.Length == 0) + return null; + + if (uri.Authority!.Equals(UriAuthorityExternalStorage, StringComparison.OrdinalIgnoreCase)) + { + if (docIdParts.Length == 2) + { + var storageType = docIdParts[0]; + var uriPath = docIdParts[1]; + + // This is the internal "external" memory, NOT the SD Card + if (storageType.Equals(storageTypePrimary, StringComparison.OrdinalIgnoreCase)) + { +#pragma warning disable CS0618 // Type or member is obsolete + var root = Android.OS.Environment.ExternalStorageDirectory!.Path; +#pragma warning restore CS0618 // Type or member is obsolete + + return Path.Combine(root, uriPath); + } + + // TODO: support other types, such as actual SD Cards + } + } + else if (uri.Authority.Equals(UriAuthorityDownloads, StringComparison.OrdinalIgnoreCase)) + { + // NOTE: This only really applies to older Android vesions since the privacy changes + + if (docIdParts.Length == 2) + { + var storageType = docIdParts[0]; + var uriPath = docIdParts[1]; + + if (storageType.Equals(storageTypeRaw, StringComparison.OrdinalIgnoreCase)) + return uriPath; + } + + // ID could be "###" or "msf:###" + var fileId = docIdParts.Length == 2 + ? docIdParts[1] + : docIdParts[0]; + + foreach (var prefix in contentUriPrefixes) + { + var uriString = prefix + "/" + fileId; + var contentUri = AndroidUri.Parse(uriString); + + if (GetDataFilePath(contentUri!) is string filePath) + return filePath; + } + } + else if (uri.Authority.Equals(UriAuthorityMedia, StringComparison.OrdinalIgnoreCase)) + { + if (docIdParts.Length == 2) + { + var storageType = docIdParts[0]; + var uriPath = docIdParts[1]; + + AndroidUri? contentUri = null; + if (storageType.Equals(storageTypeImage, StringComparison.OrdinalIgnoreCase)) + contentUri = MediaStore.Images.Media.ExternalContentUri!; + else if (storageType.Equals(storageTypeVideo, StringComparison.OrdinalIgnoreCase)) + contentUri = MediaStore.Video.Media.ExternalContentUri!; + else if (storageType.Equals(storageTypeAudio, StringComparison.OrdinalIgnoreCase)) + contentUri = MediaStore.Audio.Media.ExternalContentUri!; + +#pragma warning disable CS0618 // 类型或成员已过时 + if (contentUri != null && GetDataFilePath(contentUri, $"{MediaStore.MediaColumns.Id!}=?", new[] { uriPath }) is string filePath) +#pragma warning restore CS0618 // 类型或成员已过时 + return filePath; + } + } + + return null; + } + + static string? ResolveContentPath(AndroidUri uri) + { + if (GetDataFilePath(uri) is string filePath) + return filePath; + + // TODO: support some additional things, like Google Photos if that is possible + + return null; + } + + static string? GetDataFilePath(AndroidUri contentUri, string? selection = null, string[]? selectionArgs = null) + { +#pragma warning disable CS0618 // Type or member is obsolete + const string column = MediaStore.Files.FileColumns.Data; +#pragma warning restore CS0618 // Type or member is obsolete + + // ask the content provider for the data column, which may contain the actual file path + var path = GetColumnValue(contentUri, column, selection, selectionArgs); + if (!string.IsNullOrEmpty(path) && Path.IsPathRooted(path)) + return path; + + return null; + } + + static string? GetColumnValue(AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null) + { + try + { + var value = QueryContentResolverColumn(contentUri, column, selection, selectionArgs); + if (!string.IsNullOrEmpty(value)) + return value; + } + catch + { + // Ignore all exceptions and use null for the error indicator + } + + return null; + } + + static string? QueryContentResolverColumn(AndroidUri contentUri, string columnName, string? selection = null, string[]? selectionArgs = null) + { + string? text = null; + + var projection = new[] { columnName }; + using var cursor = XEPlatform.AppContext.ContentResolver!.Query(contentUri, projection, selection, selectionArgs, null); + if (cursor?.MoveToFirst() == true) + { + var columnIndex = cursor.GetColumnIndex(columnName); + if (columnIndex != -1) + text = cursor.GetString(columnIndex); + } + + return text; + } + + static string? CacheContentFile(AndroidUri uri) + { + if (!uri.Scheme!.Equals(UriSchemeContent, StringComparison.OrdinalIgnoreCase)) + return null; + + // open the source stream + using var srcStream = OpenContentStream(uri, out var extension); + if (srcStream == null) + return null; + + // resolve or generate a valid destination path +#pragma warning disable CS0618 // 类型或成员已过时 + var filename = GetColumnValue(uri, MediaStore.Files.FileColumns.DisplayName) ?? Guid.NewGuid().ToString("N"); +#pragma warning restore CS0618 // 类型或成员已过时 + if (!Path.HasExtension(filename) && !string.IsNullOrEmpty(extension)) + filename = Path.ChangeExtension(filename, extension); + + // create a temporary file + var hasPermission = Permissions.IsDeclaredInManifest(Manifest.Permission.WriteExternalStorage); + var root = hasPermission + ? XEPlatform.AppContext.ExternalCacheDir + : XEPlatform.AppContext.CacheDir; + var tmpFile = GetEssentialsTemporaryFile(root, filename); + + // copy to the destination + using var dstStream = File.Create(tmpFile.CanonicalPath); + srcStream.CopyTo(dstStream); + + return tmpFile.CanonicalPath; + } + + static Stream? OpenContentStream(AndroidUri uri, out string? extension) + { + var isVirtual = IsVirtualFile(uri); + if (isVirtual) + { + return GetVirtualFileStream(uri, out extension); + } + + extension = GetFileExtension(uri); + return XEPlatform.AppContext.ContentResolver!.OpenInputStream(uri); + } + + static string? GetFileExtension(AndroidUri uri) + { + var mimeType = XEPlatform.AppContext.ContentResolver!.GetType(uri); + + return mimeType != null + ? MimeTypeMap.Singleton!.GetExtensionFromMimeType(mimeType) + : null; + } + + static bool IsVirtualFile(AndroidUri uri) + { + if (!DocumentsContract.IsDocumentUri(XEPlatform.AppContext, uri)) + return false; + + var value = GetColumnValue(uri, DocumentsContract.Document.ColumnFlags); + if (!string.IsNullOrEmpty(value) && int.TryParse(value, out var flagsInt)) + { + var flags = (DocumentContractFlags)flagsInt; + return flags.HasFlag(DocumentContractFlags.VirtualDocument); + } + + return false; + } + + static Stream? GetVirtualFileStream(AndroidUri uri, out string? extension) + { + var mimeTypes = XEPlatform.AppContext.ContentResolver!.GetStreamTypes(uri, MediaTypeNames.All); + if (mimeTypes?.Length >= 1) + { + var mimeType = mimeTypes[0]; + + var stream = XEPlatform.AppContext.ContentResolver! + .OpenTypedAssetFileDescriptor(uri, mimeType, null)! + .CreateInputStream(); + + extension = MimeTypeMap.Singleton!.GetExtensionFromMimeType(mimeType); + + return stream; + } + + extension = null; + return null; + } + + internal static Java.IO.File? GetEssentialsTemporaryFile(Java.IO.File root, string fileName) + { + // create the directory for all Essentials files + var rootDir = new Java.IO.File(root, EssentialsFolderHash); + rootDir.Mkdirs(); + rootDir.DeleteOnExit(); + + // create a unique directory just in case there are multiple file with the same name + var tmpDir = new Java.IO.File(rootDir, Guid.NewGuid().ToString("N")); + tmpDir.Mkdirs(); + tmpDir.DeleteOnExit(); + + // create the new temporary file + var tmpFile = new Java.IO.File(tmpDir, fileName); + tmpFile.DeleteOnExit(); + + return tmpFile; + } + + internal const string EssentialsFolderHash = "2203693cc04e0be7f4f024d5f9499e13"; + } +} \ No newline at end of file diff --git a/src/Common.ClientLib.Droid/Logging/PlatformLoggerProvider.cs b/src/Common.ClientLib.Droid/Logging/PlatformLoggerProvider.cs index f4119371e..665324099 100644 --- a/src/Common.ClientLib.Droid/Logging/PlatformLoggerProvider.cs +++ b/src/Common.ClientLib.Droid/Logging/PlatformLoggerProvider.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; // ReSharper disable once CheckNamespace namespace System.Logging diff --git a/src/Common.ClientLib.Droid/ServiceCollectionExtensions.cs b/src/Common.ClientLib.Droid/ServiceCollectionExtensions.cs index 6a5db31ce..ea0dab9e7 100644 --- a/src/Common.ClientLib.Droid/ServiceCollectionExtensions.cs +++ b/src/Common.ClientLib.Droid/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using System.Application.Services; using System.Application.Services.Implementation; @@ -19,28 +19,6 @@ namespace Microsoft.Extensions.DependencyInjection public static IServiceCollection TryAddToast(this IServiceCollection services) => PlatformToastImpl.TryAddToast(services); - /// - /// 添加客户端日志提供程序 - /// - /// - /// - public static ILoggingBuilder AddClientProvider(this ILoggingBuilder builder) - { - builder.AddProvider(PlatformLoggerProvider.Instance); - return builder; - } - - /// - /// 添加客户端日志 - /// - /// - /// - public static IServiceCollection AddClientLogging(this IServiceCollection services) - { - services.AddLogging(l => l.AddClientProvider()); - return services; - } - /// /// 添加调试模式显示 /// diff --git a/src/Common.CoreLib/IOPath.cs b/src/Common.CoreLib/IOPath.cs index 91bb145ba..6a721baa6 100644 --- a/src/Common.CoreLib/IOPath.cs +++ b/src/Common.CoreLib/IOPath.cs @@ -104,6 +104,9 @@ namespace System #region FileSystem + public const string DirName_AppData = "AppData"; + public const string DirName_Cache = "Cache"; + static Func? getAppDataDirectory; static Func? getCacheDirectory; @@ -125,7 +128,7 @@ namespace System { if (DI.DeviceIdiom == DeviceIdiom.Desktop) { - var r = Path.Combine(BaseDirectory, "AppData"); + var r = Path.Combine(BaseDirectory, DirName_AppData); DirCreateByNotExists(r); return r; } @@ -153,7 +156,7 @@ namespace System { if (DI.DeviceIdiom == DeviceIdiom.Desktop) { - var r = Path.Combine(BaseDirectory, "Cache"); + var r = Path.Combine(BaseDirectory, DirName_Cache); DirCreateByNotExists(r); return r; } @@ -237,14 +240,14 @@ namespace System /// /// 判断路径是否为[文件夹(Dir)] /// - /// + /// /// - public static bool IsDirectory(string ioPath) + public static bool IsDirectory(string IOPath) { #if NET35 throw new PlatformNotSupportedException(); #else - var attr = File.GetAttributes(ioPath); + var attr = File.GetAttributes(IOPath); return attr.HasFlag(FileAttributes.Directory); #endif } @@ -254,21 +257,21 @@ namespace System /// 如果传入的参数为[文件(File)]路径,则返回当前所在[文件夹(Dir)]路径 /// 如果传入的参数为[文件夹(Dir)]路径,则直接返回参数 /// - /// + /// /// - public static string GetDirectoryPath(string ioPath) + public static string GetDirectoryPath(string IOPath) { try { - if (!IsDirectory(ioPath)) + if (!IsDirectory(IOPath)) { - return Path.GetDirectoryName(ioPath) ?? string.Empty; + return Path.GetDirectoryName(IOPath) ?? string.Empty; } } catch { } - return ioPath; + return IOPath; } static FileStream OpenReadCore(string filePath) => new(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); diff --git a/src/Common.CoreLib/MediaTypeNames.cs b/src/Common.CoreLib/MediaTypeNames.cs index a04b44e76..b0fa71942 100644 --- a/src/Common.CoreLib/MediaTypeNames.cs +++ b/src/Common.CoreLib/MediaTypeNames.cs @@ -76,5 +76,7 @@ namespace System public const string VideoAll = "video/*"; public const string PDF = "application/pdf"; + + public const string All = "*/*"; } } \ No newline at end of file diff --git a/src/ST.Client.Desktop.Avalonia.App/Program.InitLogDir.cs b/src/ST.Client.Desktop.Avalonia.App/Program.InitLogDir.cs deleted file mode 100644 index 2f777d50d..000000000 --- a/src/ST.Client.Desktop.Avalonia.App/Program.InitLogDir.cs +++ /dev/null @@ -1,104 +0,0 @@ -using ArchiSteamFarm; -using NLog; -using NLog.Common; -using NLog.Config; -using NLog.Filters; -using NLog.Targets; -using System.Application.Services; -using System.Application.UI; -using System.IO; -using NLogLevel = NLog.LogLevel; - -#if CONSOLEAPP -namespace System.Application -#else -namespace System.Application.UI -#endif -{ - static partial class Program - { - public static Logger? logger; - - public static string InitLogDir(string? alias = null) - { - var logDirPath = Path.Combine( -#if WINDOWS_DESKTOP_BRIDGE || MAC - IOPath.CacheDirectory -#else - IOPath.BaseDirectory -#endif - , "Logs"); - IOPath.DirCreateByNotExists(logDirPath); -#if StartupTrace - StartupTrace.Restart("InitLogDir.IO"); -#endif - var logDirPath_ = logDirPath + Path.DirectorySeparatorChar; - - InternalLogger.LogFile = logDirPath_ + "internal-nlog" + alias + ".txt"; - InternalLogger.LogLevel = NLogLevel.Error; - var objConfig = new LoggingConfiguration(); - var defMinLevel = AppHelper.DefaultNLoggerMinLevel; - var logfile = new FileTarget("logfile") - { - FileName = logDirPath_ + "nlog-all-${shortdate}" + alias + ".log", - Layout = "${longdate}|${level}|${logger}|${message} |${all-event-properties} ${exception:format=tostring}", - ArchiveAboveSize = 10485760, - MaxArchiveFiles = 14, - MaxArchiveDays = 7, - }; - var asfLogDirPath = ASFPathHelper.GetLogDirectory(logDirPath_); - FileTarget logfile_asf = new("File") - { - ArchiveFileName = ASFPathHelper.GetNLogArchiveFileName(asfLogDirPath), - ArchiveNumbering = ArchiveNumberingMode.Rolling, - ArchiveOldFileOnStartup = true, - CleanupFileName = false, - ConcurrentWrites = false, - DeleteOldFileOnStartup = true, - FileName = ASFPathHelper.GetNLogFileName(asfLogDirPath), - Layout = ASFPathHelper.NLogGeneralLayout, - ArchiveAboveSize = 10485760, - MaxArchiveFiles = 10, - MaxArchiveDays = 7, - }; - objConfig.AddTarget(logfile); - objConfig.AddTarget(logfile_asf); - - objConfig.AddRule(NLogLevel.Error, NLogLevel.Fatal, logfile, "Microsoft.*"); - objConfig.AddRule(NLogLevel.Error, NLogLevel.Fatal, logfile, "System.Net.Http.*"); - objConfig.LoggingRules.Add(new LoggingRule("ArchiSteamFarm*", NLogLevel.Off, logfile) { Final = true, }); - objConfig.AddRule(defMinLevel, NLogLevel.Fatal, logfile, "*"); - objConfig.LoggingRules.Add(new LoggingRule("ArchiSteamFarm*", defMinLevel, logfile_asf)); -#if StartupTrace - StartupTrace.Restart("InitLogDir.CreateLoggingConfiguration"); -#endif - NLog.LogManager.Configuration = objConfig; - - IArchiSteamFarmService.InitCoreLoggers = () => - { - if (ArchiSteamFarm.LogManager.Configuration != null) return; - LoggingConfiguration config = new(); - var logDirPath_ASF = ASFPathHelper.GetLogDirectory(logDirPath_); - FileTarget fileTarget = new("File") - { - ArchiveFileName = ASFPathHelper.GetNLogArchiveFileName(logDirPath_ASF), - ArchiveNumbering = ArchiveNumberingMode.Rolling, - ArchiveOldFileOnStartup = true, - CleanupFileName = false, - ConcurrentWrites = false, - DeleteOldFileOnStartup = true, - FileName = ASFPathHelper.GetNLogFileName(logDirPath_ASF), - Layout = ASFPathHelper.NLogGeneralLayout, - ArchiveAboveSize = 10485760, - MaxArchiveFiles = 10, - MaxArchiveDays = 7, - }; - config.AddTarget(fileTarget); - config.LoggingRules.Add(new LoggingRule("*", defMinLevel, fileTarget)); - ArchiSteamFarm.LogManager.Configuration = config; - }; - - return logDirPath; - } - } -} \ No newline at end of file diff --git a/src/ST.Client.Desktop.Avalonia.App/Program.cs b/src/ST.Client.Desktop.Avalonia.App/Program.cs index 08b58a8eb..715621ea3 100644 --- a/src/ST.Client.Desktop.Avalonia.App/Program.cs +++ b/src/ST.Client.Desktop.Avalonia.App/Program.cs @@ -11,6 +11,7 @@ namespace System.Application.UI { static partial class Program { + static Logger? logger; static readonly HashSet exceptions = new(); static readonly object lock_global_ex_log = new(); @@ -39,12 +40,12 @@ namespace System.Application.UI IsMainProcess = args.Length == 0; IsCLTProcess = !IsMainProcess && args.FirstOrDefault() == "-clt"; - var logDirPath = InitLogDir(); + AppHelper.InitLogDir(); #if StartupTrace StartupTrace.Restart("InitLogDir"); #endif - void InitCefNetApp() => CefNetApp.Init(logDirPath, args); + void InitCefNetApp() => CefNetApp.Init(AppHelper.LogDirPath, args); void InitAvaloniaApp() => BuildAvaloniaAppAndStartWithClassicDesktopLifetime(args); void InitStartup(DILevel level) => Startup.Init(level); logger = LogManager.GetCurrentClassLogger(); diff --git a/src/ST.Client.Desktop.Avalonia.App/WebViewImplType.cs b/src/ST.Client.Desktop.Avalonia.App/WebViewImplType.cs deleted file mode 100644 index f02b6ee71..000000000 --- a/src/ST.Client.Desktop.Avalonia.App/WebViewImplType.cs +++ /dev/null @@ -1,12 +0,0 @@ -//namespace System.Application.UI -//{ -// public enum WebViewImplType : byte -// { -// ChromiumEmbeddedFramework, - -// /// -// /// https://docs.microsoft.com/zh-cn/microsoft-edge/webview2 -// /// -// MicrosoftEdgeWebView2InWPF, -// } -//} \ No newline at end of file diff --git a/src/ST.Client.Desktop/FileSystemDesktop.cs b/src/ST.Client.Desktop/FileSystemDesktop.cs index 557c907ef..9c04ef4af 100644 --- a/src/ST.Client.Desktop/FileSystemDesktop.cs +++ b/src/ST.Client.Desktop/FileSystemDesktop.cs @@ -11,9 +11,6 @@ namespace System.Application /// public static class FileSystemDesktop { - public const string AppDataDirName = "AppData"; - public const string CacheDirName = "Cache"; - /// /// 初始化文件系统 /// @@ -35,8 +32,8 @@ namespace System.Application IOPath.BaseDirectory; #endif - var appDataPath = Path.Combine(appDataRootPath, AppDataDirName); - var cachePath = Path.Combine(appDataRootPath, CacheDirName); + var appDataPath = Path.Combine(appDataRootPath, IOPath.DirName_AppData); + var cachePath = Path.Combine(appDataRootPath, IOPath.DirName_Cache); IOPath.DirCreateByNotExists(appDataPath); IOPath.DirCreateByNotExists(cachePath); IOPath.InitFileSystem(GetAppDataDirectory, GetCacheDirectory); diff --git a/src/ST.Client.Desktop/ServiceCollectionExtensions.AddGeneralLogging.cs b/src/ST.Client.Desktop/ServiceCollectionExtensions.AddGeneralLogging.cs new file mode 100644 index 000000000..813e9f686 --- /dev/null +++ b/src/ST.Client.Desktop/ServiceCollectionExtensions.AddGeneralLogging.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.Logging; +using System.Application.UI; +using System.Logging; +using System.Properties; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class ServiceCollectionExtensions + { + /// + /// 添加通用日志实现 + /// + /// + /// + public static IServiceCollection AddGeneralLogging(this IServiceCollection services) + { + var (minLevel, action) = AppHelper.ConfigureLogging(); + services.AddLogging(b => + { + action(b); +#if __ANDROID__ + if (ThisAssembly.Debuggable) + { + // Android Logcat Provider Impl + b.AddProvider(PlatformLoggerProvider.Instance); + } +#endif + }); + services.Configure(o => + { + o.MinLevel = minLevel; + }); + return services; + } + } +} \ No newline at end of file diff --git a/src/ST.Client.Desktop/ServiceCollectionExtensions.cs b/src/ST.Client.Desktop/ServiceCollectionExtensions.cs index dd34cd016..37883d8e7 100644 --- a/src/ST.Client.Desktop/ServiceCollectionExtensions.cs +++ b/src/ST.Client.Desktop/ServiceCollectionExtensions.cs @@ -18,22 +18,6 @@ namespace Microsoft.Extensions.DependencyInjection public static IServiceCollection TryAddToast(this IServiceCollection services) => PlatformToastImpl.TryAddToast(services); - /// - /// 添加桌面端日志实现 - /// - /// - /// - public static IServiceCollection AddDesktopLogging(this IServiceCollection services) - { - var (minLevel, cfg) = AppHelper.Configure(); - services.AddLogging(cfg); - services.Configure(o => - { - o.MinLevel = minLevel; - }); - return services; - } - /// /// 添加 hosts 文件助手服务 /// diff --git a/src/ST.Client.Desktop/Services/IDesktopPlatformService.cs b/src/ST.Client.Desktop/Services/IDesktopPlatformService.cs index 6843ff8af..1a4cd7fa2 100644 --- a/src/ST.Client.Desktop/Services/IDesktopPlatformService.cs +++ b/src/ST.Client.Desktop/Services/IDesktopPlatformService.cs @@ -46,11 +46,8 @@ namespace System.Application.Services void OpenProcess(string name, string arguments) => Process.Start(name, arguments); void OpenProcess(string name) => Process.Start(name); - /// - /// 使用文本阅读器打开文件 - /// - /// - void OpenFileByTextReader(string filePath) + + void IPlatformService.OpenFileByTextReader(string filePath) { TextReaderProvider? userProvider = null; var p = GeneralSettings.TextReaderProvider.Value; diff --git a/src/ST.Client.Desktop/UI/AppHelper.Logger.cs b/src/ST.Client.Desktop/UI/AppHelper.Logger.cs new file mode 100644 index 000000000..89a53c869 --- /dev/null +++ b/src/ST.Client.Desktop/UI/AppHelper.Logger.cs @@ -0,0 +1,247 @@ +using ArchiSteamFarm; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NLog.Common; +using NLog.Config; +using NLog.Extensions.Logging; +using NLog.Targets; +using System.Application.Services; +using System.IO; +using System.Properties; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; +using NInternalLogger = NLog.Common.InternalLogger; +using NLogLevel = NLog.LogLevel; +using NLogManager = NLog.LogManager; + +namespace System.Application.UI +{ + partial class AppHelper + { + #region Logger + + const LogLevel DefaultLoggerMinLevel = ThisAssembly.Debuggable ? LogLevel.Debug : LogLevel.Error; + + public static readonly NLogLevel DefaultNLoggerMinLevel = ConvertLogLevel(DefaultLoggerMinLevel); + + /// + /// Convert log level to NLog variant. + /// + /// level to be converted. + /// + static NLogLevel ConvertLogLevel(LogLevel logLevel) => logLevel switch + { + // https://github.com/NLog/NLog.Extensions.Logging/blob/v1.7.0/src/NLog.Extensions.Logging/Logging/NLogLogger.cs#L416 + LogLevel.Trace => NLogLevel.Trace, + LogLevel.Debug => NLogLevel.Debug, + LogLevel.Information => NLogLevel.Info, + LogLevel.Warning => NLogLevel.Warn, + LogLevel.Error => NLogLevel.Error, + LogLevel.Critical => NLogLevel.Fatal, + LogLevel.None => NLogLevel.Off, + _ => NLogLevel.Debug, + }; + + static void SetNLoggerMinLevel(LogLevel logLevel) => SetNLoggerMinLevel(ConvertLogLevel(logLevel)); + + static void SetNLoggerMinLevel(NLogLevel logLevel) + { + NLogManager.GlobalThreshold = logLevel; + NInternalLogger.LogLevel = logLevel; + } + + public static void TrySetLoggerMinLevel(LogLevel logLevel) + { + try + { + var o = LoggerFilterOptions; + if (o != null) o.MinLevel = logLevel; + } + catch + { + } + SetNLoggerMinLevel(ConvertLogLevel(logLevel)); + } + + static LoggerFilterOptions? _LoggerFilterOptions; + /// + /// 日志过滤选项 + /// + static LoggerFilterOptions? LoggerFilterOptions + { + get + { + if (_LoggerFilterOptions != null) return _LoggerFilterOptions; + return DI.Get_Nullable>()?.Value; + } + set + { + _LoggerFilterOptions = value; + } + } + + /// + /// 配置 NLog 提供程序,仅初始化时调用 + /// + /// + public static (LogLevel minLevel, Action cfg) ConfigureLogging() + { + var minLevel = DefaultLoggerMinLevel; + + void _(ILoggingBuilder builder) + { + builder.ClearProviders(); + + builder.SetMinimumLevel(minLevel); + SetNLoggerMinLevel(minLevel); + + // 可以多个日志提供同时用,比如还可以在 Win 平台再添加一个 Windows 事件日志 + + builder.AddNLog(NLogManager.Configuration); // 添加 NLog 日志 + } + + return (minLevel, (Action)_); + } + + /// + /// 日志最低等级 + /// + public static LogLevel LoggerMinLevel + { + get + { + var o = LoggerFilterOptions; + if (o == null) + { + o = new(); + LoggerFilterOptions = o; + } + return o.MinLevel; + } + set + { + var o = LoggerFilterOptions; + if (o != null) + { + o.MinLevel = value; + } + SetNLoggerMinLevel(value); + } + } + + /// + /// 启用日志 + /// + public static bool EnableLogger + { + get => LoggerMinLevel > LogLevel.None; + set + { + LoggerMinLevel = value ? DefaultLoggerMinLevel : LogLevel.None; + } + } + + #endregion + + /// + /// 日志存放文件夹路径 + /// + public static string LogDirPath { get; private set; } = string.Empty; + + /// + /// 日志文件夹是否存放在缓存文件夹中,通常日志文件夹将存放在基目录上,因某些平台基目录为只读,则只能放在缓存文件夹中 + /// + public static bool LogUnderCache { get; private set; } + + public const string LogDirName = "Logs"; + + public static void InitLogDir(string? alias = null) + { + if (!string.IsNullOrEmpty(LogDirPath)) return; + + LogUnderCache = + +#if WINDOWS_DESKTOP_BRIDGE || MAC || __MOBILE__ + true +#else + false +#endif + ; + + var logDirPath = Path.Combine(LogUnderCache ? + IOPath.CacheDirectory : + IOPath.BaseDirectory, + LogDirName); + IOPath.DirCreateByNotExists(logDirPath); +#if StartupTrace + StartupTrace.Restart("InitLogDir.IO"); +#endif + var logDirPath_ = logDirPath + Path.DirectorySeparatorChar; + + InternalLogger.LogFile = logDirPath_ + "internal-nlog" + alias + ".txt"; + InternalLogger.LogLevel = NLogLevel.Error; + var objConfig = new LoggingConfiguration(); + var defMinLevel = AppHelper.DefaultNLoggerMinLevel; + var logfile = new FileTarget("logfile") + { + FileName = logDirPath_ + "nlog-all-${shortdate}" + alias + ".log", + Layout = "${longdate}|${level}|${logger}|${message} |${all-event-properties} ${exception:format=tostring}", + ArchiveAboveSize = 10485760, + MaxArchiveFiles = 14, + MaxArchiveDays = 7, + }; + var asfLogDirPath = ASFPathHelper.GetLogDirectory(logDirPath_); + FileTarget logfile_asf = new("File") + { + ArchiveFileName = ASFPathHelper.GetNLogArchiveFileName(asfLogDirPath), + ArchiveNumbering = ArchiveNumberingMode.Rolling, + ArchiveOldFileOnStartup = true, + CleanupFileName = false, + ConcurrentWrites = false, + DeleteOldFileOnStartup = true, + FileName = ASFPathHelper.GetNLogFileName(asfLogDirPath), + Layout = ASFPathHelper.NLogGeneralLayout, + ArchiveAboveSize = 10485760, + MaxArchiveFiles = 10, + MaxArchiveDays = 7, + }; + objConfig.AddTarget(logfile); + objConfig.AddTarget(logfile_asf); + + objConfig.AddRule(NLogLevel.Error, NLogLevel.Fatal, logfile, "Microsoft.*"); + objConfig.AddRule(NLogLevel.Error, NLogLevel.Fatal, logfile, "System.Net.Http.*"); + objConfig.LoggingRules.Add(new LoggingRule("ArchiSteamFarm*", NLogLevel.Off, logfile) { Final = true, }); + objConfig.AddRule(defMinLevel, NLogLevel.Fatal, logfile, "*"); + objConfig.LoggingRules.Add(new LoggingRule("ArchiSteamFarm*", defMinLevel, logfile_asf)); +#if StartupTrace + StartupTrace.Restart("InitLogDir.CreateLoggingConfiguration"); +#endif + NLog.LogManager.Configuration = objConfig; + + IArchiSteamFarmService.InitCoreLoggers = () => + { + if (ArchiSteamFarm.LogManager.Configuration != null) return; + LoggingConfiguration config = new(); + var logDirPath_ASF = ASFPathHelper.GetLogDirectory(logDirPath_); + FileTarget fileTarget = new("File") + { + ArchiveFileName = ASFPathHelper.GetNLogArchiveFileName(logDirPath_ASF), + ArchiveNumbering = ArchiveNumberingMode.Rolling, + ArchiveOldFileOnStartup = true, + CleanupFileName = false, + ConcurrentWrites = false, + DeleteOldFileOnStartup = true, + FileName = ASFPathHelper.GetNLogFileName(logDirPath_ASF), + Layout = ASFPathHelper.NLogGeneralLayout, + ArchiveAboveSize = 10485760, + MaxArchiveFiles = 10, + MaxArchiveDays = 7, + }; + config.AddTarget(fileTarget); + config.LoggingRules.Add(new LoggingRule("*", defMinLevel, fileTarget)); + ArchiSteamFarm.LogManager.Configuration = config; + }; + + LogDirPath = logDirPath; + } + } +} \ No newline at end of file diff --git a/src/ST.Client.Desktop/UI/AppHelper.cs b/src/ST.Client.Desktop/UI/AppHelper.cs index adaa3a25e..6f02ed996 100644 --- a/src/ST.Client.Desktop/UI/AppHelper.cs +++ b/src/ST.Client.Desktop/UI/AppHelper.cs @@ -1,18 +1,11 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using NLog.Extensions.Logging; using System.Application.Services; using System.Diagnostics; using System.IO; using System.Properties; -using LogLevel = Microsoft.Extensions.Logging.LogLevel; -using NInternalLogger = NLog.Common.InternalLogger; -using NLogLevel = NLog.LogLevel; -using NLogManager = NLog.LogManager; namespace System.Application.UI { - public static class AppHelper + public static partial class AppHelper { public static bool EnableDevtools { get; set; } = ThisAssembly.Debuggable; @@ -63,127 +56,6 @@ namespace System.Application.UI s.SetBootAutoStart(isAutoStart, Constants.HARDCODED_APP_NAME); } - #region Logger - - const LogLevel DefaultLoggerMinLevel = ThisAssembly.Debuggable ? LogLevel.Debug : LogLevel.Error; - - public static readonly NLogLevel DefaultNLoggerMinLevel = ConvertLogLevel(DefaultLoggerMinLevel); - - /// - /// Convert log level to NLog variant. - /// - /// level to be converted. - /// - static NLogLevel ConvertLogLevel(LogLevel logLevel) => logLevel switch - { - // https://github.com/NLog/NLog.Extensions.Logging/blob/v1.7.0/src/NLog.Extensions.Logging/Logging/NLogLogger.cs#L416 - LogLevel.Trace => NLogLevel.Trace, - LogLevel.Debug => NLogLevel.Debug, - LogLevel.Information => NLogLevel.Info, - LogLevel.Warning => NLogLevel.Warn, - LogLevel.Error => NLogLevel.Error, - LogLevel.Critical => NLogLevel.Fatal, - LogLevel.None => NLogLevel.Off, - _ => NLogLevel.Debug, - }; - - static void SetNLoggerMinLevel(LogLevel logLevel) => SetNLoggerMinLevel(ConvertLogLevel(logLevel)); - - static void SetNLoggerMinLevel(NLogLevel logLevel) - { - NLogManager.GlobalThreshold = logLevel; - NInternalLogger.LogLevel = logLevel; - } - - public static void TrySetLoggerMinLevel(LogLevel logLevel) - { - try - { - var o = LoggerFilterOptions; - if (o != null) o.MinLevel = logLevel; - } - catch - { - } - SetNLoggerMinLevel(ConvertLogLevel(logLevel)); - } - - static LoggerFilterOptions? _LoggerFilterOptions; - /// - /// 日志过滤选项 - /// - static LoggerFilterOptions? LoggerFilterOptions - { - get - { - if (_LoggerFilterOptions != null) return _LoggerFilterOptions; - return DI.Get_Nullable>()?.Value; - } - set - { - _LoggerFilterOptions = value; - } - } - - internal static (LogLevel minLevel, Action cfg) Configure() - { - var minLevel = DefaultLoggerMinLevel; - - void _(ILoggingBuilder builder) - { - builder.ClearProviders(); - - builder.SetMinimumLevel(minLevel); - SetNLoggerMinLevel(minLevel); - - // 可以多个日志提供同时用,比如还可以在 Win 平台再添加一个 Windows 事件日志 - - builder.AddNLog(NLogManager.Configuration); // 添加 NLog 日志 - } - - return (minLevel, (Action)_); - } - - /// - /// 日志最低等级 - /// - public static LogLevel LoggerMinLevel - { - get - { - var o = LoggerFilterOptions; - if (o == null) - { - o = new(); - LoggerFilterOptions = o; - } - return o.MinLevel; - } - set - { - var o = LoggerFilterOptions; - if (o != null) - { - o.MinLevel = value; - } - SetNLoggerMinLevel(value); - } - } - - /// - /// 启用日志 - /// - public static bool EnableLogger - { - get => LoggerMinLevel > LogLevel.None; - set - { - LoggerMinLevel = value ? DefaultLoggerMinLevel : LogLevel.None; - } - } - - #endregion - public static IDesktopAppService Current => DI.Get(); #if DEBUG diff --git a/src/ST.Client.Desktop.Avalonia.App/ApplicationInstance.cs b/src/ST.Client.Desktop/UI/ApplicationInstance.cs similarity index 100% rename from src/ST.Client.Desktop.Avalonia.App/ApplicationInstance.cs rename to src/ST.Client.Desktop/UI/ApplicationInstance.cs diff --git a/src/ST.Client.Desktop/UI/Resx/AppResources.Designer.cs b/src/ST.Client.Desktop/UI/Resx/AppResources.Designer.cs index cc09df07a..43bb095f3 100644 --- a/src/ST.Client.Desktop/UI/Resx/AppResources.Designer.cs +++ b/src/ST.Client.Desktop/UI/Resx/AppResources.Designer.cs @@ -458,6 +458,15 @@ namespace System.Application.UI.Resx { } } + /// + /// 查找类似 浏览基目录 的本地化字符串。 + /// + public static string BrowseBaseDirectory { + get { + return ResourceManager.GetString("BrowseBaseDirectory", resourceCulture); + } + } + /// /// 查找类似 完成 的本地化字符串。 /// diff --git a/src/ST.Client.Desktop/UI/Resx/AppResources.resx b/src/ST.Client.Desktop/UI/Resx/AppResources.resx index 77505a9d7..190075b2f 100644 --- a/src/ST.Client.Desktop/UI/Resx/AppResources.resx +++ b/src/ST.Client.Desktop/UI/Resx/AppResources.resx @@ -1499,4 +1499,7 @@ 应用通知(系统设置) - + + 浏览基目录 + + \ No newline at end of file diff --git a/src/ST.Client.Desktop/UI/ViewModels/Pages/ProxyScriptManagePageViewModel.cs b/src/ST.Client.Desktop/UI/ViewModels/Pages/ProxyScriptManagePageViewModel.cs index a21c91c0f..e4b51d13e 100644 --- a/src/ST.Client.Desktop/UI/ViewModels/Pages/ProxyScriptManagePageViewModel.cs +++ b/src/ST.Client.Desktop/UI/ViewModels/Pages/ProxyScriptManagePageViewModel.cs @@ -175,22 +175,21 @@ namespace System.Application.UI.ViewModels public void EditScriptItemButton(ScriptDTO script) { - var url = Path.Combine(IOPath.AppDataDirectory, script.FilePath); var fileInfo = new FileInfo(url); if (fileInfo.Exists) { - DI.Get().OpenFileByTextReader(url); + IPlatformService.Instance.OpenFileByTextReader(url); var result = MessageBoxCompat.ShowAsync(@AppResources.Script_EditTxt, ThisAssembly.AssemblyTrademark, MessageBoxButtonCompat.OKCancel).ContinueWith(async (s) => { if (s.Result == MessageBoxResultCompat.OK) { - var item = await DI.Get().AddScriptAsync(url, script, build: script.IsBuild, order: script.Order, ignoreCache: true); - if (item.state) + var (state, model, msg) = await DI.Get().AddScriptAsync(url, script, build: script.IsBuild, order: script.Order, ignoreCache: true); + if (state) { - if (ProxyService.Current.ProxyScripts.Items.Any() && item.model != null) + if (ProxyService.Current.ProxyScripts.Items.Any() && model != null) { - ProxyService.Current.ProxyScripts.Replace(script, item.model); + ProxyService.Current.ProxyScripts.Replace(script, model); Toast.Show(AppResources.Success_.Format(AppResources.Script_Edit)); } } @@ -241,7 +240,7 @@ namespace System.Application.UI.ViewModels var result = await MessageBoxCompat.ShowAsync(@AppResources.ScriptShop_NoLogin, ThisAssembly.AssemblyTrademark, MessageBoxButtonCompat.OKCancel); if (result == MessageBoxResultCompat.OK) { - await IShowWindowService.Instance.Show(CustomWindow.LoginOrRegister, new LoginOrRegisterWindowViewModel(), string.Empty, ResizeModeCompat.CanResize); + await IShowWindowService.Instance.Show(CustomWindow.LoginOrRegister, new LoginOrRegisterWindowViewModel(), string.Empty, ResizeModeCompat.CanResize); } } else diff --git a/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable-hdpi/baseline_folder_black_24.png b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable-hdpi/baseline_folder_black_24.png new file mode 100644 index 000000000..c86c7dbe2 Binary files /dev/null and b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable-hdpi/baseline_folder_black_24.png differ diff --git a/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable-mdpi/baseline_folder_black_24.png b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable-mdpi/baseline_folder_black_24.png new file mode 100644 index 000000000..067f0ea22 Binary files /dev/null and b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable-mdpi/baseline_folder_black_24.png differ diff --git a/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable-xhdpi/baseline_folder_black_24.png b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable-xhdpi/baseline_folder_black_24.png new file mode 100644 index 000000000..863560d3d Binary files /dev/null and b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable-xhdpi/baseline_folder_black_24.png differ diff --git a/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable-xxhdpi/baseline_folder_black_24.png b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable-xxhdpi/baseline_folder_black_24.png new file mode 100644 index 000000000..9c3ad1eb9 Binary files /dev/null and b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable-xxhdpi/baseline_folder_black_24.png differ diff --git a/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable-xxxhdpi/baseline_folder_black_24.png b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable-xxxhdpi/baseline_folder_black_24.png new file mode 100644 index 000000000..2a7c64f56 Binary files /dev/null and b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable-xxxhdpi/baseline_folder_black_24.png differ diff --git a/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable/baseline_folder_black_24.xml b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable/baseline_folder_black_24.xml new file mode 100644 index 000000000..dc6b08023 --- /dev/null +++ b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/drawable/baseline_folder_black_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/layout/activity_explorer.xml b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/layout/activity_explorer.xml new file mode 100644 index 000000000..c1f03c3a7 --- /dev/null +++ b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/layout/activity_explorer.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/layout/activity_steam_auth_trade.xml b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/layout/activity_steam_auth_trade.xml index 71d983b03..b78b95d71 100644 --- a/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/layout/activity_steam_auth_trade.xml +++ b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/layout/activity_steam_auth_trade.xml @@ -59,7 +59,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:overScrollMode="never" - android:paddingBottom="@dimen/fab_full_height" tools:itemCount="4" tools:listitem="@layout/layout_card_steam_auth_trade_confirmation" xamarin:managedType="AndroidX.RecyclerView.Widget.RecyclerView" /> diff --git a/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/layout/layout_file_or_dir.xml b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/layout/layout_file_or_dir.xml new file mode 100644 index 000000000..fe8f6d729 --- /dev/null +++ b/src/ST.Client.Mobile.Droid.Design/ui/src/main/res/layout/layout_file_or_dir.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + diff --git a/src/ST.Client.Mobile.Droid/GoToPlatformPages.cs b/src/ST.Client.Mobile.Droid/GoToPlatformPages.cs index 060bfa334..bff7aa483 100644 --- a/src/ST.Client.Mobile.Droid/GoToPlatformPages.cs +++ b/src/ST.Client.Mobile.Droid/GoToPlatformPages.cs @@ -2,8 +2,8 @@ using Android.App; using Android.Content; using Android.OS; using AndroidX.Core.Content; +using AndroidUri = Android.Net.Uri; using Fragment = AndroidX.Fragment.App.Fragment; -using AUri = Android.Net.Uri; using JFile = Java.IO.File; using Settings = Android.Provider.Settings; @@ -138,25 +138,27 @@ namespace System.Application StartActivity(activity, viewModel); } - public static void InstallApk(Context context, JFile apkFile) + public static void OpenFile(Context context, JFile apkFile, string mime) { var sdkInt = (int)Build.VERSION.SdkInt; var intent = new Intent(Intent.ActionView); - AUri apkUri; - if (sdkInt >= (int)BuildVersionCodes.N) // 7.0开始要用新的API! + AndroidUri apkUri; + if (sdkInt >= (int)BuildVersionCodes.N) // 7.0 FileProvider { apkUri = FileProvider.GetUriForFile(context, GetAuthority(context), apkFile); intent.AddFlags(ActivityFlags.GrantReadUriPermission); // FLAG_GRANT_READ_URI_PERMISSION 添加这一句表示对目标应用临时授权该Uri所代表的文件 } else { - apkUri = AUri.FromFile(apkFile)!; + apkUri = AndroidUri.FromFile(apkFile)!; } - intent.SetDataAndType(apkUri, MediaTypeNames.APK); + intent.SetDataAndType(apkUri, mime); intent.AddFlags(ActivityFlags.NewTask); context.StartActivity(intent); } + public static void InstallApk(Context context, JFile apkFile) => OpenFile(context, apkFile, MediaTypeNames.APK); + public static void InstallApk(Context context, string apkFilePath) { JFile apkFile = new(apkFilePath); @@ -194,7 +196,7 @@ namespace System.Application { intent.SetAction(Settings.ActionApplicationDetailsSettings); intent.AddCategory(Intent.CategoryDefault); - intent.SetData(AUri.FromParts("package", context.PackageName, null)); + intent.SetData(AndroidUri.FromParts("package", context.PackageName, null)); } else if (sdkInt <= 8) { diff --git a/src/ST.Client.Mobile.Droid/ST.Client.Mobile.Droid.csproj b/src/ST.Client.Mobile.Droid/ST.Client.Mobile.Droid.csproj index b75a561fb..5c538c3b9 100644 --- a/src/ST.Client.Mobile.Droid/ST.Client.Mobile.Droid.csproj +++ b/src/ST.Client.Mobile.Droid/ST.Client.Mobile.Droid.csproj @@ -54,6 +54,9 @@ ServiceCollectionExtensions.Link.cs + + Extensions + Extensions @@ -102,6 +105,7 @@ + @@ -238,6 +242,9 @@ Resources\drawable\baseline_female_black_24.xml + + Resources\drawable\baseline_folder_black_24.xml + Resources\drawable\baseline_info_black_24.xml @@ -373,6 +380,9 @@ Resources\drawable-hdpi\baseline_female_black_24.png + + Resources\drawable-hdpi\baseline_folder_black_24.png + Resources\drawable-hdpi\baseline_info_black_24.png @@ -478,6 +488,9 @@ Resources\drawable-mdpi\baseline_female_black_24.png + + Resources\drawable-mdpi\baseline_folder_black_24.png + Resources\drawable-mdpi\baseline_info_black_24.png @@ -583,6 +596,9 @@ Resources\drawable-xhdpi\baseline_female_black_24.png + + Resources\drawable-xhdpi\baseline_folder_black_24.png + Resources\drawable-xhdpi\baseline_info_black_24.png @@ -688,6 +704,9 @@ Resources\drawable-xxhdpi\baseline_female_black_24.png + + Resources\drawable-xxhdpi\baseline_folder_black_24.png + Resources\drawable-xxhdpi\baseline_info_black_24.png @@ -799,6 +818,9 @@ Resources\drawable-xxxhdpi\baseline_female_black_24.png + + Resources\drawable-xxxhdpi\baseline_folder_black_24.png + Resources\drawable-xxxhdpi\baseline_info_black_24.png @@ -871,6 +893,9 @@ Resources\layout\activity_encryption_auth.xml + + Resources\layout\activity_explorer.xml + Resources\layout\activity_export_auth.xml @@ -955,6 +980,9 @@ Resources\layout\layout_card_steam_auth_trade_confirmation_content.xml + + Resources\layout\layout_file_or_dir.xml + Resources\layout\layout_login_and_register_by_fast_channel.xml @@ -1079,3 +1107,5 @@ + + diff --git a/src/ST.Client.Mobile.Droid/Services/Implementation/MobilePlatformServiceImpl.cs b/src/ST.Client.Mobile.Droid/Services/Implementation/MobilePlatformServiceImpl.cs index 63c8e9cd1..bff94bb2e 100644 --- a/src/ST.Client.Mobile.Droid/Services/Implementation/MobilePlatformServiceImpl.cs +++ b/src/ST.Client.Mobile.Droid/Services/Implementation/MobilePlatformServiceImpl.cs @@ -5,5 +5,11 @@ namespace System.Application.Services.Implementation internal sealed class MobilePlatformServiceImpl : IMobilePlatformService { public object CurrentPlatformUIHost => XEPlatform.CurrentActivity; + + void IPlatformService.OpenFileByTextReader(string filePath) + => GoToPlatformPages.OpenFile( + XEPlatform.CurrentActivity, + new(filePath), + MediaTypeNames.TXT); } } \ No newline at end of file diff --git a/src/ST.Client.Mobile.Droid/UI/Activities/AboutActivity.cs b/src/ST.Client.Mobile.Droid/UI/Activities/AboutActivity.cs index 0a8f67af3..1ef90ab4a 100644 --- a/src/ST.Client.Mobile.Droid/UI/Activities/AboutActivity.cs +++ b/src/ST.Client.Mobile.Droid/UI/Activities/AboutActivity.cs @@ -127,6 +127,9 @@ namespace System.Application.UI.Activities FontSizeResId = Resource.Dimension.translators_font_size, }); break; + case PreferenceButton.浏览基目录: + this.StartActivity(); + break; } }; var layout = new LinearLayoutManager(this, LinearLayoutManager.Vertical, false); diff --git a/src/ST.Client.Mobile.Droid/UI/Activities/ExplorerActivity.cs b/src/ST.Client.Mobile.Droid/UI/Activities/ExplorerActivity.cs new file mode 100644 index 000000000..ca925c885 --- /dev/null +++ b/src/ST.Client.Mobile.Droid/UI/Activities/ExplorerActivity.cs @@ -0,0 +1,45 @@ +using Android.App; +using Android.Content.PM; +using Android.OS; +using Android.Runtime; +using AndroidX.RecyclerView.Widget; +using Binding; +using ReactiveUI; +using System.Application.UI.Adapters; +using System.Application.UI.ViewModels; + +namespace System.Application.UI.Activities +{ + [Register(JavaPackageConstants.Activities + nameof(ExplorerActivity))] + [Activity(Theme = ManifestConstants.MainTheme_NoActionBar, + LaunchMode = LaunchMode.SingleTask, + ConfigurationChanges = ManifestConstants.ConfigurationChanges)] + internal sealed class ExplorerActivity : BaseActivity + { + protected override int? LayoutResource => Resource.Layout.activity_explorer; + + protected override void OnCreate(Bundle? savedInstanceState) + { + base.OnCreate(savedInstanceState); + + this.SetSupportActionBarWithNavigationClick(binding!.toolbar, true); + + ViewModel!.WhenAnyValue(x => x.Title).SubscribeInMainThread(value => + { + Title = value; + }).AddTo(this); + + var adapter = new ExplorerAdapter(ViewModel!); + adapter.ItemClick += (_, e) => ViewModel!.OnItemClick(e.Current); + var layout = new LinearLayoutManager(this, LinearLayoutManager.Vertical, false); + binding.rvExplorer.SetLayoutManager(layout); + binding.rvExplorer.SetAdapter(adapter); + } + + public override void OnBackPressed() + { + if (ViewModel!.OnBack()) return; + base.OnBackPressed(); + } + } +} \ No newline at end of file diff --git a/src/ST.Client.Mobile.Droid/UI/Activities/TextBlockActivity.cs b/src/ST.Client.Mobile.Droid/UI/Activities/TextBlockActivity.cs index 83f54033f..51e93a271 100644 --- a/src/ST.Client.Mobile.Droid/UI/Activities/TextBlockActivity.cs +++ b/src/ST.Client.Mobile.Droid/UI/Activities/TextBlockActivity.cs @@ -3,9 +3,12 @@ using Android.Content.PM; using Android.OS; using Android.Runtime; using Android.Util; +using AndroidX.Core.Net; using Binding; using System.Application.Models; using System.Application.UI.ViewModels; +using System.IO; +using static Android.Content.Intent; namespace System.Application.UI.Activities { @@ -13,6 +16,7 @@ namespace System.Application.UI.Activities [Activity(Theme = ManifestConstants.MainTheme_NoActionBar, LaunchMode = LaunchMode.SingleTask, ConfigurationChanges = ManifestConstants.ConfigurationChanges)] + [IntentFilter(new[] { ActionView }, Categories = new[] { CategoryDefault }, DataMimeType = MediaTypeNames.TXT)] internal sealed class TextBlockActivity : BaseActivity { protected override int? LayoutResource => Resource.Layout.activity_textblock; @@ -22,6 +26,27 @@ namespace System.Application.UI.Activities base.OnCreate(savedInstanceState); var vm = this.GetViewModel(); + if (vm == null) + { + if (Intent?.Action == ActionView && Intent.Data != null) + { + try + { + var filePath = Intent.Data.EnsurePhysicalPath(); + var fileContent = File.ReadAllText(filePath); + vm = new TextBlockViewModel + { + Title = Path.GetFileName(filePath), + Content = fileContent, + }; + } + catch (Exception e) + { + Log.Error(nameof(TextBlockActivity), nameof(OnCreate), e); + } + } + } + if (vm == null) { Finish(); diff --git a/src/ST.Client.Mobile.Droid/UI/Adapters/ExplorerAdapter.cs b/src/ST.Client.Mobile.Droid/UI/Adapters/ExplorerAdapter.cs new file mode 100644 index 000000000..6c25d9e28 --- /dev/null +++ b/src/ST.Client.Mobile.Droid/UI/Adapters/ExplorerAdapter.cs @@ -0,0 +1,42 @@ +using Android.Views; +using Binding; +using System.Application.UI.ViewModels; +using TViewHolder = System.Application.UI.Adapters.ExplorerViewHolder; +using TViewModel = System.Application.UI.ViewModels.ExplorerViewModel.PathInfoViewModel; + +namespace System.Application.UI.Adapters +{ + internal sealed class ExplorerAdapter : BaseReactiveRecycleViewAdapter + { + public ExplorerAdapter(ExplorerViewModel viewModel) : base(viewModel.PathInfos) + { + } + + public override int? GetLayoutResource(int viewType) + { + return Resource.Layout.layout_file_or_dir; + } + } + + internal sealed class ExplorerViewHolder : BaseReactiveViewHolder + { + readonly layout_file_or_dir binding; + + public ExplorerViewHolder(View itemView) : base(itemView) + { + binding = new(itemView); + } + + public override void OnBind() + { + base.OnBind(); + + binding.ivIcon.SetImageResource( + ViewModel!.IsDirectory ? + Resource.Drawable.baseline_folder_black_24 : + Resource.Drawable.baseline_description_black_24); + binding.tvName.Text = ViewModel.Name; + binding.tvDesc.Text = ViewModel.Desc; + } + } +} \ No newline at end of file diff --git a/src/ST.Client.Mobile.Droid/UI/MainApplication.cs b/src/ST.Client.Mobile.Droid/UI/MainApplication.cs index 053e8e134..a05ecbb59 100644 --- a/src/ST.Client.Mobile.Droid/UI/MainApplication.cs +++ b/src/ST.Client.Mobile.Droid/UI/MainApplication.cs @@ -58,6 +58,8 @@ namespace System.Application.UI #endif var stopwatch = Stopwatch.StartNew(); + AppHelper.InitLogDir(); + VisualStudioAppCenterSDK.Init(); XEFileProvider.TemporaryLocation = FileProviderLocation.Internal; diff --git a/src/ST.Client.Mobile/ST.Client.Mobile.csproj b/src/ST.Client.Mobile/ST.Client.Mobile.csproj index d763b9213..fca5ad14c 100644 --- a/src/ST.Client.Mobile/ST.Client.Mobile.csproj +++ b/src/ST.Client.Mobile/ST.Client.Mobile.csproj @@ -69,6 +69,9 @@ + + UI + Services diff --git a/src/ST.Client.Mobile/UI/AppHelper.cs b/src/ST.Client.Mobile/UI/AppHelper.cs new file mode 100644 index 000000000..1e749ea20 --- /dev/null +++ b/src/ST.Client.Mobile/UI/AppHelper.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace System.Application.UI +{ + public static partial class AppHelper + { + + } +} diff --git a/src/ST.Client.Mobile/UI/ViewModels/ExplorerViewModel.cs b/src/ST.Client.Mobile/UI/ViewModels/ExplorerViewModel.cs new file mode 100644 index 000000000..0a641a6aa --- /dev/null +++ b/src/ST.Client.Mobile/UI/ViewModels/ExplorerViewModel.cs @@ -0,0 +1,182 @@ +using ReactiveUI; +using System.Application.Services; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; + +namespace System.Application.UI.ViewModels +{ + public class ExplorerViewModel : ViewModelBase + { + const double unit = 1024d; + static readonly string[] units = new[] { "B", "KB", "MB", "GB", "TB" }; + public static string GetSize(double length) + { + for (int i = 0; i < units.Length; i++) + { + if (i > 0) length /= unit; + if (length < unit) return $"{length:0.00} {units[i]}"; + } + return $"{length:0.00} {units.Last()}"; + } + + string _CurrentPath = string.Empty; + public string CurrentPath + { + get => _CurrentPath; + set + { + if (_CurrentPath == value) return; + _CurrentPath = value; + var title = DefaultTitle; + foreach (var item in GetRootPaths()) + { + if (_CurrentPath.StartsWith(item.Key)) + { + title = Path.DirectorySeparatorChar + + item.Value + + _CurrentPath.TrimStart(item.Key); + break; + } + } + Title = title; + foreach (var item in PathInfos.ToArray()) + { + PathInfos.Remove(item); + } + if (string.IsNullOrEmpty(_CurrentPath)) + { + PathInfos.AddRange(GetRootPathInfoViewModels>()); + } + else + { + AddRange(PathInfos, _CurrentPath); + } + } + } + + static readonly string DefaultTitle = Path.DirectorySeparatorChar.ToString(); + + string title = DefaultTitle; + public string Title + { + get => title; + set => this.RaiseAndSetIfChanged(ref title, value); + } + + public ObservableCollection PathInfos { get; } = GetRootPathInfoViewModels>(); + + public class PathInfoViewModel : ReactiveObject + { + public PathInfoViewModel(FileSystemInfo fileSystemInfo, string? name = null) + { + Name = name ?? fileSystemInfo.Name; + FullName = fileSystemInfo.FullName; + Desc = $" | {fileSystemInfo.CreationTime.ToString(DateTimeFormat.Standard)}"; + if (fileSystemInfo is FileInfo fileInfo) + { + IsDirectory = false; + var fileSize = GetSize(fileInfo.Length); + Desc = $"Size: {fileSize}{Desc}"; + } + else if (fileSystemInfo is DirectoryInfo dirInfo) + { + IsDirectory = true; + var ignore_rule_log_under_cache_on_cache = AppHelper.LogUnderCache && dirInfo.FullName == IOPath.CacheDirectory; + var filesCount = dirInfo.GetFiles().Length; + var dirsCount = dirInfo.GetDirectories().Length; + Desc = $"Count: {filesCount + (ignore_rule_log_under_cache_on_cache ? dirsCount - 1 : dirsCount)}{Desc}"; + } + else + { + throw new NotSupportedException(); + } + } + + public bool IsDirectory { get; } + + public string Name { get; } + + public string FullName { get; } + + public string Desc { get; } + } + + static IEnumerable> GetRootPaths() + { + yield return new(IOPath.AppDataDirectory, IOPath.DirName_AppData); + yield return new(AppHelper.LogDirPath, AppHelper.LogDirName); + yield return new(IOPath.CacheDirectory, IOPath.DirName_Cache); + } + + static T GetRootPathInfoViewModels() where T : ICollection, new() + { + return new T() + { + new PathInfoViewModel(new DirectoryInfo(IOPath.AppDataDirectory), IOPath.DirName_AppData), + new PathInfoViewModel(new DirectoryInfo(IOPath.CacheDirectory), IOPath.DirName_Cache), + new PathInfoViewModel(new DirectoryInfo(AppHelper.LogDirPath)), + }; + } + + public static void AddRange(IList list, string dirPath) + { + var ignore_rule_log_under_cache_on_cache = AppHelper.LogUnderCache && dirPath == IOPath.CacheDirectory; + var dirInfo = new DirectoryInfo(dirPath); + Array.ForEach(dirInfo.GetDirectories(), x => + { + if (ignore_rule_log_under_cache_on_cache && x.Name == AppHelper.LogDirName) return; + list.Add(new(x)); + }); + Array.ForEach(dirInfo.GetFiles(), x => list.Add(new(x))); + } + + public void OnItemClick(PathInfoViewModel pathInfo) + { + if (pathInfo.IsDirectory) + { + CurrentPath = pathInfo.FullName; + } + else + { + var extension = Path.GetExtension(pathInfo.FullName); + if (string.Equals(extension, FileEx.JSON, StringComparison.OrdinalIgnoreCase) || + string.Equals(extension, FileEx.TXT, StringComparison.OrdinalIgnoreCase) || + string.Equals(extension, FileEx.LOG, StringComparison.OrdinalIgnoreCase)) + { + IPlatformService.Instance.OpenFileByTextReader(pathInfo.FullName); + } + } + } + + public bool OnBack() + { + if (Title != DefaultTitle) + { + var dirs = Title.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); + if (dirs.Length == 1) + { + CurrentPath = string.Empty; + } + else if (dirs.Length > 0) + { + foreach (var item in GetRootPaths()) + { + if (item.Value == dirs[0]) + { + CurrentPath = Path.Combine(new[] { item.Key }.Concat(dirs.Skip(1).Take(dirs.Length - 2)).ToArray()); + break; + } + } + } + else + { + return false; + } + return true; + } + return false; + } + } +} \ No newline at end of file diff --git a/src/ST.Client.Mobile/UI/ViewModels/Pages/About/AboutPageViewModel.cs b/src/ST.Client.Mobile/UI/ViewModels/Pages/About/AboutPageViewModel.cs index f2b031b3b..a89a70125 100644 --- a/src/ST.Client.Mobile/UI/ViewModels/Pages/About/AboutPageViewModel.cs +++ b/src/ST.Client.Mobile/UI/ViewModels/Pages/About/AboutPageViewModel.cs @@ -34,6 +34,7 @@ namespace System.Application.UI.ViewModels 源码仓库, 产品官网, 开放源代码许可, + 浏览基目录, 账号注销, } @@ -67,6 +68,7 @@ namespace System.Application.UI.ViewModels PreferenceButton.Bug反馈 => AppResources.BugFeedback, PreferenceButton.账号注销 => AppResources.DelAccount, PreferenceButton.社区翻译 => AppResources.Translators, + PreferenceButton.浏览基目录 => AppResources.BrowseBaseDirectory, _ => id.ToString(), }; return title; diff --git a/src/ST.Client/AppFolder.cs b/src/ST.Client/AppFolder.cs index dc04e65b5..358b628b5 100644 --- a/src/ST.Client/AppFolder.cs +++ b/src/ST.Client/AppFolder.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; namespace System.Application { diff --git a/src/ST.Client/Services/IPlatformService.cs b/src/ST.Client/Services/IPlatformService.cs index 9e0fab936..0158da6b0 100644 --- a/src/ST.Client/Services/IPlatformService.cs +++ b/src/ST.Client/Services/IPlatformService.cs @@ -9,8 +9,14 @@ namespace System.Application.Services public static IPlatformService Instance => DI.Get(); string[] GetMacNetworksetup() => throw new PlatformNotSupportedException(); - async void AdminShell(string str,bool admin=false) => await AdminShellAsync(str,admin); + async void AdminShell(string str, bool admin = false) => await AdminShellAsync(str, admin); ValueTask AdminShellAsync(string str, bool admin = false) => throw new PlatformNotSupportedException(); + + /// + /// 使用文本阅读器打开文件 + /// + /// + void OpenFileByTextReader(string filePath); } } \ No newline at end of file diff --git a/src/ST.Tools.AreaImport/ST.Tools.AreaImport.csproj b/src/ST.Tools.AreaImport/ST.Tools.AreaImport.csproj index c94b11d01..bbb3f6131 100644 --- a/src/ST.Tools.AreaImport/ST.Tools.AreaImport.csproj +++ b/src/ST.Tools.AreaImport/ST.Tools.AreaImport.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/src/ST.Tools.OpenSourceLibraryList/ST.Tools.OpenSourceLibraryList.csproj b/src/ST.Tools.OpenSourceLibraryList/ST.Tools.OpenSourceLibraryList.csproj index 014881130..9845c7b84 100644 --- a/src/ST.Tools.OpenSourceLibraryList/ST.Tools.OpenSourceLibraryList.csproj +++ b/src/ST.Tools.OpenSourceLibraryList/ST.Tools.OpenSourceLibraryList.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/src/ST.Tools.Packager/Application/UI/Views/Form1.cs b/src/ST.Tools.Packager/Application/UI/Views/Form1.cs index ee90ab1d3..88defa435 100644 --- a/src/ST.Tools.Packager/Application/UI/Views/Form1.cs +++ b/src/ST.Tools.Packager/Application/UI/Views/Form1.cs @@ -1,4 +1,4 @@ -using ICSharpCode.SharpZipLib.Tar; +using ICSharpCode.SharpZipLib.Tar; using System.IO; using System.Linq; using System.Threading.Tasks; diff --git a/src/ST.Tools.Packager/ST.Tools.Packager.csproj b/src/ST.Tools.Packager/ST.Tools.Packager.csproj index 0e522ecc5..befb5604e 100644 --- a/src/ST.Tools.Packager/ST.Tools.Packager.csproj +++ b/src/ST.Tools.Packager/ST.Tools.Packager.csproj @@ -1,4 +1,4 @@ - + WinExe diff --git a/src/ST.Tools.Translate/ST.Tools.Translate.csproj b/src/ST.Tools.Translate/ST.Tools.Translate.csproj index 446cf2375..d81fc219a 100644 --- a/src/ST.Tools.Translate/ST.Tools.Translate.csproj +++ b/src/ST.Tools.Translate/ST.Tools.Translate.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/src/Startup2.cs b/src/Startup2.cs index fbc88f43a..c1dc4b145 100644 --- a/src/Startup2.cs +++ b/src/Startup2.cs @@ -98,13 +98,7 @@ namespace System.Application static void ConfigureRequiredServices(IServiceCollection services) { // 添加日志实现 -#if __ANDROID__ - services.AddClientLogging(); -#elif __IOS__ - services.AddLogging(cfg => cfg.AddProvider(NullLoggerProvider.Instance)); -#else - services.AddDesktopLogging(); -#endif + services.AddGeneralLogging(); #if __MOBILE__ // 添加运行时权限 services.TryAddPermissions();