using System.Text.Json;
using JiDangJia.Contracts;
using JiDangJia.Services;
namespace JiDangJia.Views;
public sealed class AppUpdatePage : ContentPage
{
private readonly AppRelease release;private readonly INavigation owner;private readonly Action closed;private readonly bool required;
private readonly Label state=Ui.Text("",13,Ui.Muted);private readonly ProgressBar progress=new(){ProgressColor=Ui.Bronze};
private readonly Button download;private readonly CancellationTokenSource cancellation=new();private string? apk;private bool busy,closing;
public AppUpdatePage(AppRelease release,INavigation owner,Action closed)
{
this.release=release;this.owner=owner;this.closed=closed;required=release.RequiredFor(AppUpdates.InstalledCode);Title="发现新版本";NavigationPage.SetHasBackButton(this,false);
download=Ui.Button("下载并更新",Update);
var body=Ui.Stack(Ui.Card(Ui.Stack(Ui.Section(AppProduct.Name+" "+release.VersionName),Ui.Text("当前版本 "+AppInfo.Current.VersionString+" · 安装包 "+(release.Size/1048576d).ToString("0.0")+" MB",12,Ui.Muted),Ui.Text(required?"当前版本需要更新后继续使用。":"更新后继续使用现有账号与本机数据。",13,Ui.Bronze),Ui.Divider(),Ui.Text(release.Notes,15))),progress,state,download);
if(!required)body.Add(Ui.Button("稍后更新",Close,false));body.Padding=22;Content=new ScrollView{Content=body};
}
private async Task Update()
{
busy=true;
try
{
if(apk is null)
{state.Text="正在下载…";download.Text="正在下载";apk=await AppUpdates.Download(release,new Progress<double>(value=>{progress.Progress=value;state.Text="正在下载 "+(value*100).ToString("0")+"%";}),cancellation.Token);}
state.Text="校验通过,正在打开系统安装界面。";
if(!AppUpdates.Install(apk,release)){state.Text="请在系统页面允许安装此来源的应用,返回后点击“继续安装”。";download.Text="继续安装";}
else{state.Text="请在系统安装界面确认。安装完成后重新打开"+AppProduct.Name+"。";download.Text="再次打开安装界面";}
}
catch(OperationCanceledException){state.Text="下载已取消。";}
catch(Exception ex){apk=null;state.Text=ex is HttpRequestException?"下载未完成,请检查网络后重试。":ex.Message;download.Text="重试更新";}
finally{busy=false;}
}
private async Task Close(){if(closing)return;closing=true;cancellation.Cancel();AppUpdates.Defer(release.Id);try{await owner.PopAsync();}finally{closed();}}
protected override bool OnBackButtonPressed(){if(required||busy)return true;_ = Close();return true;}
}
public sealed class AppUpdateAdminPage : ContentPage
{
private readonly Editor notes=new(){Placeholder="填写本次更新内容",AutoSize=EditorAutoSizeOption.TextChanges,MinimumHeightRequest=100,MaxLength=8000};
private readonly Entry minimum=new(){Text="0",Keyboard=Keyboard.Numeric,Placeholder="最低可用版本号,0 表示可稍后更新"};
private readonly Label selected=Ui.Text("尚未选择安装包",13,Ui.Muted),status=Ui.Text("",13,Ui.Bronze);
private readonly VerticalStackLayout list=Ui.Stack();private FileResult? file;private bool uploading;
public AppUpdateAdminPage()
{
if(!AuthClient.Current.IsAdmin)throw new InvalidOperationException("需要管理员权限");Title="APP 版本更新管理";
var body=Ui.Stack(Ui.Card(Ui.Stack(Ui.Section("发布 Android 更新"),Ui.Text("版本名称和版本号自动从 APK 读取。用户打开 APP 时收到更新提示,可在设置中手动检查。",12,Ui.Muted),selected,Ui.Button("选择签名 APK",Pick,false),notes,Ui.Text("最低可用版本号(构建号)",12,Ui.Muted),minimum,Ui.Text("0:可稍后更新;填写新版本构建号:所有旧版本须更新。安装包须与已安装应用使用相同签名。",12,Ui.Muted),status,Ui.Button("发布更新",Publish))),Ui.Section("已发布版本"),list,Ui.Button("清理旧安装包,仅保留最新有效版",Clean,false));body.Padding=18;Content=new ScrollView{Content=body};
}
protected override async void OnAppearing(){base.OnAppearing();try{await Load();}catch(Exception ex){status.Text=ex.Message;}}
private async Task Pick()
{if(uploading)return;using var scope=new ExternalPhotoScope();file=await FilePicker.Default.PickAsync(new PickOptions{PickerTitle="选择"+AppProduct.Name+"签名 APK"});if(file is not null)selected.Text=file.FileName;}
private async Task Publish()
{
if(file is null)throw new ArgumentException("请先选择安装包");if(!int.TryParse(minimum.Text,out var code)||code<0)throw new ArgumentException("最低可用版本号请填写非负整数");
var request=new PublishReleaseRequest(code,notes.Text??"");if(string.IsNullOrWhiteSpace(request.Notes)||request.Notes.Length>8000)throw new ArgumentException("请填写更新说明");
uploading=true;status.Text="正在上传并校验安装包,请保持此页面打开…";
try{await using var stream=await file.OpenReadAsync();var released=await AuthClient.Current.PublishUpdate(stream,request,CancellationToken.None);file=null;selected.Text="尚未选择安装包";notes.Text="";status.Text="已发布 "+released.VersionName+"("+released.VersionCode+")";await Load();}
catch{status.Text="发布未完成,请刷新列表确认后重试。";throw;}
finally{uploading=false;}
}
private async Task Load()
{
using var data=await AuthClient.Current.AdminRequest("/updates");var rows=data.RootElement.Deserialize<List<AppRelease>>(new JsonSerializerOptions(JsonSerializerDefaults.Web))??[];list.Clear();
if(rows.Count==0)list.Add(Ui.Text("尚未发布更新。",13,Ui.Muted));
foreach(var release in rows)
{
var card=Ui.Stack(Ui.Text(release.VersionName+"("+release.VersionCode+")"+(release.Enabled?" · 已发布":" · 已撤回"),16,bold:true),Ui.Text(release.Notes,13),Ui.Text("最低版本号 "+release.MinimumVersionCode+" · "+(release.Size/1048576d).ToString("0.0")+" MB",11,Ui.Muted));
if(release.Enabled)card.Add(Ui.Button("撤回此更新",async()=>{if(await DisplayAlertAsync("撤回更新","停止向用户提供此版本?","撤回","取消")){using var result=await AuthClient.Current.AdminRequest("/updates/"+release.Id+"/withdraw",new{});await Load();}},false));list.Add(Ui.Card(card));
}
}
private async Task Clean()
{if(uploading)return;if(await DisplayAlertAsync("清理旧安装包","保留最新有效版本,删除其余服务器更新安装包。正在下载旧版本的用户需重新检查更新。","清理","取消")){using var result=await AuthClient.Current.AdminRequest("/updates/clean",new{});status.Text="已清理 "+result.RootElement.GetProperty("removed").GetInt32()+" 个旧安装包";await Load();}}
}
using System.Text.Json;
using JiDangJia.Contracts;
using JiDangJia.Services;
namespace JiDangJia.Views;
public sealed class AppUpdatePage : ContentPage
{
private readonly AppRelease release;private readonly INavigation owner;private readonly Action closed;private readonly bool required;
private readonly Label state=Ui.Text("",13,Ui.Muted);private readonly ProgressBar progress=new(){ProgressColor=Ui.Bronze};
private readonly Button download;private readonly CancellationTokenSource cancellation=new();private string? apk;private bool busy,closing;
public AppUpdatePage(AppRelease release,INavigation owner,Action closed)
{
this.release=release;this.owner=owner;this.closed=closed;required=release.RequiredFor(AppUpdates.InstalledCode);Title="发现新版本";NavigationPage.SetHasBackButton(this,false);
download=Ui.Button("下载并更新",Update);
var body=Ui.Stack(Ui.Card(Ui.Stack(Ui.Section(AppProduct.Name+" "+release.VersionName),Ui.Text("当前版本 "+AppInfo.Current.VersionString+" · 安装包 "+(release.Size/1048576d).ToString("0.0")+" MB",12,Ui.Muted),Ui.Text(required?"当前版本需要更新后继续使用。":"更新后继续使用现有账号与本机数据。",13,Ui.Bronze),Ui.Divider(),Ui.Text(release.Notes,15))),progress,state,download);
if(!required)body.Add(Ui.Button("稍后更新",Close,false));body.Padding=22;Content=new ScrollView{Content=body};
}
private async Task Update()
{
busy=true;
try
{
if(apk is null)
{state.Text="正在下载…";download.Text="正在下载";apk=await AppUpdates.Download(release,new Progress<double>(value=>{progress.Progress=value;state.Text="正在下载 "+(value*100).ToString("0")+"%";}),cancellation.Token);}
state.Text="校验通过,正在打开系统安装界面。";
if(!AppUpdates.Install(apk,release)){state.Text="请在系统页面允许安装此来源的应用,返回后点击“继续安装”。";download.Text="继续安装";}
else{state.Text="请在系统安装界面确认。安装完成后重新打开"+AppProduct.Name+"。";download.Text="再次打开安装界面";}
}
catch(OperationCanceledException){state.Text="下载已取消。";}
catch(Exception ex){apk=null;state.Text=ex is HttpRequestException?"下载未完成,请检查网络后重试。":ex.Message;download.Text="重试更新";}
finally{busy=false;}
}
private async Task Close(){if(closing)return;closing=true;cancellation.Cancel();AppUpdates.Defer(release.Id);try{await owner.PopAsync();}finally{closed();}}
protected override bool OnBackButtonPressed(){if(required||busy)return true;_ = Close();return true;}
}
public sealed class AppUpdateAdminPage : ContentPage
{
private readonly Editor notes=new(){Placeholder="填写本次更新内容",AutoSize=EditorAutoSizeOption.TextChanges,MinimumHeightRequest=100,MaxLength=8000};
private readonly Entry minimum=new(){Text="0",Keyboard=Keyboard.Numeric,Placeholder="最低可用版本号,0 表示可稍后更新"};
private readonly Label selected=Ui.Text("尚未选择安装包",13,Ui.Muted),status=Ui.Text("",13,Ui.Bronze);
private readonly VerticalStackLayout list=Ui.Stack();private FileResult? file;private bool uploading;
public AppUpdateAdminPage()
{
if(!AuthClient.Current.IsAdmin)throw new InvalidOperationException("需要管理员权限");Title="APP 版本更新管理";
var body=Ui.Stack(Ui.Card(Ui.Stack(Ui.Section("发布 Android 更新"),Ui.Text("版本名称和版本号自动从 APK 读取。用户打开 APP 时收到更新提示,可在设置中手动检查。",12,Ui.Muted),selected,Ui.Button("选择签名 APK",Pick,false),notes,Ui.Text("最低可用版本号(构建号)",12,Ui.Muted),minimum,Ui.Text("0:可稍后更新;填写新版本构建号:所有旧版本须更新。安装包须与已安装应用使用相同签名。",12,Ui.Muted),status,Ui.Button("发布更新",Publish))),Ui.Section("已发布版本"),list,Ui.Button("清理旧安装包,仅保留最新有效版",Clean,false));body.Padding=18;Content=new ScrollView{Content=body};
}
protected override async void OnAppearing(){base.OnAppearing();try{await Load();}catch(Exception ex){status.Text=ex.Message;}}
private async Task Pick()
{if(uploading)return;using var scope=new ExternalPhotoScope();file=await FilePicker.Default.PickAsync(new PickOptions{PickerTitle="选择"+AppProduct.Name+"签名 APK"});if(file is not null)selected.Text=file.FileName;}
private async Task Publish()
{
if(file is null)throw new ArgumentException("请先选择安装包");if(!int.TryParse(minimum.Text,out var code)||code<0)throw new ArgumentException("最低可用版本号请填写非负整数");
var request=new PublishReleaseRequest(code,notes.Text??"");if(string.IsNullOrWhiteSpace(request.Notes)||request.Notes.Length>8000)throw new ArgumentException("请填写更新说明");
uploading=true;status.Text="正在上传并校验安装包,请保持此页面打开…";
try{await using var stream=await file.OpenReadAsync();var released=await AuthClient.Current.PublishUpdate(stream,request,CancellationToken.None);file=null;selected.Text="尚未选择安装包";notes.Text="";status.Text="已发布 "+released.VersionName+"("+released.VersionCode+")";await Load();}
catch{status.Text="发布未完成,请刷新列表确认后重试。";throw;}
finally{uploading=false;}
}
private async Task Load()
{
using var data=await AuthClient.Current.AdminRequest("/updates");var rows=data.RootElement.Deserialize<List<AppRelease>>(new JsonSerializerOptions(JsonSerializerDefaults.Web))??[];list.Clear();
if(rows.Count==0)list.Add(Ui.Text("尚未发布更新。",13,Ui.Muted));
foreach(var release in rows)
{
var card=Ui.Stack(Ui.Text(release.VersionName+"("+release.VersionCode+")"+(release.Enabled?" · 已发布":" · 已撤回"),16,bold:true),Ui.Text(release.Notes,13),Ui.Text("最低版本号 "+release.MinimumVersionCode+" · "+(release.Size/1048576d).ToString("0.0")+" MB",11,Ui.Muted));
if(release.Enabled)card.Add(Ui.Button("撤回此更新",async()=>{if(await DisplayAlertAsync("撤回更新","停止向用户提供此版本?","撤回","取消")){using var result=await AuthClient.Current.AdminRequest("/updates/"+release.Id+"/withdraw",new{});await Load();}},false));list.Add(Ui.Card(card));
}
}
private async Task Clean()
{if(uploading)return;if(await DisplayAlertAsync("清理旧安装包","保留最新有效版本,删除其余服务器更新安装包。正在下载旧版本的用户需重新检查更新。","清理","取消")){using var result=await AuthClient.Current.AdminRequest("/updates/clean",new{});status.Text="已清理 "+result.RootElement.GetProperty("removed").GetInt32()+" 个旧安装包";await Load();}}
}