予約投稿を行う。

This commit is contained in:
tateisu 2019-01-06 17:20:37 +09:00
parent 4e83cb9fcc
commit 14530707c3
17 changed files with 457 additions and 59 deletions

View File

@ -40,6 +40,7 @@ import okhttp3.RequestBody
import okio.BufferedSink
import org.json.JSONObject
import java.io.*
import java.util.regex.Pattern
class ActAccountSetting
: AppCompatActivity(), View.OnClickListener, CompoundButton.OnCheckedChangeListener {
@ -1157,7 +1158,8 @@ class ActAccountSetting
private fun sendNote(bConfirmed : Boolean = false) {
val sv = etNote.text.toString()
if(! bConfirmed) {
val length = sv.codePointCount(0, sv.length)
val length = countNoteText(sv)
if(length > max_length_note) {
AlertDialog.Builder(this)
.setMessage(
@ -1178,6 +1180,18 @@ class ActAccountSetting
updateCredential("note", EmojiDecoder.decodeShortCode(sv))
}
// Mastodon 2.7 でnoteの文字数計算が変わる
// https://github.com/tootsuite/mastodon/commit/45899cfa691b1e4f43da98c456ae8faa584eb437
private val reLinkUrl = Pattern.compile("""(https?://[\w/:%#@${'$'}&?!()\[\]~.=+\-]+)""")
private val reMention =Pattern.compile("""(?<=^|[^/\w\p{Pc}])@((\w+([\w.-]+\w+)?)(?:@[a-z0-9.\-]+[a-z0-9]+)?)""",Pattern.CASE_INSENSITIVE)
private val strUrlReplacement = (0 until 23).map{ ' '}.joinToString()
private fun countNoteText(s:String):Int{
val s2 = s
.replaceAll(reLinkUrl,strUrlReplacement)
.replaceAll(reMention,"@\\2")
return s2.codePointCount(0,s2.length)
}
private fun sendLocked(willLocked : Boolean) {
updateCredential("locked", willLocked)
}

View File

@ -725,16 +725,22 @@ class ActMain : AppCompatActivity()
etQuickToot.hideKeyboard()
post_helper.post(
account
) { target_account, status ->
etQuickToot.setText("")
posted_acct = target_account.acct
posted_status_id = status.id
posted_reply_id = status.in_reply_to_id
posted_redraft_id = null
refreshAfterPost()
}
post_helper.post(account,callback=object:PostHelper.PostCompleteCallback{
override fun onPostComplete(
target_account : SavedAccount,
status : TootStatus
) {
etQuickToot.setText("")
posted_acct = target_account.acct
posted_status_id = status.id
posted_reply_id = status.in_reply_to_id
posted_redraft_id = null
refreshAfterPost()
}
override fun onScheduledPostComplete(target_account : SavedAccount) { // TODO
}
})
}
override fun onPageScrolled(
@ -1102,6 +1108,13 @@ class ActMain : AppCompatActivity()
, bAllowPseudo = false
, bAllowMisskey = false
)
R.id.nav_scheduled_statuses_list-> Action_Account.timeline(
this
, defaultInsertPosition
, Column.TYPE_SCHEDULED_STATUS
, bAllowPseudo = false
, bAllowMisskey = false
)
R.id.nav_add_list -> Action_Account.timeline(
this
, defaultInsertPosition

View File

@ -30,6 +30,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.*
import jp.juggler.subwaytooter.R.string.status
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.dialog.*
@ -57,7 +58,9 @@ import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callback {
class ActPost : AppCompatActivity(),
View.OnClickListener,
PostAttachment.Callback {
companion object {
internal val log = LogCategory("ActPost")
@ -166,6 +169,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
private const val STATE_MUSHROOM_END = "mushroom_end"
private const val STATE_REDRAFT_STATUS_ID = "redraft_status_id"
private const val STATE_URI_CAMERA_IMAGE = "uri_camera_image"
private const val STATE_TIME_SCHEDULE = "time_schedule"
fun open(
activity : Activity,
@ -256,6 +260,10 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
private lateinit var ivReply : MyNetworkImageView
private lateinit var scrollView : ScrollView
private lateinit var tvSchedule : TextView
private lateinit var ibSchedule : ImageButton
private lateinit var ibScheduleReset : ImageButton
internal lateinit var pref : SharedPreferences
internal lateinit var app_state : AppState
private lateinit var post_helper : PostHelper
@ -268,6 +276,8 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
private var redraft_status_id : EntityId? = null
private var timeSchedule = 0L
private val text_watcher : TextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence : CharSequence, i : Int, i1 : Int, i2 : Int) {
@ -308,7 +318,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
private var mushroom_end : Int = 0
private val link_click_listener : MyClickableSpanClickCallback = { _, span ->
App1.openBrowser(this@ActPost,span.url)
App1.openBrowser(this@ActPost, span.url)
}
////////////////////////////////////////////////////////////////
@ -327,6 +337,8 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
R.id.btnMore -> performMore()
R.id.btnPlugin -> openMushroom()
R.id.btnEmojiPicker -> post_helper.openEmojiPickerFromMore()
R.id.ibSchedule -> performSchedule()
R.id.ibScheduleReset -> resetSchedule()
}
}
@ -415,6 +427,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
mushroom_start = savedInstanceState.getInt(STATE_MUSHROOM_START, 0)
mushroom_end = savedInstanceState.getInt(STATE_MUSHROOM_END, 0)
redraft_status_id = EntityId.from(savedInstanceState, STATE_REDRAFT_STATUS_ID)
timeSchedule = savedInstanceState.getLong(STATE_TIME_SCHEDULE, 0L)
savedInstanceState.getString(STATE_URI_CAMERA_IMAGE).mayUri()?.let {
uriCameraImage = it
@ -724,6 +737,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
showReplyTo()
showEnquete()
showQuotedRenote()
showSchedule()
}
override fun onDestroy() {
@ -741,6 +755,9 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
outState.putInt(STATE_MUSHROOM_START, mushroom_start)
outState.putInt(STATE_MUSHROOM_END, mushroom_end)
redraft_status_id?.putTo(outState, STATE_REDRAFT_STATUS_ID)
outState.putLong(STATE_TIME_SCHEDULE, timeSchedule)
if(uriCameraImage != null) {
outState.putString(STATE_URI_CAMERA_IMAGE, uriCameraImage.toString())
}
@ -894,6 +911,13 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
btnRemoveReply = findViewById(R.id.btnRemoveReply)
ivReply = findViewById(R.id.ivReply)
tvSchedule = findViewById(R.id.tvSchedule)
ibSchedule = findViewById(R.id.ibSchedule)
ibScheduleReset= findViewById(R.id.ibScheduleReset)
ibSchedule.setOnClickListener(this)
ibScheduleReset.setOnClickListener(this)
account_list = SavedAccount.loadAccountList(this@ActPost)
SavedAccount.sort(account_list)
@ -969,18 +993,19 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
private var lastInstanceTask : TootTaskRunner? = null
private fun getMaxCharCount() : Int {
val account = account
when{
account == null || account.isPseudo -> {}
else->{
when {
account == null || account.isPseudo -> {
}
else -> {
val info = account.instance
// 情報がないか古いなら再取得
if(info == null || System.currentTimeMillis() - info.time_parse >= 300000L) {
// 同時に実行するタスクは1つまで
var lastTask = lastInstanceTask
if(lastTask?.isActive != true) {
@ -990,15 +1015,16 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
var newInfo : TootInstance? = null
override fun background(client : TootApiClient) : TootApiResult? {
val result = if( account.isMisskey){
val result = if(account.isMisskey) {
client.request(
"/api/meta",
account.putMisskeyApiToken().toPostRequestBuilder()
)
}else{
} else {
client.request("/api/v1/instance")
}
newInfo = TootParser(this@ActPost, account).instance(result?.jsonObject)
newInfo =
TootParser(this@ActPost, account).instance(result?.jsonObject)
return result
}
@ -1299,7 +1325,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
override fun background(client : TootApiClient) : TootApiResult? {
try {
val result = client.request(
"/api/v1/media/${attachment.id}" ,
"/api/v1/media/${attachment.id}",
JSONObject()
.put("focus", "%.2f,%.2f".format(x, y))
.toPutRequestBuilder()
@ -1809,7 +1835,6 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
}
}
)
val result = client.request(
"/api/v1/media",
@ -2090,16 +2115,30 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
post_helper.useQuotedRenote = cbQuoteRenote.isChecked
post_helper.post(account) { target_account, status ->
val data = Intent()
data.putExtra(EXTRA_POSTED_ACCT, target_account.acct)
status.id.putTo(data, EXTRA_POSTED_STATUS_ID)
redraft_status_id?.putTo(data, EXTRA_POSTED_REDRAFT_ID)
status.in_reply_to_id?.putTo(data, EXTRA_POSTED_REPLY_ID)
setResult(RESULT_OK, data)
isPostComplete = true
this@ActPost.finish()
}
post_helper.scheduledAt = timeSchedule
post_helper.post(account,callback=object:PostHelper.PostCompleteCallback{
override fun onPostComplete(
target_account : SavedAccount,
status : TootStatus
) {
val data = Intent()
data.putExtra(EXTRA_POSTED_ACCT, target_account.acct)
status.id.putTo(data, EXTRA_POSTED_STATUS_ID)
redraft_status_id?.putTo(data, EXTRA_POSTED_REDRAFT_ID)
status.in_reply_to_id?.putTo(data, EXTRA_POSTED_REPLY_ID)
setResult(RESULT_OK, data)
isPostComplete = true
this@ActPost.finish()
}
override fun onScheduledPostComplete(target_account : SavedAccount) {
showToast(this@ActPost,false,getString(R.string.scheduled_status_sent))
setResult(Activity.RESULT_CANCELED)
isPostComplete = true
this@ActPost.finish()
}
})
}
private fun showQuotedRenote() {
@ -2549,4 +2588,23 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
true
}
private fun showSchedule() {
tvSchedule.text = when(timeSchedule) {
0L -> getString(R.string.unspecified)
else -> TootStatus.formatTime(this, timeSchedule, Pref.bpRelativeTimestamp(pref))
}
}
private fun performSchedule() {
DlgDateTime(this).open(timeSchedule) { t ->
timeSchedule = t
showSchedule()
}
}
private fun resetSchedule(){
timeSchedule = 0L
showSchedule()
}
}

View File

@ -218,6 +218,7 @@ class Column(
internal const val TYPE_LOCAL_AROUND = 29
internal const val TYPE_FEDERATED_AROUND = 30
internal const val TYPE_ACCOUNT_AROUND = 31
internal const val TYPE_SCHEDULED_STATUS = 33
internal const val TAB_STATUS = 0
internal const val TAB_FOLLOWING = 1
@ -279,6 +280,7 @@ class Column(
TYPE_LIST_TL -> context.getString(R.string.list_timeline)
TYPE_DIRECT_MESSAGES -> context.getString(R.string.direct_messages)
TYPE_TREND_TAG -> context.getString(R.string.trend_tag)
TYPE_SCHEDULED_STATUS ->context.getString(R.string.scheduled_status)
else -> "?"
}
}
@ -317,6 +319,7 @@ class Column(
TYPE_LIST_TL -> R.attr.ic_list_tl
TYPE_DIRECT_MESSAGES -> R.attr.ic_mail
TYPE_TREND_TAG -> R.attr.ic_hashtag
TYPE_SCHEDULED_STATUS -> R.attr.ic_timer
else -> R.attr.ic_info
}
}
@ -803,8 +806,8 @@ class Column(
when(column_type) {
TYPE_CONVERSATION, TYPE_BOOSTED_BY, TYPE_FAVOURITED_BY, TYPE_LOCAL_AROUND, TYPE_FEDERATED_AROUND, TYPE_ACCOUNT_AROUND -> status_id =
when(isMisskey) {
TYPE_CONVERSATION, TYPE_BOOSTED_BY, TYPE_FAVOURITED_BY, TYPE_LOCAL_AROUND, TYPE_FEDERATED_AROUND, TYPE_ACCOUNT_AROUND ->
status_id = when(isMisskey) {
true -> EntityId.mayNull(src.parseString(KEY_STATUS_ID))
else -> EntityId.mayNull(src.parseLong(KEY_STATUS_ID))
}
@ -2582,6 +2585,18 @@ class Column(
}
private fun getScheduledStatuses(client:TootApiClient):TootApiResult?{
val result = client.request("/api/v1/scheduled_statuses")
val src = parser.statusList(result?.jsonArray)
list_tmp = addWithFilterStatus(list_tmp,src)
// TODO: paging?
idOld = null
idRecent = null
return result
}
override fun doInBackground(vararg unused : Void) : TootApiResult? {
ctStarted.set(true)
@ -3268,6 +3283,8 @@ class Column(
return result
}
TYPE_SCHEDULED_STATUS -> return getScheduledStatuses(client)
else -> return getStatuses(client, makeHomeTlUrl())
}
} finally {
@ -6199,6 +6216,7 @@ class Column(
TYPE_CONVERSATION,
TYPE_LIST_LIST,
TYPE_TREND_TAG,
TYPE_SCHEDULED_STATUS,
TYPE_FOLLOW_SUGGESTION -> true
TYPE_LIST_MEMBER,

View File

@ -0,0 +1,126 @@
package jp.juggler.subwaytooter.dialog
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.os.Build
import android.provider.Settings
import android.view.View
import android.view.WindowManager
import android.widget.Button
import android.widget.DatePicker
import android.widget.TimePicker
import jp.juggler.subwaytooter.R
import java.util.*
import android.provider.Settings.System.TIME_12_24
class DlgDateTime(
val activity : Activity
) : DatePicker.OnDateChangedListener, View.OnClickListener {
private lateinit var datePicker : DatePicker
private lateinit var timePicker : TimePicker
private lateinit var btnCancel : Button
private lateinit var btnOk : Button
private lateinit var dialog : Dialog
private lateinit var callback : (Long) -> Unit
@SuppressLint("InflateParams")
fun open(initialValue : Long, callback : (Long) -> Unit) {
this.callback = callback
val view = activity.layoutInflater.inflate(R.layout.dlg_date_time, null, false)
datePicker = view.findViewById(R.id.datePicker)
timePicker = view.findViewById(R.id.timePicker)
btnCancel = view.findViewById(R.id.btnCancel)
btnOk = view.findViewById(R.id.btnOk)
val c = GregorianCalendar.getInstance(TimeZone.getDefault())
c.timeInMillis = when(initialValue) {
0L -> System.currentTimeMillis() + 10 * 60000L
else -> initialValue
}
datePicker.firstDayOfWeek = Calendar.MONDAY
datePicker.init(
c.get(Calendar.YEAR),
c.get(Calendar.MONTH),
c.get(Calendar.DAY_OF_MONTH),
this
)
if(Build.VERSION.SDK_INT >= 23) {
timePicker.hour = c.get(Calendar.HOUR_OF_DAY)
timePicker.minute = c.get(Calendar.MINUTE)
} else {
@Suppress("DEPRECATION")
timePicker.currentHour = c.get(Calendar.HOUR_OF_DAY)
@Suppress("DEPRECATION")
timePicker.currentMinute = c.get(Calendar.MINUTE)
}
timePicker.setIs24HourView(
when(Settings.System.getString(activity.contentResolver, Settings.System.TIME_12_24)) {
"12" -> false
else -> true
}
)
btnCancel.setOnClickListener(this)
btnOk.setOnClickListener(this)
dialog = Dialog(activity)
dialog.setContentView(view)
dialog.window?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT
)
dialog.show()
}
override fun onClick(v : View) {
when(v.id) {
R.id.btnCancel -> dialog.cancel()
R.id.btnOk -> {
dialog.dismiss()
callback(getTime())
}
}
}
override fun onDateChanged(
view : DatePicker,
year : Int,
monthOfYear : Int,
dayOfMonth : Int
) {
// nothing to do
}
private fun getTime() : Long {
val y = datePicker.year
val m = datePicker.month
val d = datePicker.dayOfMonth
val h : Int
val j : Int
if(Build.VERSION.SDK_INT >= 23) {
h = timePicker.hour
j = timePicker.minute
} else {
@Suppress("DEPRECATION")
h = timePicker.currentHour
@Suppress("DEPRECATION")
j = timePicker.currentMinute
}
val c = GregorianCalendar.getInstance(TimeZone.getDefault())
c.set(y, m, d, h, j)
c.set(Calendar.SECOND, 0)
c.set(Calendar.MILLISECOND, 0)
return c.timeInMillis
}
}

View File

@ -202,7 +202,7 @@ object EmojiDecoder {
private interface ShortCodeSplitterCallback {
fun onString(part : String) // shortcode以外の文字列
fun onShortCode(part : String, name : String) // part : ":shortcode:", name : "shortcode"
fun onShortCode(prevCodePoint:Int,part : String, name : String) // part : ":shortcode:", name : "shortcode"
}
private fun splitShortCode(
@ -256,9 +256,16 @@ object EmojiDecoder {
continue
}
val prevCodePoint = if(start >0){
s.codePointBefore(start)
}else{
0x20
}
callback.onShortCode(
s.substring(start, posEndColon + 1) // ":shortcode:"
, s.substring(start + 1, posEndColon) // "shortcode"
prevCodePoint,
s.substring(start, posEndColon + 1), // ":shortcode:"
s.substring(start + 1, posEndColon) // "shortcode"
)
i = posEndColon + 1 // コロンの次の位置
@ -280,7 +287,7 @@ object EmojiDecoder {
builder.addUnicodeString(part)
}
override fun onShortCode(part : String, name : String) {
override fun onShortCode(prevCodePoint:Int,part : String, name : String) {
// フレニコのプロフ絵文字
if(emojiMapProfile != null && name.length >= 2 && name[0] == '@') {
@ -346,7 +353,7 @@ object EmojiDecoder {
sb.append(part)
}
override fun onShortCode(part : String, name : String) {
override fun onShortCode(prevCodePoint:Int,part : String, name : String) {
// カスタム絵文字にマッチするなら変換しない
val emojiCustom = emojiMapCustom?.get(name)

View File

@ -56,19 +56,25 @@ class PostHelper(
}
interface PostCompleteCallback {
fun onPostComplete(target_account : SavedAccount, status : TootStatus)
fun onScheduledPostComplete(target_account : SavedAccount)
}
///////////////////////////////////////////////////////////////////////////////////
// 投稿機能
var content : String? = null
var spoiler_text : String? = null
var visibility : TootVisibility = TootVisibility.Public
var bNSFW : Boolean = false
var bNSFW = false
var in_reply_to_id : EntityId? = null
var attachment_list : ArrayList<PostAttachment>? = null
var enquete_items : ArrayList<String>? = null
var emojiMapCustom : HashMap<String, CustomEmoji>? = null
var redraft_status_id : EntityId? = null
var useQuotedRenote : Boolean = false
var useQuotedRenote = false
var scheduledAt = 0L
private var last_post_tapped : Long = 0L
@ -88,6 +94,7 @@ class PostHelper(
val attachment_list = this.attachment_list
val enquete_items = this.enquete_items
val visibility = this.visibility
val scheduledAt = this.scheduledAt
val hasAttachment = attachment_list?.isNotEmpty() ?: false
@ -224,6 +231,8 @@ class PostHelper(
val parser = TootParser(activity, account)
var scheduledStatusSucceeded = false
fun getInstanceInformation(client : TootApiClient) : TootApiResult? {
val result = if(account.isMisskey) {
val params = JSONObject().apply {
@ -408,6 +417,10 @@ class PostHelper(
}
}
if(scheduledAt != 0L) {
return TootApiResult("misskey has no scheduled status API")
}
} else {
json.put(
"status",
@ -456,6 +469,25 @@ class PostHelper(
json.put("enquete_items", array)
}
if(scheduledAt != 0L) {
if(! instance.versionGE(TootInstance.VERSION_2_7_0_rc1)) {
return TootApiResult("Mastodon pre-2.7.0 has no scheduled status API")
}
// UTCの日時を渡す
val c = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC"))
c.timeInMillis = scheduledAt
val sv = String.format(
"%d-%02d-%02d %02d:%02d:%02d",
c.get(Calendar.YEAR),
c.get(Calendar.MONTH) + 1,
c.get(Calendar.DAY_OF_MONTH),
c.get(Calendar.HOUR_OF_DAY),
c.get(Calendar.MINUTE),
c.get(Calendar.SECOND)
)
json.put("scheduled_at", sv)
}
}
} catch(ex : JSONException) {
log.trace(ex)
@ -478,6 +510,14 @@ class PostHelper(
client.request("/api/v1/statuses", request_builder)
}
val jsonObject = result?.jsonObject
if(scheduledAt != 0L && jsonObject != null) {
// {"id":"3","scheduled_at":"2019-01-06T07:08:00.000Z","media_attachments":[]}
scheduledStatusSucceeded = true
return result
}
val status = parser.status(
if(isMisskey) {
result?.jsonObject?.optJSONObject("createdNote") ?: result?.jsonObject
@ -513,16 +553,23 @@ class PostHelper(
}
override fun handleResult(result : TootApiResult?) {
result ?: return // cancelled.
result ?: return
val status = this.status
if(status != null) {
// 連投してIdempotency が同じだった場合もエラーにはならず、ここを通る
callback(account, status)
} else {
showToast(activity, true, result.error)
when {
status != null -> {
// 連投してIdempotency が同じだった場合もエラーにはならず、ここを通る
callback.onPostComplete(account, status)
return
}
scheduledStatusSucceeded -> {
callback.onScheduledPostComplete(account)
return
}
else -> showToast(activity, true, result.error)
}
}
})
)
@ -711,13 +758,13 @@ class PostHelper(
val dst = ArrayList<CharSequence>()
if(instance?.isNotEmpty() == true) {
val custom_list = App1.custom_emoji_lister.getListWithAliases(
instance,
isMisskey,
onEmojiListLoad
)
if(custom_list != null) {
for(item in custom_list) {

View File

@ -2,8 +2,6 @@ package jp.juggler.subwaytooter.util
import android.content.DialogInterface
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.table.SavedAccount
/////////////////////////////////////////////////////////////////
@ -18,6 +16,5 @@ typealias SavedAccountCallback = (ai : SavedAccount) -> Unit
typealias DialogInterfaceCallback = (dialog: DialogInterface) -> Unit
typealias PostCompleteCallback = (target_account : SavedAccount, status : TootStatus) -> Unit
typealias ProgressResponseBodyCallback = (bytesRead : Long, bytesTotal : Long)->Unit

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M15,1L9,1v2h6L15,1zM11,14h2L13,8h-2v6zM19.03,7.39l1.42,-1.42c-0.43,-0.51 -0.9,-0.99 -1.41,-1.41l-1.42,1.42C16.07,4.74 14.12,4 12,4c-4.97,0 -9,4.03 -9,9s4.02,9 9,9 9,-4.03 9,-9c0,-2.12 -0.74,-4.07 -1.97,-5.61zM12,20c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M15,1L9,1v2h6L15,1zM11,14h2L13,8h-2v6zM19.03,7.39l1.42,-1.42c-0.43,-0.51 -0.9,-0.99 -1.41,-1.41l-1.42,1.42C16.07,4.74 14.12,4 12,4c-4.97,0 -9,4.03 -9,9s4.02,9 9,9 9,-4.03 9,-9c0,-2.12 -0.74,-4.07 -1.97,-5.61zM12,20c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
</vector>

View File

@ -225,7 +225,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPostFormBackground"
android:layout_marginBottom="32dp"
>
<jp.juggler.subwaytooter.view.MyEditText
@ -240,6 +240,44 @@
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/scheduled_status"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/tvSchedule"
android:gravity="center"
android:layout_gravity="center_vertical"
/>
<ImageButton
android:id="@+id/ibSchedule"
android:layout_width="40dp"
android:layout_height="40dp"
android:contentDescription="@string/edit"
android:src="?attr/ic_edit"
android:background="@drawable/btn_bg_transparent"
/>
<ImageButton
android:id="@+id/ibScheduleReset"
android:layout_width="40dp"
android:layout_height="40dp"
android:contentDescription="@string/reset"
android:src="?attr/btn_close"
android:background="@drawable/btn_bg_transparent"
/>
</LinearLayout>
<CheckBox
@ -247,6 +285,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/make_enquete"
android:layout_marginTop="32dp"
/>
<LinearLayout

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<DatePicker
android:id="@+id/datePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:datePickerMode="spinner"
android:calendarViewShown="false"
android:spinnersShown="true"
android:layout_gravity="center_horizontal"
/>
<TimePicker
android:id="@+id/timePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:timePickerMode="spinner"
android:layout_gravity="center_horizontal"
/>
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<Button
android:id="@+id/btnCancel"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/cancel"
/>
<Button
android:id="@+id/btnOk"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/ok"
/>
</LinearLayout>
</LinearLayout>

View File

@ -117,6 +117,10 @@
android:icon="?attr/ic_domain_block"
android:title="@string/blocked_domains"/>
<item
android:id="@+id/nav_scheduled_statuses_list"
android:icon="?attr/ic_timer"
android:title="@string/scheduled_status"/>
<!--<item-->
<!--android:id="@+id/nav_add_reports"-->
<!--android:icon="?attr/btn_report"-->

View File

@ -833,5 +833,8 @@
<string name="agree_terms">サービスの規約に同意します</string>
<string name="pseudo_account_cant_get_follow_list">疑似アカウントではフォローリストを読めません</string>
<string name="reaction_remove_confirm">あなたのリアクション \"%1$s\"を削除しますか?</string>
<string name="scheduled_status">予約投稿</string>
<string name="unspecified">指定なし</string>
<string name="scheduled_status_sent">予約投稿を送信しました。</string>
</resources>

View File

@ -157,5 +157,6 @@
<attr name="ic_local_lock_open" format="reference" />
<attr name="ic_local_ltl" format="reference" />
<attr name="ic_music" format="reference" />
<attr name="ic_timer" format="reference" />
</resources>

View File

@ -853,5 +853,8 @@
<string name="username_not_need_atmark">The user name must not contains \"@\" or \"/\".</string>
<string name="pseudo_account_cant_get_follow_list">Can\'t read follow list from pseudo account.</string>
<string name="reaction_remove_confirm">Remove your reaction \"%1$s\"?</string>
<string name="scheduled_status">Scheduled post</string>
<string name="unspecified">Unspecified</string>
<string name="scheduled_status_sent">Scheduled status was sent.</string>
</resources>

View File

@ -117,6 +117,7 @@
<item name="ic_local_lock_open">@drawable/ic_local_lock_open</item>
<item name="ic_local_ltl">@drawable/ic_local_ltl</item>
<item name="ic_music">@drawable/ic_music</item>
<item name="ic_timer">@drawable/ic_timer</item>
</style>
@ -244,6 +245,7 @@
<item name="ic_local_lock_open">@drawable/ic_local_lock_open_dark</item>
<item name="ic_local_ltl">@drawable/ic_local_ltl_dark</item>
<item name="ic_music">@drawable/ic_music_dark</item>
<item name="ic_timer">@drawable/ic_timer_dark</item>
</style>