M1 공유 타겟(U1)+M2 저장 진행 알림: ACTION_SEND 인텐트 필터+ShareTarget/SharePayload(RED 7건)+루트 타이머 큐 전송(am start 인텐트 E2E로 서버 큐 반영 실증), 오프라인 저장 Android ProgressBar 노티(5% 스로틀)→폰 저장 완료 전환(34MB E2E, Report(1) 되덮어쓰기 방지). 154/154 GREEN
This commit is contained in:
parent
69094595a5
commit
04ff0ff93d
7 changed files with 166 additions and 6 deletions
|
|
@ -163,6 +163,21 @@ public sealed class QueuePage : ContentPage
|
|||
_summary.Text = err != null ? $"❌ {err}" : items.Count == 0 ? "대기 중인 항목이 없습니다" : $"{items.Count}건";
|
||||
}
|
||||
|
||||
/// <summary>U1: 공유받은 링크 처리 결과 표시(루트 타이머가 호출).</summary>
|
||||
public void ShowSharedResult(string message) => _summary.Text = message;
|
||||
|
||||
/// <summary>U1: 공유 타겟으로 받은 텍스트에서 URL 을 추출해 PC 대기열에 추가.</summary>
|
||||
private async Task HandleSharedUrlAsync()
|
||||
{
|
||||
var shared = ShareTarget.Consume();
|
||||
if (shared == null || !ApiClient.Instance.IsConfigured) return;
|
||||
var url = SharePayload.ExtractUrl(shared);
|
||||
if (url == null) { _summary.Text = "🔗 공유받은 텍스트에 URL 이 없습니다"; return; }
|
||||
_summary.Text = "🔗 공유 링크 전송 중…";
|
||||
var ok = await ApiClient.Instance.AddAsync(url);
|
||||
_summary.Text = ok ? $"🔗 공유 링크 전송됨: {url}" : "❌ 공유 링크 전송 실패";
|
||||
}
|
||||
|
||||
/// <summary>U5: 완료 로컬 알림(Android 13+ 는 최초 1회 권한 요청).</summary>
|
||||
private static async Task NotifyCompletionAsync(string title)
|
||||
{
|
||||
|
|
@ -234,14 +249,19 @@ public sealed class LibraryPage : ContentPage
|
|||
if (item == null) return;
|
||||
if (AppOfflineStore.Default.Exists(item.Id)) { button.Text = "✓ 저장됨"; return; }
|
||||
button.Text = "0%";
|
||||
var notifId = 5000 + Math.Abs(item.Id.GetHashCode() % 1000); // M2 진행 알림: 항목별 고정 id 로 갱신
|
||||
try
|
||||
{
|
||||
var dest = AppOfflineStore.Default.PathFor(item.Id);
|
||||
var progress = new Progress<double>(p => MainThread.BeginInvokeOnMainThread(
|
||||
() => button.Text = p >= 1 ? "✓ 저장됨" : $"{(int)(p * 100)}%"));
|
||||
var progress = new Progress<double>(p => MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
button.Text = p >= 1 ? "✓ 저장됨" : $"{(int)(p * 100)}%";
|
||||
if (p < 1) ShowSaveProgress(notifId, item.FileName, p); // 100% 는 완료 노티가 대체(되덮어쓰기 방지)
|
||||
}));
|
||||
await ApiClient.Instance.DownloadFileAsync(item, dest, progress, CancellationToken.None);
|
||||
AppOfflineStore.Default.Record(item.Id, item.FileName);
|
||||
button.Text = "✓ 저장됨";
|
||||
ShowSaveDone(notifId, item.FileName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -249,6 +269,53 @@ public sealed class LibraryPage : ContentPage
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>M2: 오프라인 저장 진행 알림(같은 id 갱신) — 5% 단위 스로틀.</summary>
|
||||
private static int _lastNotifiedPercent = -1;
|
||||
|
||||
private static void ShowSaveProgress(int notifId, string fileName, double p)
|
||||
{
|
||||
var percent = (int)(p * 100);
|
||||
if (p < 1 && percent / 5 == _lastNotifiedPercent) return; // 알림 스팸 방지
|
||||
_lastNotifiedPercent = percent / 5;
|
||||
try
|
||||
{
|
||||
Plugin.LocalNotification.LocalNotificationCenter.Current.Show(new Plugin.LocalNotification.NotificationRequest
|
||||
{
|
||||
NotificationId = notifId,
|
||||
Title = "폰에 저장 중",
|
||||
Description = $"{fileName} — {percent}%",
|
||||
Android =
|
||||
{
|
||||
ChannelId = "vd_save",
|
||||
Ongoing = true,
|
||||
AutoCancel = false,
|
||||
ProgressBar = new Plugin.LocalNotification.AndroidOption.AndroidProgressBar
|
||||
{
|
||||
Max = 100,
|
||||
Progress = percent,
|
||||
IsIndeterminate = false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static void ShowSaveDone(int notifId, string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
Plugin.LocalNotification.LocalNotificationCenter.Current.Show(new Plugin.LocalNotification.NotificationRequest
|
||||
{
|
||||
NotificationId = notifId,
|
||||
Title = "폰 저장 완료",
|
||||
Description = fileName,
|
||||
Android = { ChannelId = "vd_save", Ongoing = false, AutoCancel = true },
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async void OpenPlayer(LibraryItem item)
|
||||
{
|
||||
// 탭 자식엔 NavigationPage 가 없어 PushAsync 불가 → 루트에서 모달 push (닫기는 상단바 뒤로)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,31 @@
|
|||
using Android.App;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
|
||||
namespace VideoDownloader.Mobile;
|
||||
|
||||
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
|
||||
// M1 공유 타겟(U1): 다른 앱의 공유 시트에 "PC에서 받기" 로 등록
|
||||
[IntentFilter(new[] { Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataMimeType = "text/plain", Label = "PC에서 받기")]
|
||||
public class MainActivity : MauiAppCompatActivity
|
||||
{
|
||||
protected override void OnCreate(Bundle? savedInstanceState)
|
||||
{
|
||||
base.OnCreate(savedInstanceState);
|
||||
ReceiveShare(Intent);
|
||||
}
|
||||
|
||||
protected override void OnNewIntent(Intent? intent)
|
||||
{
|
||||
base.OnNewIntent(intent);
|
||||
ReceiveShare(intent);
|
||||
}
|
||||
|
||||
private static void ReceiveShare(Intent? intent)
|
||||
{
|
||||
if (intent?.Action != Intent.ActionSend) return;
|
||||
var text = intent.GetStringExtra(Intent.ExtraText);
|
||||
ShareTarget.Deliver(text);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
VideoDownloader.Mobile/ShareTarget.cs
Normal file
16
VideoDownloader.Mobile/ShareTarget.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
namespace VideoDownloader.Mobile;
|
||||
|
||||
/// <summary>M1 공유 타겟(U1): 플랫폼(안드로이드 인텐트)이 전달한 공유 텍스트 큐.</summary>
|
||||
public static class ShareTarget
|
||||
{
|
||||
private static string? _pending;
|
||||
|
||||
public static void Deliver(string? sharedText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sharedText)) return;
|
||||
Volatile.Write(ref _pending, sharedText);
|
||||
}
|
||||
|
||||
/// <summary>대기 중 공유 텍스트를 꺼낸다(없으면 null).</summary>
|
||||
public static string? Consume() => Interlocked.Exchange(ref _pending, null);
|
||||
}
|
||||
|
|
@ -1,15 +1,33 @@
|
|||
namespace VideoDownloader.Mobile;
|
||||
|
||||
/// <summary>컴패니언 루트: 연결 / 큐 / 라이브러리 탭.</summary>
|
||||
/// <summary>컴팠니언 루트: 큐 / 라이브러리 / 저장 / 연결 탭 + 공유 타겟 처리.</summary>
|
||||
public sealed class TabbedRootPage : TabbedPage
|
||||
{
|
||||
public TabbedRootPage()
|
||||
{
|
||||
ApiClient.Instance.Restore();
|
||||
Children.Add(new QueuePage());
|
||||
var queuePage = new QueuePage();
|
||||
Children.Add(queuePage);
|
||||
Children.Add(new LibraryPage());
|
||||
Children.Add(new DownloadsPage());
|
||||
Children.Add(new ConnectPage());
|
||||
Title = "VD 원격";
|
||||
|
||||
// M1 공유 타겟(U1): 어떤 탭에 있든/다른 앱에서 공유받으면 URL 을 PC 큐로 전송 후 큐 탭 표시
|
||||
Device.StartTimer(TimeSpan.FromSeconds(2), () =>
|
||||
{
|
||||
_ = HandleShareAsync(queuePage);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task HandleShareAsync(QueuePage queuePage)
|
||||
{
|
||||
var shared = ShareTarget.Consume();
|
||||
if (shared == null || !ApiClient.Instance.IsConfigured) return;
|
||||
var url = SharePayload.ExtractUrl(shared);
|
||||
if (url == null) { queuePage.ShowSharedResult("🔗 공유받은 텍스트에 URL 이 없습니다"); return; }
|
||||
var ok = await ApiClient.Instance.AddAsync(url);
|
||||
queuePage.ShowSharedResult(ok ? $"🔗 공유 링크 전송됨: {url}" : "❌ 공유 링크 전송 실패");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue